From ce61749408e47a7870a4e015af44ee649fc97db4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Tue, 17 Nov 2020 13:55:05 -0500 Subject: [PATCH] swarm: remove a user/device from the conversation TODO Change-Id: I23b5d00e9b69dcc667cb52543d528171f9f34fdc GitLab: #298 GitLab: #299 --- src/jamidht/conversation.cpp | 77 +++- src/jamidht/conversation.h | 5 +- src/jamidht/conversationrepository.cpp | 146 ++++++- src/jamidht/conversationrepository.h | 3 + src/jamidht/jamiaccount.cpp | 53 ++- src/jamidht/jamiaccount.h | 4 +- test/unitTest/conversation/conversation.cpp | 460 +++++++++++++++++++- 7 files changed, 710 insertions(+), 38 deletions(-) diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index f10ff171ff..e32f1a87e4 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -59,6 +59,7 @@ public: } ~Impl() = default; + bool isAdmin() const; std::string repoPath() const; std::unique_ptr<ConversationRepository> repository_; @@ -73,6 +74,24 @@ public: std::deque<std::tuple<std::string, std::string, OnPullCb>> pullcbs_ {}; }; +bool +Conversation::Impl::isAdmin() const +{ + auto shared = account_.lock(); + if (!shared) + return false; + + auto adminsPath = repoPath() + DIR_SEPARATOR_STR + "admins"; + auto cert = shared->identity().second; + auto parentCert = cert->issuer; + if (!parentCert) { + JAMI_ERR("Parent cert is null!"); + return false; + } + auto uri = parentCert->getId().toString(); + return fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt")); +} + std::string Conversation::Impl::repoPath() const { @@ -164,14 +183,37 @@ Conversation::id() const std::string Conversation::addMember(const std::string& contactUri) { + if (isMember(contactUri, true)) { + JAMI_WARN("Could not add member %s because it's already a member", contactUri.c_str()); + return {}; + } + if (isBanned(contactUri)) { + JAMI_WARN("Could not add member %s because this member is banned", contactUri.c_str()); + return {}; + } // Add member files and commit return pimpl_->repository_->addMember(contactUri); } bool -Conversation::removeMember(const std::string& contactUri) +Conversation::removeMember(const std::string& contactUri, bool isDevice) { - // TODO + // Check if admin + if (!pimpl_->isAdmin()) { + JAMI_WARN("You're not an admin of this repo. Cannot ban %s", contactUri.c_str()); + return false; + } + // Vote for removal + if (pimpl_->repository_->voteKick(contactUri, isDevice).empty()) { + JAMI_WARN("Kicking %s failed", contactUri.c_str()); + return false; + } + // If admin, check vote + if (!pimpl_->repository_->resolveVote(contactUri, isDevice).empty()) { + JAMI_WARN("Vote solved for %s. %s banned", + contactUri.c_str(), + isDevice ? "Device" : "Member"); + } return true; } @@ -183,12 +225,9 @@ Conversation::getMembers(bool includeInvited) const if (!shared) return result; - auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR - + 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"; + auto adminsPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "admins"; + auto membersPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "members"; for (const auto& certificate : fileutils::readDirectory(adminsPath)) { if (certificate.find(".crt") == std::string::npos) { JAMI_WARN("Incorrect file found: %s/%s", adminsPath.c_str(), certificate.c_str()); @@ -229,18 +268,15 @@ Conversation::join() } bool -Conversation::isMember(const std::string& uri, bool includeInvited) +Conversation::isMember(const std::string& uri, bool includeInvited) const { auto shared = pimpl_->account_.lock(); if (!shared) return false; - auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR - + pimpl_->repository_->id(); - auto invitedPath = repoPath + DIR_SEPARATOR_STR + "invited"; - auto adminsPath = repoPath + DIR_SEPARATOR_STR + "admins"; - auto membersPath = repoPath + DIR_SEPARATOR_STR + "members"; + auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited"; + auto adminsPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "admins"; + auto membersPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "members"; std::vector<std::string> pathsToCheck = {adminsPath, membersPath}; if (includeInvited) pathsToCheck.emplace_back(invitedPath); @@ -259,6 +295,19 @@ Conversation::isMember(const std::string& uri, bool includeInvited) return false; } +bool +Conversation::isBanned(const std::string& uri, bool isDevice) const +{ + auto shared = pimpl_->account_.lock(); + if (!shared) + return true; + + auto type = isDevice ? "devices" : "members"; + auto bannedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + type + + DIR_SEPARATOR_STR + uri + ".crt"; + return fileutils::isFile(bannedPath); +} + std::string Conversation::sendMessage(const std::string& message, const std::string& type, diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index aab97c5a46..f3d93a2d12 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -52,7 +52,7 @@ public: * @return Commit id or empty if fails */ std::string addMember(const std::string& contactUri); - bool removeMember(const std::string& contactUri); + bool removeMember(const std::string& contactUri, bool isDevice); /** * @param includeInvited If we want invited members * @return a vector of member details: @@ -76,7 +76,8 @@ public: * @param uri URI to test * @return true if uri is a member */ - bool isMember(const std::string& uri, bool includInvited = false); + bool isMember(const std::string& uri, bool includeInvited = false) const; + bool isBanned(const std::string& uri, bool isDevice = false) const; // Message send std::string sendMessage(const std::string& message, diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 53e8974068..76c5243530 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -1210,7 +1210,7 @@ ConversationRepository::join() Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; - return pimpl_->commit(Json::writeString(wbuilder, json)); + return commitMessage(Json::writeString(wbuilder, json)); } std::string @@ -1281,7 +1281,7 @@ ConversationRepository::leave() Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; - return pimpl_->commit(Json::writeString(wbuilder, json)); + return commitMessage(Json::writeString(wbuilder, json)); } void @@ -1294,4 +1294,146 @@ ConversationRepository::erase() fileutils::removeAll(repoPath, true); } +std::string +ConversationRepository::voteKick(const std::string& uri, bool isDevice) +{ + // Add vote + commit + // TODO simplify + std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto account = pimpl_->account_.lock(); + if (!account) + return {}; + auto cert = account->identity().second; + auto parentCert = cert->issuer; + if (!parentCert) { + JAMI_ERR("Parent cert is null!"); + return {}; + } + auto adminUri = parentCert->getId().toString(); + if (adminUri == uri) { + JAMI_WARN("Admin tried to ban theirself"); + return {}; + } + + // TODO avoid duplicate + auto relativeVotePath = std::string("votes") + DIR_SEPARATOR_STR + + (isDevice ? "devices" : "members") + DIR_SEPARATOR_STR + uri; + auto voteDirectory = repoPath + DIR_SEPARATOR_STR + relativeVotePath; + if (!fileutils::recursive_mkdir(voteDirectory, 0700)) { + JAMI_ERR("Error when creating %s. Abort vote", voteDirectory.c_str()); + return {}; + } + auto votePath = fileutils::getFullPath(voteDirectory, adminUri); + auto voteFile = fileutils::ofstream(votePath, std::ios::trunc | std::ios::binary); + if (!voteFile.is_open()) { + JAMI_ERR("Could not write data to %s", votePath.c_str()); + return {}; + } + voteFile.close(); + + auto toAdd = fileutils::getFullPath(relativeVotePath, adminUri); + if (!pimpl_->add(toAdd.c_str())) + return {}; + + Json::Value json; + json["uri"] = uri; + json["type"] = "vote"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + return commitMessage(Json::writeString(wbuilder, json)); +} + +std::string +ConversationRepository::resolveVote(const std::string& uri, bool isDevice) +{ + // Count ratio admin/votes + auto nbAdmins = 0, nbVote = 0; + // For each admin, check if voted + std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + std::string adminsPath = repoPath + "admins"; + std::string membersPath = repoPath + "members"; + std::string devicesPath = repoPath + "devices"; + std::string bannedPath = repoPath + "banned"; + auto isAdmin = fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt")); + std::string type = "members"; + if (isDevice) + type = "devices"; + else if (isAdmin) + type = "admins"; + + auto voteDirectory = repoPath + DIR_SEPARATOR_STR + "votes" + DIR_SEPARATOR_STR + + (isDevice ? "devices" : "members") + DIR_SEPARATOR_STR + uri; + 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()); + continue; + } + auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size()); + nbAdmins += 1; + if (fileutils::isFile(fileutils::getFullPath(voteDirectory, adminUri))) + nbVote += 1; + } + + if (nbAdmins > 0 && (static_cast<double>(nbVote) / static_cast<double>(nbAdmins)) > .5) { + JAMI_WARN("More than half of the admins voted to ban %s, apply the ban", uri.c_str()); + + // Remove vote directory + fileutils::removeAll(voteDirectory, true); + + // Move from device or members file into banned + std::string originFilePath = membersPath; + if (isDevice) + originFilePath = devicesPath; + else if (isAdmin) + originFilePath = adminsPath; + originFilePath += DIR_SEPARATOR_STR + uri + ".crt"; + auto destPath = bannedPath + DIR_SEPARATOR_STR + (isDevice ? "devices" : "members"); + auto destFilePath = destPath + DIR_SEPARATOR_STR + uri + ".crt"; + if (!fileutils::recursive_mkdir(destPath, 0700)) { + JAMI_ERR("Error when creating %s. Abort resolving vote", destPath.c_str()); + return {}; + } + + if (std::rename(originFilePath.c_str(), destFilePath.c_str())) { + JAMI_ERR("Error when moving %s to %s. Abort resolving vote", + originFilePath.c_str(), + destFilePath.c_str()); + return {}; + } + + // If members, remove related devices + if (!isDevice) { + for (const auto& certificate : fileutils::readDirectory(devicesPath)) { + auto certPath = fileutils::getFullPath(devicesPath, certificate); + auto deviceCert = fileutils::loadTextFile(certPath); + try { + crypto::Certificate cert(deviceCert); + if (auto issuer = cert.issuer) + if (issuer->toString() == uri) + fileutils::remove(certPath, true); + } catch (...) { + continue; + } + } + } + + // Commit + if (!git_add_all(pimpl_->repository_.get())) + return {}; + + Json::Value json; + json["action"] = "ban"; + json["uri"] = uri; + json["type"] = "member"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + return commitMessage(Json::writeString(wbuilder, json)); + } + + // If vote nok + return {}; +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index a19a46d1ef..b5492f4027 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -187,6 +187,9 @@ public: */ void erase(); + std::string voteKick(const std::string& uri, bool isDevice); + std::string resolveVote(const std::string& uri, bool isDevice); + private: ConversationRepository() = delete; class Impl; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index bfda853359..b9a658f5b6 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -4105,6 +4105,15 @@ JamiAccount::addConversationMember(const std::string& conversationId, JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); return false; } + + if (it->second->isMember(contactUri, true)) { + JAMI_DBG("%s is already a member of %s, resend invite", contactUri.c_str(), conversationId.c_str()); + // Note: This should not be necessary, but if for whatever reason the other side didn't join + // we should not forbid new invites + sendTextMessage(contactUri, it->second->generateInvitation()); + return true; + } + auto commitId = it->second->addMember(contactUri); if (commitId.empty()) { JAMI_WARN("Couldn't add %s to %s", contactUri.c_str(), conversationId.c_str()); @@ -4131,11 +4140,34 @@ JamiAccount::addConversationMember(const std::string& conversationId, bool JamiAccount::removeConversationMember(const std::string& conversationId, - const std::string& contactUri) + const std::string& contactUri, + bool isDevice) { std::lock_guard<std::mutex> lk(conversationsMtx_); - conversations_[conversationId]->removeMember(contactUri); - return true; + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + auto lastCommit = conversation->second->lastCommitId(); + if (conversation->second->removeMember(contactUri, isDevice)) { + conversation->second->loadMessages([w=weak(), lastCommit, conversationId] (auto&& messages) { + if (auto shared = w.lock()) { + std::reverse(messages.begin(), messages.end()); + // Announce new commits + for (const auto& msg : messages) { + emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), + conversationId, + msg); + } + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + auto conversation = shared->conversations_.find(conversationId); + // Send notification for others + if (conversation != shared->conversations_.end() && conversation->second) + shared->sendMessageNotification(*conversation->second, lastCommit, true); + } + }, "", lastCommit); + return true; + } + } + return false; } std::vector<std::map<std::string, std::string>> @@ -4225,6 +4257,21 @@ JamiAccount::fetchNewCommits(const std::string& peer, std::unique_lock<std::mutex> lk(conversationsMtx_); auto conversation = conversations_.find(conversationId); if (conversation != conversations_.end() && conversation->second) { + if (!conversation->second->isMember(peer, true)) { + JAMI_WARN("[Account %s] %s is not a member of %s", + getAccountID().c_str(), + peer.c_str(), + conversationId.c_str()); + return; + } + if (conversation->second->isBanned(deviceId)) { + JAMI_WARN("[Account %s] %s is a banned device in conversation %s", + getAccountID().c_str(), + deviceId.c_str(), + conversationId.c_str()); + return; + } + // Retrieve current last message auto lastMessageId = conversation->second->lastCommitId(); if (lastMessageId.empty()) { diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 6fcc1bfb2d..9e7dcbd19e 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -518,7 +518,9 @@ public: bool addConversationMember(const std::string& conversationId, const std::string& contactUri, bool sendRequest = true); - bool removeConversationMember(const std::string& conversationId, const std::string& contactUri); + bool removeConversationMember(const std::string& conversationId, + const std::string& contactUri, + bool isDevice = false); std::vector<std::map<std::string, std::string>> getConversationMembers( const std::string& conversationId); diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 3aff208e79..58f296fe7f 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -52,6 +52,7 @@ public: std::string aliceId; std::string bobId; + std::string bob2Id; std::string carlaId; private: @@ -71,23 +72,43 @@ private: void testDeclineRequest(); void testSendMessageToMultipleParticipants(); void testPingPongMessages(); + void testRemoveMember(); + //void testBanDevice(); + void testMemberTryToRemoveAdmin(); + void testBannedMemberCannotSendMessage(); + // void testBannedDeviceCannotSendMessageButMemberCan(); + // void test2AdminsCannotBanEachOthers(); + void testAddBannedMember(); + // void testRevokedDeviceCannotSendMessage(); + // void test2AdminsBanMembers(); + // void test2AdminsBanOtherAdmin(); + // void testCheckAdminFakeAVoteIsDetected(); + void testAdminCannotKickTheirself(); + // void testDetectionAdminKickedHimself(); + // void testAdminRemoveConversationShouldPromoteOther(); CPPUNIT_TEST_SUITE(ConversationTest); - CPPUNIT_TEST(testCreateConversation); - CPPUNIT_TEST(testGetConversation); - CPPUNIT_TEST(testGetConversationsAfterRm); - CPPUNIT_TEST(testRemoveInvalidConversation); - CPPUNIT_TEST(testRemoveConversationNoMember); - CPPUNIT_TEST(testRemoveConversationWithMember); - 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(testCreateConversation); + // CPPUNIT_TEST(testGetConversation); + // CPPUNIT_TEST(testGetConversationsAfterRm); + // CPPUNIT_TEST(testRemoveInvalidConversation); + // CPPUNIT_TEST(testRemoveConversationNoMember); + // CPPUNIT_TEST(testRemoveConversationWithMember); + // 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(testRemoveMember); + // CPPUNIT_TEST(testBanDevice); + // CPPUNIT_TEST(testMemberTryToRemoveAdmin); + // CPPUNIT_TEST(testBannedMemberCannotSendMessage); + CPPUNIT_TEST(testAddBannedMember); + // CPPUNIT_TEST(testAdminCannotKickTheirself); CPPUNIT_TEST_SUITE_END(); }; @@ -163,9 +184,15 @@ ConversationTest::tearDown() Manager::instance().removeAccount(aliceId, true); Manager::instance().removeAccount(bobId, true); Manager::instance().removeAccount(carlaId, true); + if (!bob2Id.empty()) + Manager::instance().removeAccount(bob2Id, true); + + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + // Because cppunit is not linked with dbus, just poll if removed for (int i = 0; i < 40; ++i) { - if (Manager::instance().getAccountList().size() <= currentAccSize - 3) + if (Manager::instance().getAccountList().size() <= currentAccSize - bob2Id.empty() ? 3 : 4) break; std::this_thread::sleep_for(std::chrono::milliseconds(100)); } @@ -980,6 +1007,407 @@ ConversationTest::testPingPongMessages() DRing::unregisterSignalHandlers(); } +void +ConversationTest::testRemoveMember() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + 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, memberMessageGenerated = false, + voteMessageGenerated = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + cv.notify_one(); + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + // Now check that alice, has the only admin, can remove bob + memberMessageGenerated = false; + voteMessageGenerated = false; + aliceAccount->removeConversationMember(convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && voteMessageGenerated; + })); + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 1); + CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername()); + CPPUNIT_ASSERT(members[0]["role"] == "admin"); +} + +/*void +ConversationTest::testBanDevice() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + auto bobDeviceId = std::string(bobAccount->currentDeviceId()); + 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, memberMessageGenerated = false, + voteMessageGenerated = false, bob2GetMessage = false, bobGetMessage = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId* /, + const std::string& /* conversationId * /, + std::map<std::string, std::string> /*metadatas* /) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if ((accountId == bobId || accountId == bob2Id) && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + } else if (accountId == bobId) { + bobGetMessage = true; + } else if (accountId == bob2Id) { + bob2GetMessage = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + // Add second device for Bob + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + bobAccount->exportArchive(bobArchive); + std::map<std::string, std::string> details = DRing::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; + bob2Id = Manager::instance().addAccount(details); + + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto bob2Account = Manager::instance().getAccount<JamiAccount>(bob2Id); + auto details = bob2Account->getVolatileAccountDetails(); + auto daemonStatus = details[DRing::Account::ConfProperties::Registration::STATUS]; + if (daemonStatus == "REGISTERED") + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + + conversationReady = false; + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(60), [&]() { return conversationReady; })); + + // Now check that alice, has the only admin, can remove bob + memberMessageGenerated = false; + voteMessageGenerated = false; + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 2); + aliceAccount->removeConversationMember(convId, bobDeviceId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && voteMessageGenerated; + })); + + auto bannedFile = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId + + DIR_SEPARATOR_STR + "banned" + DIR_SEPARATOR_STR + "devices" + + DIR_SEPARATOR_STR + bobDeviceId + ".crt"; + CPPUNIT_ASSERT(fileutils::isFile(bannedFile)); + members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 2); + + // Assert that bob2 get the message, not Bob + CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(10), [&]() { return bobGetMessage; })); + CPPUNIT_ASSERT(bob2GetMessage && !bobGetMessage); + DRing::unregisterSignalHandlers(); +}*/ + +void +ConversationTest::testMemberTryToRemoveAdmin() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + 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, memberMessageGenerated = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + // Now check that alice, has the only admin, can remove bob + memberMessageGenerated = false; + bobAccount->removeConversationMember(convId, aliceUri); + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 2 && !memberMessageGenerated); +} + +void +ConversationTest::testBannedMemberCannotSendMessage() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + 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, memberMessageGenerated = false, + voteMessageGenerated = false, aliceMessageReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "plain/text") { + aliceMessageReceived = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + memberMessageGenerated = false; + voteMessageGenerated = false; + aliceAccount->removeConversationMember(convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && voteMessageGenerated; + })); + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 1); + + // Now check that alice doesn't receive a message from Bob + aliceMessageReceived = false; + bobAccount->sendMessage(convId, "hi"); + CPPUNIT_ASSERT( + !cv.wait_for(lk, std::chrono::seconds(30), [&]() { return aliceMessageReceived; })); +} + +void +ConversationTest::testAddBannedMember() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + 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, memberMessageGenerated = false, + voteMessageGenerated = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + cv.notify_one(); + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + // Now check that alice, has the only admin, can remove bob + memberMessageGenerated = false; + voteMessageGenerated = false; + aliceAccount->removeConversationMember(convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return memberMessageGenerated && voteMessageGenerated; + })); + + // Then check that bobUri cannot be re-added + CPPUNIT_ASSERT(!aliceAccount->addConversationMember(convId, bobUri)); +} + +void +ConversationTest::testAdminCannotKickTheirself() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + 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, memberMessageGenerated = false, + voteMessageGenerated = false, aliceMessageReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "member") { + memberMessageGenerated = true; + } else if (accountId == aliceId && conversationId == convId + && message["type"] == "plain/text") { + aliceMessageReceived = true; + } + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + auto members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 1); + aliceAccount->removeConversationMember(convId, aliceUri); + members = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(members.size() == 1); +} + } // namespace test } // namespace jami -- GitLab