diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index a978d063915faccefbf77ef92022bb8e7c81524e..8b39558b88bc12806bce7bc9bb753c6802fc7b61 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1707,6 +1707,28 @@ <arg type="s" name="accountId" direction="in"/> </method> + <method name="updateConversationInfos" tp:name-for-bindings="updateConversationInfos"> + <tp:added version="10.0.0"/> + <tp:docstring> + Update conversation's infos (supported keys: title, description, avatar) + </tp:docstring> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="conversationId" direction="in"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.In2" value="MapStringString"/> + <arg type="a{ss}" name="infos" direction="in"/> + </method> + + <method name="conversationInfos" tp:name-for-bindings="conversationInfos"> + <tp:added version="10.0.0"/> + <tp:docstring> + Get conversation's infos (mode, title, description, avatar) + </tp:docstring> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="MapStringString"/> + <arg type="a{ss}" name="infos" direction="out"/> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="conversationId" direction="in"/> + </method> + <method name="addConversationMember" tp:name-for-bindings="addConversationMember"> <tp:added version="10.0.0"/> <tp:docstring> diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 1dd4e1c1d4100e8d079b7bbf9310831e93c3991d..aea2a4d1d1f826213880dfe15192edfe837fff36 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -538,7 +538,9 @@ DBusConfigurationManager::setAccountsOrder(const std::string& order) } auto -DBusConfigurationManager::validateCertificate(const std::string& accountId, const std::string& certificate) -> decltype(DRing::validateCertificate(accountId, certificate)) +DBusConfigurationManager::validateCertificate(const std::string& accountId, + const std::string& certificate) + -> decltype(DRing::validateCertificate(accountId, certificate)) { return DRing::validateCertificate(accountId, certificate); } @@ -856,6 +858,21 @@ DBusConfigurationManager::getConversationRequests(const std::string& accountId) return DRing::getConversationRequests(accountId); } +void +DBusConfigurationManager::updateConversationInfos(const std::string& accountId, + const std::string& conversationId, + const std::map<std::string, std::string>& infos) +{ + DRing::updateConversationInfos(accountId, conversationId, infos); +} + +std::map<std::string, std::string> +DBusConfigurationManager::conversationInfos(const std::string& accountId, + const std::string& conversationId) +{ + return DRing::conversationInfos(accountId, conversationId); +} + bool DBusConfigurationManager::addConversationMember(const std::string& accountId, const std::string& conversationId, diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index e40620fa606f0853bd56c3c0dc1939cc7e6b177f..c471fd08b88ec9c60c221a2f08931994b1187b6b 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -195,6 +195,8 @@ public: bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId); + void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map<std::string, std::string>& infos); + std::map<std::string, std::string> conversationInfos(const std::string& accountId, const std::string& conversationId); bool addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index 1427bd08ad8f5be8def108299ab046f6873da1d3..82a36b6e74f97162bdc50b3820f4a291c1a3578a 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -46,6 +46,8 @@ namespace DRing { bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId); + void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map<std::string, std::string>& infos); + std::map<std::string, std::string> conversationInfos(const std::string& accountId, const std::string& conversationId); // Member management bool addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index af662e05ec08345d53971cf08e7f2a5666e5e66f..cd522022b6b07662fa749644ca01005fbe6ef328 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -59,6 +59,8 @@ namespace DRing { bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId); + void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map<std::string, std::string>& infos); + std::map<std::string, std::string> conversationInfos(const std::string& accountId, const std::string& conversationId); // Member management bool addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); diff --git a/src/Makefile.am b/src/Makefile.am index 346d88b2108eef39c39e24023e331ed4523e0cde..dab1cfc8beca06e3d0e21ebb5261f356c681d76b 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -183,7 +183,8 @@ libring_la_SOURCES = \ generic_io.h \ scheduled_executor.h \ scheduled_executor.cpp \ - transport/peer_channel.h + transport/peer_channel.h \ + vcard.h if HAVE_WIN32 libring_la_SOURCES += \ diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp index 4f0c3cf98de0280977cce44ca5e3320e43b08f48..2f7379174e5ae520526ac4a3a95b561875a25bd2 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -80,6 +80,23 @@ getConversationRequests(const std::string& accountId) return {}; } +void +updateConversationInfos(const std::string& accountId, + const std::string& conversationId, + const std::map<std::string, std::string>& infos) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + acc->updateConversationInfos(conversationId, infos); +} + +std::map<std::string, std::string> +conversationInfos(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + return acc->conversationInfos(conversationId); + return {}; +} + // Member management bool addConversationMember(const std::string& accountId, diff --git a/src/dring/conversation_interface.h b/src/dring/conversation_interface.h index 9e33a649c2f8570cf421c2d8ac1a805e9815eb32..15ea61a6caa4181e93ecd6c98f1e2f63999bb228 100644 --- a/src/dring/conversation_interface.h +++ b/src/dring/conversation_interface.h @@ -43,6 +43,13 @@ DRING_PUBLIC std::vector<std::string> getConversations(const std::string& accoun DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationRequests( const std::string& accountId); +// Conversation's infos management +DRING_PUBLIC void updateConversationInfos(const std::string& accountId, + const std::string& conversationId, + const std::map<std::string, std::string>& infos); +DRING_PUBLIC std::map<std::string, std::string> conversationInfos(const std::string& accountId, + const std::string& conversationId); + // Member management DRING_PUBLIC bool addConversationMember(const std::string& accountId, const std::string& conversationId, diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 2acce647948195c49b6b37175ebff7cb4b658e87..6d4a6cc53b374c9578b4f7a64dd93fe64a72cda1 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -534,8 +534,10 @@ Conversation::generateInvitation() const // Invite the new member to the conversation std::map<std::string, std::string> invite; Json::Value root; + for (const auto& [k, v] : infos()) { + root["metadatas"][k] = v; + } root["conversationId"] = id(); - // TODO metadatas Json::StreamWriterBuilder wbuilder; wbuilder["commentStyle"] = "None"; wbuilder["indentation"] = ""; @@ -587,4 +589,26 @@ Conversation::isInitialMember(const std::string& uri) const return std::find(members.begin(), members.end(), uri) != members.end(); } +std::string +Conversation::updateInfos(const std::map<std::string, std::string>& map) +{ + return pimpl_->repository_->updateInfos(map); +} + +std::map<std::string, std::string> +Conversation::infos() const +{ + return pimpl_->repository_->infos(); +} + +std::vector<uint8_t> +Conversation::vCard() const +{ + try { + return fileutils::loadFile(pimpl_->repoPath() + DIR_SEPARATOR_STR + "profile.vcf"); + } catch (...) { + } + return {}; +} + } // namespace jami \ No newline at end of file diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index eb7e5365b6a7b9ce03656dab571554497a5d5969..52bd889e21f981dd347271fa9af55fe49a0bf4c6 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -32,14 +32,17 @@ class JamiAccount; class ConversationRepository; enum class ConversationMode; -using OnPullCb = std::function<void(bool fetchOk,std::vector<std::map<std::string, std::string>>&& newMessages)>; -using OnLoadMessages = std::function<void(std::vector<std::map<std::string, std::string>>&& messages)>; - +using OnPullCb = std::function<void(bool fetchOk, + std::vector<std::map<std::string, std::string>>&& newMessages)>; +using OnLoadMessages + = std::function<void(std::vector<std::map<std::string, std::string>>&& messages)>; class Conversation : public std::enable_shared_from_this<Conversation> { public: - Conversation(const std::weak_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember = ""); + Conversation(const std::weak_ptr<JamiAccount>& account, + ConversationMode mode, + const std::string& otherMember = ""); Conversation(const std::weak_ptr<JamiAccount>& account, const std::string& conversationId = ""); Conversation(const std::weak_ptr<JamiAccount>& account, const std::string& remoteDevice, @@ -100,7 +103,9 @@ public: * @param fromMessage The most recent message ("" = last (default)) * @param toMessage The oldest message ("" = last (default)), no limit */ - void loadMessages(const OnLoadMessages& cb, const std::string& fromMessage = "", const std::string& toMessage = ""); + void loadMessages(const OnLoadMessages& cb, + const std::string& fromMessage = "", + const std::string& toMessage = ""); /** * Get last commit id * @return last commit id @@ -170,8 +175,22 @@ public: */ std::vector<std::string> getInitialMembers() const; bool isInitialMember(const std::string& uri) const; -private: + /** + * Change repository's infos + * @param map New infos (supported keys: title, description, avatar) + * @return the commit id + */ + std::string updateInfos(const std::map<std::string, std::string>& map); + + /** + * Retrieve current infos (title, description, avatar, mode) + * @return infos + */ + std::map<std::string, std::string> infos() const; + std::vector<uint8_t> vCard() const; + +private: std::shared_ptr<Conversation> shared() { return std::static_pointer_cast<Conversation>(shared_from_this()); diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 693dd27aa06b07e4abd74aff9b11dd8a0d7a9b7c..69844babf043732f1db1c57bd24c42a77c04b9e7 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -24,6 +24,7 @@ #include "gittransport.h" #include "string_utils.h" #include "client/ring_signal.h" +#include "vcard.h" using random_device = dht::crypto::random_device; @@ -47,23 +48,19 @@ public: : account_(account) , id_(id) { - repository_ = repository(); - if (!repository_) - throw std::logic_error("Couldn't initialize repo"); - initMembers(); } + // NOTE! We use temporary GitRepository to avoid to keep file opened (TODO check why + // git_remote_fetch() leaves pack-data opened) GitRepository repository() const { - // TODO use only one object auto shared = account_.lock(); if (!shared) return {nullptr, git_repository_free}; auto path = fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + id_; git_repository* repo = nullptr; - // TODO share this repo with GitServer if (git_repository_open(&repo, path.c_str()) != 0) return {nullptr, git_repository_free}; return {std::move(repo), git_repository_free}; @@ -94,12 +91,16 @@ public: const std::string& uriMember, const std::string& commitid, const std::string& parentId) const; + bool checkValidProfileUpdate(const std::string& userDevice, + const std::string& commitid, + const std::string& parentId) const; bool add(const std::string& path); std::string commit(const std::string& msg); ConversationMode mode() const; - GitDiff diff(const std::string& idNew, const std::string& idOld) const; + // NOTE! GitDiff needs to be deteleted before repo + GitDiff diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const; std::string diffStats(const std::string& newId, const std::string& oldId) const; std::string diffStats(const GitDiff& diff) const; @@ -109,13 +110,12 @@ public: unsigned n) const; GitObject fileAtTree(const std::string& path, const GitTree& tree) const; - GitTree treeAtCommit(const std::string& commitId) const; + // NOTE! GitDiff needs to be deteleted before repo + GitTree treeAtCommit(git_repository* repo, const std::string& commitId) const; std::string getCommitType(const std::string& commitMsg) const; std::vector<std::string> getInitialMembers() const; - GitRepository repository_ {nullptr, git_repository_free}; - std::weak_ptr<JamiAccount> account_; const std::string id_; mutable std::optional<ConversationMode> mode_ {}; @@ -130,7 +130,12 @@ public: return members_; } + bool resolveConflicts(git_index* index, const std::string& other_id); + void initMembers(); + + // Permissions + MemberRole updateProfilePermLvl_ {MemberRole::ADMIN}; }; ///////////////////////////////////////////////////////////////////////////////// @@ -387,7 +392,8 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str { // The merge will occur between current HEAD and wanted_ref git_reference* head_ref_ptr = nullptr; - if (git_repository_head(&head_ref_ptr, repository_.get()) < 0) { + auto repo = repository(); + if (git_repository_head(&head_ref_ptr, repo.get()) < 0) { JAMI_ERR("Could not get HEAD reference"); return false; } @@ -395,7 +401,7 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str // Maybe that's a ref, so DWIM it git_reference* merge_ref_ptr = nullptr; - git_reference_dwim(&merge_ref_ptr, repository_.get(), wanted_ref.c_str()); + git_reference_dwim(&merge_ref_ptr, repo.get(), wanted_ref.c_str()); GitReference merge_ref {merge_ref_ptr, git_reference_free}; GitSignature sig {signature()}; @@ -424,13 +430,12 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str return false; } git_annotated_commit* annotated_ptr = nullptr; - if (git_annotated_commit_lookup(&annotated_ptr, repository_.get(), &commit_id) < 0) { + if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) { JAMI_ERR("Couldn't lookup commit %s", wanted_ref.c_str()); return false; } GitAnnotatedCommit annotated {annotated_ptr, git_annotated_commit_free}; - if (git_commit_lookup(&parent, repository_.get(), git_annotated_commit_id(annotated.get())) - < 0) { + if (git_commit_lookup(&parent, repo.get(), git_annotated_commit_id(annotated.get())) < 0) { JAMI_ERR("Couldn't lookup commit %s", wanted_ref.c_str()); return false; } @@ -439,11 +444,14 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str // Prepare our commit tree git_oid tree_oid; git_tree* tree = nullptr; - if (git_index_write_tree(&tree_oid, index) < 0) { + if (git_index_write_tree_to(&tree_oid, index, repo.get()) < 0) { + const git_error* err = giterr_last(); + if (err) + JAMI_ERR("XXX checkout index: %s", err->message); JAMI_ERR("Couldn't write index"); return false; } - if (git_tree_lookup(&tree, repository_.get(), &tree_oid) < 0) { + if (git_tree_lookup(&tree, repo.get(), &tree_oid) < 0) { JAMI_ERR("Couldn't lookup tree"); return false; } @@ -452,7 +460,7 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str git_buf to_sign = {}; const git_commit* parents_ptr[2] {parents[0].get(), parents[1].get()}; if (git_commit_create_buffer(&to_sign, - repository_.get(), + repo.get(), sig.get(), sig.get(), nullptr, @@ -474,7 +482,7 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str std::string signed_str = base64::encode(signed_buf); git_oid commit_oid; if (git_commit_create_with_signature(&commit_oid, - repository_.get(), + repo.get(), to_sign.ptr, signed_str.c_str(), "signature") @@ -488,20 +496,24 @@ ConversationRepository::Impl::createMergeCommit(git_index* index, const std::str JAMI_INFO("New merge commit added with id: %s", commit_str); // Move commit to main branch git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, - repository_.get(), - "refs/heads/main", - &commit_oid, - true, - nullptr) + if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_oid, true, nullptr) < 0) { JAMI_WARN("Could not move commit to main"); } git_reference_free(ref_ptr); } - // We're done merging, cleanup the repository state - git_repository_state_cleanup(repository_.get()); + // We're done merging, cleanup the repository state & index + git_repository_state_cleanup(repo.get()); + + git_object* target_ptr = nullptr; + if (git_object_lookup(&target_ptr, repo.get(), &commit_oid, GIT_OBJ_COMMIT) != 0) { + JAMI_ERR("failed to lookup OID %s", git_oid_tostr_s(&commit_oid)); + return false; + } + GitObject target {target_ptr, git_object_free}; + + git_reset(repo.get(), target.get(), GIT_RESET_HARD, nullptr); return true; } @@ -515,7 +527,7 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is if (is_unborn) { git_reference* head_ref_ptr = nullptr; // HEAD reference is unborn, lookup manually so we don't try to resolve it - if (git_reference_lookup(&head_ref_ptr, repository_.get(), "HEAD") < 0) { + if (git_reference_lookup(&head_ref_ptr, repo.get(), "HEAD") < 0) { JAMI_ERR("failed to lookup HEAD ref"); return false; } @@ -525,18 +537,13 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get()); // Create our main reference on the target OID - if (git_reference_create(&target_ref_ptr, - repository_.get(), - symbolic_ref, - target_oid, - 0, - nullptr) + if (git_reference_create(&target_ref_ptr, repo.get(), symbolic_ref, target_oid, 0, nullptr) < 0) { JAMI_ERR("failed to create main reference"); return false; } - } else if (git_repository_head(&target_ref_ptr, repository_.get()) < 0) { + } else if (git_repository_head(&target_ref_ptr, repo.get()) < 0) { // HEAD exists, just lookup and resolve JAMI_ERR("failed to get HEAD reference"); return false; @@ -545,7 +552,7 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is // Lookup the target object git_object* target_ptr = nullptr; - if (git_object_lookup(&target_ptr, repository_.get(), target_oid, GIT_OBJ_COMMIT) != 0) { + if (git_object_lookup(&target_ptr, repo.get(), target_oid, GIT_OBJ_COMMIT) != 0) { JAMI_ERR("failed to lookup OID %s", git_oid_tostr_s(target_oid)); return false; } @@ -555,7 +562,7 @@ ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is git_checkout_options ff_checkout_options; git_checkout_init_options(&ff_checkout_options, GIT_CHECKOUT_OPTIONS_VERSION); ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE; - if (git_checkout_tree(repository_.get(), target.get(), &ff_checkout_options) != 0) { + if (git_checkout_tree(repo.get(), target.get(), &ff_checkout_options) != 0) { JAMI_ERR("failed to checkout HEAD reference"); return false; } @@ -578,7 +585,7 @@ ConversationRepository::Impl::add(const std::string& path) if (!repo) return false; git_index* index_ptr = nullptr; - if (git_repository_index(&index_ptr, repository_.get()) < 0) { + if (git_repository_index(&index_ptr, repo.get()) < 0) { JAMI_ERR("Could not open repository index"); return false; } @@ -611,8 +618,9 @@ ConversationRepository::Impl::checkOnlyDeviceCertificate(const std::string& user } // Retrieve tree for recent commit - auto treeNew = treeAtCommit(commitId); - auto treeOld = treeAtCommit(parentId); + auto repo = repository(); + auto treeNew = treeAtCommit(repo.get(), commitId); + auto treeOld = treeAtCommit(repo.get(), parentId); if (not treeNew or not treeOld) return false; if (!fileAtTree(deviceFile, treeNew)) { @@ -655,8 +663,9 @@ ConversationRepository::Impl::checkVote(const std::string& userDevice, return false; } - auto treeNew = treeAtCommit(commitId); - auto treeOld = treeAtCommit(parentId); + auto repo = repository(); + auto treeNew = treeAtCommit(repo.get(), commitId); + auto treeOld = treeAtCommit(repo.get(), parentId); if (not treeNew or not treeOld) return false; @@ -758,7 +767,7 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice, auto userUri = cert->getIssuerUID(); auto repo = repository(); - std::string repoPath = git_repository_workdir(repository_.get()); + std::string repoPath = git_repository_workdir(repo.get()); if (mode() == ConversationMode::ONE_TO_ONE) { auto initialMembers = getInitialMembers(); auto it = std::find(initialMembers.begin(), initialMembers.end(), uriMember); @@ -801,10 +810,10 @@ ConversationRepository::Impl::checkValidAdd(const std::string& userDevice, } } - auto treeOld = treeAtCommit(parentId); + auto treeOld = treeAtCommit(repo.get(), parentId); if (not treeOld) return false; - auto treeNew = treeAtCommit(commitId); + auto treeNew = treeAtCommit(repo.get(), commitId); if (not treeNew) return false; auto blob_invite = fileAtTree(invitedFile, treeNew); @@ -854,8 +863,9 @@ ConversationRepository::Impl::checkValidJoins(const std::string& userDevice, auto deviceFile = std::string("devices") + DIR_SEPARATOR_STR + userDevice + ".crt"; // Retrieve tree for commits - auto treeNew = treeAtCommit(commitId); - auto treeOld = treeAtCommit(parentId); + auto repo = repository(); + auto treeNew = treeAtCommit(repo.get(), commitId); + auto treeOld = treeAtCommit(repo.get(), parentId); if (not treeNew or not treeOld) return false; @@ -907,8 +917,9 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, auto removeSelf = userUri == uriMember; // Retrieve tree for recent commit - auto treeNew = treeAtCommit(commitId); - auto treeOld = treeAtCommit(parentId); + auto repo = repository(); + auto treeNew = treeAtCommit(repo.get(), commitId); + auto treeOld = treeAtCommit(repo.get(), parentId); if (not treeNew or not treeOld) return false; @@ -986,8 +997,7 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, // If not for self check that vote is valid and not added auto nbAdmins = 0; auto nbVotes = 0; - auto repo = repository(); - std::string repoPath = git_repository_workdir(repository_.get()); + std::string repoPath = git_repository_workdir(repo.get()); for (const auto& certificate : fileutils::readDirectory(repoPath + "admins")) { if (certificate.find(".crt") == std::string::npos) { JAMI_WARN("Incorrect file found: %s", certificate.c_str()); @@ -1009,6 +1019,51 @@ ConversationRepository::Impl::checkValidRemove(const std::string& userDevice, return !bannedFiles.empty(); } +bool +ConversationRepository::Impl::checkValidProfileUpdate(const std::string& userDevice, + const std::string& commitId, + const std::string& parentId) const +{ + auto cert = tls::CertificateStore::instance().getCertificate(userDevice); + if (!cert && cert->issuer) + return false; + auto userUri = cert->issuer->getId().toString(); + auto valid = false; + { + std::lock_guard<std::mutex> lk(membersMtx_); + for (const auto& member : members_) { + if (member.uri == userUri) { + valid = member.role <= updateProfilePermLvl_; + break; + } + } + } + if (!valid) { + JAMI_ERR("Profile changed from unauthorized user: %s", userDevice.c_str()); + return false; + } + + // Retrieve tree for recent commit + auto repo = repository(); + auto treeNew = treeAtCommit(repo.get(), commitId); + auto treeOld = treeAtCommit(repo.get(), parentId); + if (not treeNew or not treeOld) + return false; + + auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId)); + // Check that no weird file is added nor removed + for (const auto& f : changedFiles) { + if (f == "profile.vcf") { + // Ignore + } else { + JAMI_ERR("Unwanted changed file detected: %s", f.c_str()); + return false; + } + } + + return true; +} + bool ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice, const std::string& commitId) const @@ -1019,7 +1074,8 @@ ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice, auto userUri = cert->getIssuerUID(); // Retrieve tree for commit - auto tree = treeAtCommit(commitId); + auto repo = repository(); + auto tree = treeAtCommit(repo.get(), commitId); if (not tree) return false; @@ -1119,7 +1175,7 @@ ConversationRepository::Impl::commit(const std::string& msg) // Retrieve current index git_index* index_ptr = nullptr; auto repo = repository(); - if (git_repository_index(&index_ptr, repository_.get()) < 0) { + if (git_repository_index(&index_ptr, repo.get()) < 0) { JAMI_ERR("Could not open repository index"); return {}; } @@ -1132,20 +1188,20 @@ ConversationRepository::Impl::commit(const std::string& msg) } git_tree* tree_ptr = nullptr; - if (git_tree_lookup(&tree_ptr, repository_.get(), &tree_id) < 0) { + if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) { JAMI_ERR("Could not look up initial tree"); return {}; } GitTree tree = {tree_ptr, git_tree_free}; git_oid commit_id; - if (git_reference_name_to_id(&commit_id, repository_.get(), "HEAD") < 0) { + if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); return {}; } git_commit* head_ptr = nullptr; - if (git_commit_lookup(&head_ptr, repository_.get(), &commit_id) < 0) { + if (git_commit_lookup(&head_ptr, repo.get(), &commit_id) < 0) { JAMI_ERR("Could not look up HEAD commit"); return {}; } @@ -1154,7 +1210,7 @@ ConversationRepository::Impl::commit(const std::string& msg) git_buf to_sign = {}; const git_commit* head_ref[1] = {head_commit.get()}; if (git_commit_create_buffer(&to_sign, - repository_.get(), + repo.get(), sig.get(), sig.get(), nullptr, @@ -1172,7 +1228,7 @@ ConversationRepository::Impl::commit(const std::string& msg) auto signed_buf = account->identity().first->sign(to_sign_vec); std::string signed_str = base64::encode(signed_buf); if (git_commit_create_with_signature(&commit_id, - repository_.get(), + repo.get(), to_sign.ptr, signed_str.c_str(), "signature") @@ -1183,7 +1239,7 @@ ConversationRepository::Impl::commit(const std::string& msg) // Move commit to main branch git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, repository_.get(), "refs/heads/main", &commit_id, true, nullptr) + if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) { JAMI_WARN("Could not move commit to main"); } @@ -1268,15 +1324,18 @@ ConversationRepository::Impl::mode() const std::string ConversationRepository::Impl::diffStats(const std::string& newId, const std::string& oldId) const { - if (auto d = diff(newId, oldId)) + auto repo = repository(); + if (auto d = diff(repo.get(), newId, oldId)) return diffStats(d); return {}; } GitDiff -ConversationRepository::Impl::diff(const std::string& idNew, const std::string& idOld) const +ConversationRepository::Impl::diff(git_repository* repo, + const std::string& idNew, + const std::string& idOld) const { - if (!repository_) { + if (!repo) { JAMI_ERR("Cannot get reference for HEAD"); return {nullptr, git_diff_free}; } @@ -1285,18 +1344,18 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& git_oid oid; git_commit* commitNew = nullptr; if (idNew == "HEAD") { - if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) { + if (git_reference_name_to_id(&oid, repo, "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); return {nullptr, git_diff_free}; } - if (git_commit_lookup(&commitNew, repository_.get(), &oid) < 0) { + if (git_commit_lookup(&commitNew, repo, &oid) < 0) { JAMI_ERR("Could not look up HEAD commit"); return {nullptr, git_diff_free}; } } else { if (git_oid_fromstr(&oid, idNew.c_str()) < 0 - || git_commit_lookup(&commitNew, repository_.get(), &oid) < 0) { + || git_commit_lookup(&commitNew, repo, &oid) < 0) { JAMI_WARN("Failed to look up commit %s", idNew.c_str()); return {nullptr, git_diff_free}; } @@ -1312,7 +1371,7 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& git_diff* diff_ptr = nullptr; if (idOld.empty()) { - if (git_diff_tree_to_tree(&diff_ptr, repository_.get(), nullptr, treeNew.get(), {}) < 0) { + if (git_diff_tree_to_tree(&diff_ptr, repo, nullptr, treeNew.get(), {}) < 0) { JAMI_ERR("Could not get diff to empty repository"); return {nullptr, git_diff_free}; } @@ -1321,8 +1380,7 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& // Retrieve tree for commit old git_commit* commitOld = nullptr; - if (git_oid_fromstr(&oid, idOld.c_str()) < 0 - || git_commit_lookup(&commitOld, repository_.get(), &oid) < 0) { + if (git_oid_fromstr(&oid, idOld.c_str()) < 0 || git_commit_lookup(&commitOld, repo, &oid) < 0) { JAMI_WARN("Failed to look up commit %s", idOld.c_str()); return {nullptr, git_diff_free}; } @@ -1336,7 +1394,7 @@ ConversationRepository::Impl::diff(const std::string& idNew, const std::string& GitTree treeOld = {tOld, git_tree_free}; // Calc diff - if (git_diff_tree_to_tree(&diff_ptr, repository_.get(), treeOld.get(), treeNew.get(), {}) < 0) { + if (git_diff_tree_to_tree(&diff_ptr, repo, treeOld.get(), treeNew.get(), {}) < 0) { JAMI_ERR("Could not get diff between %s and %s", idOld.c_str(), idNew.c_str()); return {nullptr, git_diff_free}; } @@ -1348,7 +1406,7 @@ ConversationRepository::Impl::behind(const std::string& from) const { git_oid oid_local, oid_remote; auto repo = repository(); - if (git_reference_name_to_id(&oid_local, repository_.get(), "HEAD") < 0) { + if (git_reference_name_to_id(&oid_local, repo.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); return {}; } @@ -1357,7 +1415,7 @@ ConversationRepository::Impl::behind(const std::string& from) const return {}; } size_t ahead = 0, behind = 0; - if (git_graph_ahead_behind(&ahead, &behind, repository_.get(), &oid_local, &oid_remote) != 0) { + if (git_graph_ahead_behind(&ahead, &behind, repo.get(), &oid_local, &oid_remote) != 0) { JAMI_ERR("Cannot get commits ahead for commit %s", from.c_str()); return {}; } @@ -1377,7 +1435,7 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to git_oid oid; auto repo = repository(); if (from.empty()) { - if (git_reference_name_to_id(&oid, repository_.get(), "HEAD") < 0) { + if (git_reference_name_to_id(&oid, repo.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); return commits; } @@ -1389,8 +1447,9 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to } git_revwalk* walker_ptr = nullptr; - if (git_revwalk_new(&walker_ptr, repository_.get()) < 0 - || git_revwalk_push(walker_ptr, &oid) < 0) { + if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) { + if (walker_ptr) + git_revwalk_free(walker_ptr); JAMI_DBG("Couldn't init revwalker for conversation %s", id_.c_str()); return commits; } @@ -1403,7 +1462,7 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to } git_commit* commit_ptr = nullptr; std::string id = git_oid_tostr_s(&oid); - if (git_commit_lookup(&commit_ptr, repository_.get(), &oid) < 0) { + if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) { JAMI_WARN("Failed to look up commit %s", id.c_str()); break; } @@ -1433,11 +1492,7 @@ ConversationRepository::Impl::log(const std::string& from, const std::string& to cc->author = std::move(author); cc->parents = std::move(parents); git_buf signature = {}, signed_data = {}; - if (git_commit_extract_signature(&signature, - &signed_data, - repository_.get(), - &oid, - "signature") + if (git_commit_extract_signature(&signature, &signed_data, repo.get(), &oid, "signature") < 0) { JAMI_WARN("Could not extract signature for commit %s", id.c_str()); } else { @@ -1467,13 +1522,11 @@ ConversationRepository::Impl::fileAtTree(const std::string& path, const GitTree& } GitTree -ConversationRepository::Impl::treeAtCommit(const std::string& commitId) const +ConversationRepository::Impl::treeAtCommit(git_repository* repo, const std::string& commitId) const { git_oid oid; git_commit* commit = nullptr; - auto repo = repository(); - if (git_oid_fromstr(&oid, commitId.c_str()) < 0 - || git_commit_lookup(&commit, repository_.get(), &oid) < 0) { + if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit, repo, &oid) < 0) { JAMI_WARN("Failed to look up commit %s", commitId.c_str()); return GitTree {nullptr, git_tree_free}; } @@ -1533,16 +1586,85 @@ ConversationRepository::Impl::getInitialMembers() const return {authorId}; } +bool +ConversationRepository::Impl::resolveConflicts(git_index* index, const std::string& other_id) +{ + git_index_conflict_iterator* conflict_iterator = nullptr; + const git_index_entry* ancestor_out = nullptr; + const git_index_entry* our_out = nullptr; + const git_index_entry* their_out = nullptr; + + git_index_conflict_iterator_new(&conflict_iterator, index); + + git_oid head_commit_id; + auto repo = repository(); + if (git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return false; + } + auto commit_str = git_oid_tostr_s(&head_commit_id); + if (!commit_str) + return false; + auto useRemote = (other_id > commit_str); // Choose by commit version + + // NOTE: for now, only authorize conflicts on "profile.vcf" + std::vector<git_index_entry> new_entries; + while (git_index_conflict_next(&ancestor_out, &our_out, &their_out, conflict_iterator) + != GIT_ITEROVER) { + if (ancestor_out && ancestor_out->path && our_out && our_out->path && their_out + && their_out->path) { + if (std::string(ancestor_out->path) == "profile.vcf") { + // Checkout wanted version. copy the index_entry + git_index_entry resolution = useRemote ? *their_out : *our_out; + resolution.flags &= GIT_INDEX_STAGE_NORMAL; + if (!(resolution.flags & GIT_IDXENTRY_VALID)) + resolution.flags |= GIT_IDXENTRY_VALID; + // NOTE: do no git_index_add yet, wait for after full conflict checks + new_entries.push_back(resolution); + continue; + } + JAMI_ERR("Conflict detected on a file that is not authorized: %s", ancestor_out->path); + return false; + } + return false; + } + + for (auto& entry : new_entries) + git_index_add(index, &entry); + git_index_conflict_cleanup(index); + git_index_conflict_iterator_free(conflict_iterator); + + // Checkout and cleanup + git_checkout_options opt; + git_checkout_options_init(&opt, GIT_CHECKOUT_OPTIONS_VERSION); + opt.checkout_strategy |= GIT_CHECKOUT_FORCE; + opt.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS; + if (other_id > commit_str) + opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS; + else + opt.checkout_strategy |= GIT_CHECKOUT_USE_OURS; + + if (git_checkout_index(repo.get(), index, &opt) < 0) { + const git_error* err = giterr_last(); + if (err) + JAMI_ERR("Cannot checkout index: %s", err->message); + return false; + } + + return true; +} + void ConversationRepository::Impl::initMembers() { - if (!repository_) + auto repo = repository(); + if (!repo) return; std::vector<std::string> uris; std::lock_guard<std::mutex> lk(membersMtx_); members_.clear(); - std::string repoPath = git_repository_workdir(repository_.get()); + std::string repoPath = git_repository_workdir(repo.get()); std::vector<std::string> paths = {repoPath + DIR_SEPARATOR_STR + "invited", repoPath + DIR_SEPARATOR_STR + "admins", repoPath + DIR_SEPARATOR_STR + "members", @@ -1687,7 +1809,7 @@ ConversationRepository::cloneConversation(const std::weak_ptr<JamiAccount>& acco 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", err->message); + JAMI_ERR("Error when retrieving remote conversation: %s %s", err->message, path.c_str()); return nullptr; } git_repository_free(rep); @@ -1827,6 +1949,21 @@ ConversationRepository::Impl::validCommits( } return false; } + } else if (type == "application/update-profile") { + if (!checkValidProfileUpdate(userDevice, commit.id, commit.parents[0])) { + JAMI_WARN("Malformed profile updates commit %s. Please check you use the " + "latest version " + "of Jami, or that your contact is not doing unwanted stuff.", + commit.id.c_str()); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), + id_, + EVALIDFETCH, + "Malformed profile updates commit"); + } + return false; + } } else { // Note: accept all mimetype here, as we can have new mimetypes // Just avoid to add weird files @@ -1896,7 +2033,8 @@ ConversationRepository::addMember(const std::string& uri) name = deviceId; // First, we need to add the member file to the repository if not present - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); std::string invitedPath = repoPath + "invited"; if (!fileutils::recursive_mkdir(invitedPath, 0700)) { @@ -1955,8 +2093,9 @@ ConversationRepository::amend(const std::string& id, const std::string& msg) GitSignature sig {sig_ptr, git_signature_free}; git_commit* commit_ptr = nullptr; + auto repo = pimpl_->repository(); if (git_oid_fromstr(&tree_id, id.c_str()) < 0 - || git_commit_lookup(&commit_ptr, pimpl_->repository_.get(), &tree_id) < 0) { + || git_commit_lookup(&commit_ptr, repo.get(), &tree_id) < 0) { JAMI_WARN("Failed to look up commit %s", id.c_str()); return {}; } @@ -1971,12 +2110,7 @@ ConversationRepository::amend(const std::string& id, const std::string& msg) // Move commit to main branch git_reference* ref_ptr = nullptr; - if (git_reference_create(&ref_ptr, - pimpl_->repository_.get(), - "refs/heads/main", - &commit_id, - true, - nullptr) + if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) { JAMI_WARN("Could not move commit to main"); } @@ -2005,17 +2139,15 @@ ConversationRepository::fetch(const std::string& remoteDeviceId) auto lastCommit = lastMsg[0].id; // Assert that repository exists + auto repo = pimpl_->repository(); std::string channelName = "git://" + remoteDeviceId + '/' + pimpl_->id_; - auto res = git_remote_lookup(&remote_ptr, pimpl_->repository_.get(), remoteDeviceId.c_str()); + auto res = git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str()); if (res != 0) { if (res != GIT_ENOTFOUND) { JAMI_ERR("Couldn't lookup for remote %s", remoteDeviceId.c_str()); return false; } - if (git_remote_create(&remote_ptr, - pimpl_->repository_.get(), - remoteDeviceId.c_str(), - channelName.c_str()) + if (git_remote_create(&remote_ptr, repo.get(), remoteDeviceId.c_str(), channelName.c_str()) < 0) { JAMI_ERR("Could not create remote for repository for conversation %s", pimpl_->id_.c_str()); @@ -2060,7 +2192,8 @@ ConversationRepository::remoteHead(const std::string& remoteDeviceId, const std::string& branch) const { git_remote* remote_ptr = nullptr; - if (git_remote_lookup(&remote_ptr, pimpl_->repository_.get(), remoteDeviceId.c_str()) < 0) { + auto repo = pimpl_->repository(); + if (git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str()) < 0) { JAMI_WARN("No remote found with id: %s", remoteDeviceId.c_str()); return {}; } @@ -2069,7 +2202,7 @@ ConversationRepository::remoteHead(const std::string& remoteDeviceId, git_reference* head_ref_ptr = nullptr; std::string remoteHead = "refs/remotes/" + remoteDeviceId + "/" + branch; git_oid commit_id; - if (git_reference_name_to_id(&commit_id, pimpl_->repository_.get(), remoteHead.c_str()) < 0) { + if (git_reference_name_to_id(&commit_id, repo.get(), remoteHead.c_str()) < 0) { const git_error* err = giterr_last(); if (err) JAMI_ERR("failed to lookup %s ref: %s", remoteHead.c_str(), err->message); @@ -2092,7 +2225,8 @@ ConversationRepository::commitMessage(const std::string& msg) auto deviceId = std::string(account->currentDeviceId()); // First, we need to add device file to the repository if not present - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); std::string path = std::string("devices") + DIR_SEPARATOR_STR + deviceId + ".crt"; std::string devicePath = repoPath + path; if (!fileutils::isFile(devicePath)) { @@ -2138,13 +2272,14 @@ bool ConversationRepository::merge(const std::string& merge_id) { // First, the repository must be in a clean state - int state = git_repository_state(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + int state = git_repository_state(repo.get()); if (state != GIT_REPOSITORY_STATE_NONE) { JAMI_ERR("Merge operation aborted: repository is in unexpected state %d", state); return false; } // Checkout main (to do a `git_merge branch`) - if (git_repository_set_head(pimpl_->repository_.get(), "refs/heads/main") < 0) { + if (git_repository_set_head(repo.get(), "refs/heads/main") < 0) { JAMI_ERR("Merge operation aborted: couldn't checkout main branch"); return false; } @@ -2156,7 +2291,7 @@ ConversationRepository::merge(const std::string& merge_id) return false; } git_annotated_commit* annotated_ptr = nullptr; - if (git_annotated_commit_lookup(&annotated_ptr, pimpl_->repository_.get(), &commit_id) < 0) { + if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) { JAMI_ERR("Merge operation aborted: couldn't lookup commit %s", merge_id.c_str()); return false; } @@ -2166,12 +2301,12 @@ ConversationRepository::merge(const std::string& merge_id) git_merge_analysis_t analysis; git_merge_preference_t preference; const git_annotated_commit* const_annotated = annotated.get(); - if (git_merge_analysis(&analysis, &preference, pimpl_->repository_.get(), &const_annotated, 1) - < 0) { + if (git_merge_analysis(&analysis, &preference, repo.get(), &const_annotated, 1) < 0) { JAMI_ERR("Merge operation aborted: repository analysis failed"); return false; } + // Handle easy merges if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) { JAMI_INFO("Already up-to-date"); return true; @@ -2191,40 +2326,51 @@ ConversationRepository::merge(const std::string& merge_id) return false; } return true; - } else if (analysis & GIT_MERGE_ANALYSIS_NORMAL) { - git_merge_options merge_opts; - git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION); - merge_opts.file_flags = GIT_MERGE_FILE_STYLE_DIFF3; - git_checkout_options checkout_opts; - git_checkout_options_init(&checkout_opts, GIT_CHECKOUT_OPTIONS_VERSION); - checkout_opts.checkout_strategy = GIT_CHECKOUT_FORCE | GIT_CHECKOUT_ALLOW_CONFLICTS; - - if (preference & GIT_MERGE_PREFERENCE_FASTFORWARD_ONLY) { - JAMI_ERR("Fast-forward is preferred, but only a merge is possible"); - return false; - } + } - if (git_merge(pimpl_->repository_.get(), &const_annotated, 1, &merge_opts, &checkout_opts) - < 0) { - const git_error* err = giterr_last(); - if (err) - JAMI_ERR("Git merge failed: %s", err->message); - return false; - } + // Else we want to check for conflicts + git_oid head_commit_id; + if (git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) { + JAMI_ERR("Cannot get reference for HEAD"); + return false; + } + + git_commit* head_ptr = nullptr; + if (git_commit_lookup(&head_ptr, repo.get(), &head_commit_id) < 0) { + JAMI_ERR("Could not look up HEAD commit"); + return false; } + GitCommit head_commit {head_ptr, git_commit_free}; + git_commit* other__ptr = nullptr; + if (git_commit_lookup(&other__ptr, repo.get(), &commit_id) < 0) { + JAMI_ERR("Could not look up HEAD commit"); + return false; + } + GitCommit other_commit {other__ptr, git_commit_free}; + + git_merge_options merge_opts; + git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION); git_index* index_ptr = nullptr; - if (git_repository_index(&index_ptr, pimpl_->repository_.get()) < 0) { - JAMI_ERR("Merge operation aborted: could not open repository index"); + if (git_merge_commits(&index_ptr, repo.get(), head_commit.get(), other_commit.get(), &merge_opts) + < 0) { + const git_error* err = giterr_last(); + if (err) + JAMI_ERR("Git merge failed: %s", err->message); return false; } GitIndex index {index_ptr, git_index_free}; if (git_index_has_conflicts(index.get())) { - JAMI_WARN("Merge operation aborted: the merge operation resulted in some conflicts"); - return false; + JAMI_INFO("Some conflicts were detected during the merge operations. Resolution phase."); + if (!pimpl_->resolveConflicts(index.get(), merge_id) or !git_add_all(repo.get())) { + JAMI_ERR("Merge operation aborted; Can't automatically resolve conflicts"); + return false; + } } + auto result = pimpl_->createMergeCommit(index.get(), merge_id); JAMI_INFO("Merge done between %s and main", merge_id.c_str()); + return result; } @@ -2253,7 +2399,8 @@ std::string ConversationRepository::join() { // Check that not already member - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); auto account = pimpl_->account_.lock(); if (!account) return {}; @@ -2287,7 +2434,7 @@ ConversationRepository::join() file << parentCert->toString(true); file.close(); // git add -A - if (!git_add_all(pimpl_->repository_.get())) { + if (!git_add_all(repo.get())) { return {}; } Json::Value json; @@ -2334,7 +2481,8 @@ ConversationRepository::leave() name = deviceId; // Remove related files - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); std::string adminFile = repoPath + "admins" + DIR_SEPARATOR_STR + uri + ".crt"; std::string memberFile = repoPath + "members" + DIR_SEPARATOR_STR + uri + ".crt"; @@ -2373,7 +2521,7 @@ ConversationRepository::leave() } } - if (!git_add_all(pimpl_->repository_.get())) { + if (!git_add_all(repo.get())) { return {}; } @@ -2399,7 +2547,8 @@ void ConversationRepository::erase() { // First, we need to add the member file to the repository if not present - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); JAMI_DBG() << "Erasing " << repoPath; fileutils::removeAll(repoPath, true); @@ -2416,7 +2565,8 @@ ConversationRepository::voteKick(const std::string& uri, bool isDevice) { // Add vote + commit // TODO simplify - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); auto account = pimpl_->account_.lock(); if (!account) return {}; @@ -2462,7 +2612,8 @@ ConversationRepository::resolveVote(const std::string& uri, bool isDevice) // Count ratio admin/votes auto nbAdmins = 0, nbVotes = 0; // For each admin, check if voted - std::string repoPath = git_repository_workdir(pimpl_->repository_.get()); + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); std::string adminsPath = repoPath + "admins"; std::string membersPath = repoPath + "members"; std::string devicesPath = repoPath + "devices"; @@ -2531,7 +2682,7 @@ ConversationRepository::resolveVote(const std::string& uri, bool isDevice) } // Commit - if (!git_add_all(pimpl_->repository_.get())) + if (!git_add_all(repo.get())) return {}; Json::Value json; @@ -2621,4 +2772,121 @@ ConversationRepository::pinCertificates() } } +std::string +ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile) +{ + auto account = pimpl_->account_.lock(); + if (!account) + return {}; + auto uri = std::string(account->getUsername()); + auto valid = false; + { + std::lock_guard<std::mutex> lk(pimpl_->membersMtx_); + for (const auto& member : pimpl_->members_) { + if (member.uri == uri) { + valid = member.role <= pimpl_->updateProfilePermLvl_; + break; + } + } + } + if (!valid) { + JAMI_ERR("Not enough authorization for updating infos"); + emitSignal<DRing::ConversationSignal::OnConversationError>( + account->getAccountID(), + pimpl_->id_, + EUNAUTHORIZED, + "Not enough authorization for updating infos"); + return {}; + } + + auto infosMap = infos(); + for (const auto& [k, v] : profile) { + infosMap[k] = v; + } + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); + auto profilePath = repoPath + "profile.vcf"; + auto file = fileutils::ofstream(profilePath, std::ios::trunc | std::ios::binary); + if (!file.is_open()) { + JAMI_ERR("Could not write data to %s", profilePath.c_str()); + return {}; + } + file << vCard::Delimiter::BEGIN_TOKEN; + file << vCard::Delimiter::END_LINE_TOKEN; + file << vCard::Property::VCARD_VERSION; + file << ":2.1"; + file << vCard::Delimiter::END_LINE_TOKEN; + auto titleIt = infosMap.find("title"); + if (titleIt != infosMap.end()) { + file << vCard::Property::FORMATTED_NAME; + file << ":"; + file << titleIt->second; + file << vCard::Delimiter::END_LINE_TOKEN; + } + auto descriptionIt = infosMap.find("description"); + if (descriptionIt != infosMap.end()) { + file << vCard::Property::DESCRIPTION; + file << ":"; + file << descriptionIt->second; + file << vCard::Delimiter::END_LINE_TOKEN; + } + file << vCard::Property::PHOTO; + file << vCard::Delimiter::SEPARATOR_TOKEN; + file << vCard::Property::BASE64; + auto avatarIt = infosMap.find("avatar"); + if (avatarIt != infosMap.end()) { + // TODO type=png? store another way? + file << ":"; + file << avatarIt->second; + } + file << vCard::Delimiter::END_LINE_TOKEN; + file << vCard::Delimiter::END_TOKEN; + file.close(); + + if (!pimpl_->add("profile.vcf")) + return {}; + Json::Value json; + json["type"] = "application/update-profile"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + + return pimpl_->commit(Json::writeString(wbuilder, json)); +} + +std::map<std::string, std::string> +ConversationRepository::infos() const +{ + try { + auto repo = pimpl_->repository(); + std::string repoPath = git_repository_workdir(repo.get()); + auto profilePath = repoPath + "profile.vcf"; + std::map<std::string, std::string> result; + if (fileutils::isFile(profilePath)) { + auto content = fileutils::loadTextFile(profilePath); + result = ConversationRepository::infosFromVCard(vCard::utils::toMap(content)); + } + result["mode"] = std::to_string(static_cast<int>(mode())); + return result; + } catch (...) { + } + return {}; +} + +std::map<std::string, std::string> +ConversationRepository::infosFromVCard(const std::map<std::string, std::string>& details) +{ + std::map<std::string, std::string> result; + for (const auto& [k, v] : details) { + if (k == vCard::Property::FORMATTED_NAME) { + result["title"] = v; + } else if (k == vCard::Property::DESCRIPTION) { + result["description"] = v; + } else if (k.find(vCard::Property::PHOTO) == 0) { + result["avatar"] = v; + } + } + return result; +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index f6c2752c5a60d9f8f80e36d5ce308ab485e22f67..1e8449a2d7b46dea12d3817d82fa7e2d9c36efa8 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -48,6 +48,7 @@ namespace jami { constexpr auto EFETCH = 1; constexpr auto EINVALIDMODE = 2; constexpr auto EVALIDFETCH = 3; +constexpr auto EUNAUTHORIZED = 4; class JamiAccount; class ChannelSocket; @@ -266,6 +267,21 @@ public: */ void pinCertificates(); + /** + * Change repository's infos + * @param map New infos (supported keys: title, description, avatar) + * @return the commit id + */ + std::string updateInfos(const std::map<std::string, std::string>& map); + + /** + * Retrieve current infos (title, description, avatar, mode) + * @return infos + */ + std::map<std::string, std::string> infos() const; + static std::map<std::string, std::string> infosFromVCard( + const std::map<std::string, std::string>& details); + private: ConversationRepository() = delete; class Impl; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index abf36690e9349cc25764804c5019a3a582b2f378..5d5c850e28b6ac5886d8723964a54a67553ba4c1 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -80,6 +80,7 @@ #include "security/certstore.h" #include "libdevcrypto/Common.h" #include "base64.h" +#include "vcard.h" #include "im/instant_messaging.h" #include <opendht/thread_pool.h> @@ -1232,6 +1233,9 @@ JamiAccount::loadAccount(const std::string& archive_password, req.from = uri; req.conversationId = conversationId; req.received = std::time(nullptr); + auto details = vCard::utils::toMap( + std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size())); + req.metadatas = ConversationRepository::infosFromVCard(details); acc->conversationsRequests_[conversationId] = std::move(req); } emitSignal<DRing::ConfigurationSignal::IncomingTrustRequest>( @@ -2208,11 +2212,7 @@ JamiAccount::onTrackedBuddyOnline(const dht::InfoHash& contactId) // offline maybe) To avoid the contact to never receive the conv request, retry there std::lock_guard<std::mutex> lock(configurationMutex_); if (accountManager_) - accountManager_ - ->sendTrustRequest(id, - convId, - {}); /* TODO payload?, MessageEngine not generic and will be able - to move to conversation's requests */ + accountManager_->sendTrustRequest(id, convId, conversationVCard(convId)); } } @@ -3602,7 +3602,7 @@ JamiAccount::sendTextMessage(const std::string& to, JAMI_DBG() << "[Account " << getAccountID() << "] [message " << token << "] Put encrypted " << (ok ? "ok" : "failed"); - if (not ok) { + if (not ok && dhtPeerConnector_ /* Check if not joining */) { std::unique_lock<std::mutex> l(confirm->lock); auto lt = confirm->listenTokens.find(h); if (lt != confirm->listenTokens.end()) { @@ -4121,6 +4121,73 @@ JamiAccount::getConversationRequests() return requests; } +void +JamiAccount::updateConversationInfos(const std::string& conversationId, + const std::map<std::string, std::string>& infos, + bool sync) +{ + std::lock_guard<std::mutex> lk(conversationsMtx_); + // Add a new member in the conversation + auto it = conversations_.find(conversationId); + if (it == conversations_.end()) { + JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); + return; + } + + auto commitId = it->second->updateInfos(infos); + if (commitId.empty()) { + JAMI_WARN("Couldn't update infos on %s", conversationId.c_str()); + return; + } + if (!sync) + return; + // Announce new message + it->second->loadMessages( + [w = weak(), conversationId, commitId](auto&& messages) { + auto shared = w.lock(); + if (not shared or messages.empty()) + return; // should not happen + std::lock_guard<std::mutex> lk(shared->conversationsMtx_); + auto it = shared->conversations_.find(conversationId); + if (it == shared->conversations_.end()) + return; + emitSignal<DRing::ConversationSignal::MessageReceived>(shared->getAccountID(), + conversationId, + messages.front()); + shared->sendMessageNotification(*it->second, commitId, true); + }, + commitId, + 1); +} + +std::map<std::string, std::string> +JamiAccount::conversationInfos(const std::string& conversationId) const +{ + std::lock_guard<std::mutex> lk(conversationsMtx_); + // Add a new member in the conversation + auto it = conversations_.find(conversationId); + if (it == conversations_.end()) { + JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); + return {}; + } + + return it->second->infos(); +} + +std::vector<uint8_t> +JamiAccount::conversationVCard(const std::string& conversationId) const +{ + std::lock_guard<std::mutex> lk(conversationsMtx_); + // Add a new member in the conversation + auto it = conversations_.find(conversationId); + if (it == conversations_.end()) { + JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); + return {}; + } + + return it->second->vCard(); +} + // Member management bool JamiAccount::addConversationMember(const std::string& conversationId, diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 7d425839b5111eef8fca90c3329900cbcf70e765..6a801e8d2c3b730b4eeb902c4d524202cd047a23 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -504,13 +504,21 @@ public: std::string_view currentDeviceId() const; // Conversation management - std::string startConversation(ConversationMode mode = ConversationMode::INVITES_ONLY, const std::string& otherMember = ""); + std::string startConversation(ConversationMode mode = ConversationMode::INVITES_ONLY, + const std::string& otherMember = ""); void acceptConversationRequest(const std::string& conversationId); void declineConversationRequest(const std::string& conversationId); std::vector<std::string> getConversations(); bool removeConversation(const std::string& conversationId); std::vector<std::map<std::string, std::string>> getConversationRequests(); + // Conversation's infos management + void updateConversationInfos(const std::string& conversationId, + const std::map<std::string, std::string>& infos, + bool sync = true); + std::map<std::string, std::string> conversationInfos(const std::string& conversationId) const; + std::vector<uint8_t> conversationVCard(const std::string& conversationId) const; + // Member management bool addConversationMember(const std::string& conversationId, const std::string& contactUri, diff --git a/src/vcard.h b/src/vcard.h new file mode 100644 index 0000000000000000000000000000000000000000..9e9f1d33e4c8d34959afbd4e89a2d9fe43ac86ec --- /dev/null +++ b/src/vcard.h @@ -0,0 +1,103 @@ +/**************************************************************************** + * Copyright (C) 2017-2020 Savoir-faire Linux Inc. * + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> * + * Author : Alexandre Lision <alexandre.lision@savoirfairelinux.com> * + * * + * This library is free software; you can redistribute it and/or * + * modify it under the terms of the GNU Lesser General Public * + * License as published by the Free Software Foundation; either * + * version 2.1 of the License, or (at your option) any later version. * + * * + * This library 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 * + * Lesser 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 <http://www.gnu.org/licenses/>. * + ***************************************************************************/ +#pragma once + +#include <string> +#include <string_view> +#include <map> + +#include "string_utils.h" + +namespace vCard { + +constexpr static const char* PROFILE_VCF = "x-jami/jami.profile.vcard"; + +struct Delimiter +{ + constexpr static const char* SEPARATOR_TOKEN = ";"; + constexpr static const char* END_LINE_TOKEN = "\n"; + constexpr static const char* BEGIN_TOKEN = "BEGIN:VCARD"; + constexpr static const char* END_TOKEN = "END:VCARD"; +}; + +struct Property +{ + constexpr static const char* UID = "UID"; + constexpr static const char* VCARD_VERSION = "VERSION"; + constexpr static const char* ADDRESS = "ADR"; + constexpr static const char* AGENT = "AGENT"; + constexpr static const char* BIRTHDAY = "BDAY"; + constexpr static const char* CATEGORIES = "CATEGORIES"; + constexpr static const char* CLASS = "CLASS"; + constexpr static const char* DELIVERY_LABEL = "LABEL"; + constexpr static const char* EMAIL = "EMAIL"; + constexpr static const char* FORMATTED_NAME = "FN"; + constexpr static const char* GEOGRAPHIC_POSITION = "GEO"; + constexpr static const char* KEY = "KEY"; + constexpr static const char* LOGO = "LOGO"; + constexpr static const char* MAILER = "MAILER"; + constexpr static const char* NAME = "N"; + constexpr static const char* NICKNAME = "NICKNAME"; + constexpr static const char* DESCRIPTION = "DESCRIPTION"; + constexpr static const char* NOTE = "NOTE"; + constexpr static const char* ORGANIZATION = "ORG"; + constexpr static const char* PHOTO = "PHOTO"; + constexpr static const char* PRODUCT_IDENTIFIER = "PRODID"; + constexpr static const char* REVISION = "REV"; + constexpr static const char* ROLE = "ROLE"; + constexpr static const char* SORT_STRING = "SORT-STRING"; + constexpr static const char* SOUND = "SOUND"; + constexpr static const char* TELEPHONE = "TEL"; + constexpr static const char* TIME_ZONE = "TZ"; + constexpr static const char* TITLE = "TITLE"; + constexpr static const char* URL = "URL"; + constexpr static const char* BASE64 = "ENCODING=BASE64"; + constexpr static const char* TYPE_PNG = "TYPE=PNG"; + constexpr static const char* TYPE_JPEG = "TYPE=JPEG"; + constexpr static const char* PHOTO_PNG = "PHOTO;ENCODING=BASE64;TYPE=PNG"; + constexpr static const char* PHOTO_JPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG"; + + constexpr static const char* X_RINGACCOUNT = "X-RINGACCOUNTID"; +}; + +namespace utils { +/** + * Payload to vCard + * @param content payload + * @return the vCard representation + */ +static std::map<std::string, std::string> +toMap(std::string_view content) +{ + std::map<std::string, std::string> vCard; + + std::string_view line; + while (jami::getline(content, line)) { + if (line.size()) { + const auto dblptPos = line.find(':'); + if (dblptPos == std::string::npos) + continue; + vCard.emplace(line.substr(0, dblptPos), line.substr(dblptPos + 1)); + } + } + return vCard; +} +} // namespace utils + +} // namespace vCard diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 36d7b77f9e0afb7ee6f3d8fa05200d727d533391..ddc452576a6cff20493eb777754531833328a150 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -143,6 +143,12 @@ private: void testAddOfflineContactThenConnect(); void testDeclineTrustRequestDoNotGenerateAnother(); void testConversationMemberEvent(); + void testUpdateProfile(); + void testCheckProfileInConversationRequest(); + void testCheckProfileInTrustRequest(); + void testMemberCannotUpdateProfile(); + void testUpdateProfileWithBadFile(); + void testFetchProfileUnauthorized(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); @@ -189,6 +195,12 @@ private: CPPUNIT_TEST(testAddOfflineContactThenConnect); CPPUNIT_TEST(testDeclineTrustRequestDoNotGenerateAnother); CPPUNIT_TEST(testConversationMemberEvent); + CPPUNIT_TEST(testUpdateProfile); + CPPUNIT_TEST(testCheckProfileInConversationRequest); + CPPUNIT_TEST(testCheckProfileInTrustRequest); + CPPUNIT_TEST(testMemberCannotUpdateProfile); + CPPUNIT_TEST(testUpdateProfileWithBadFile); + CPPUNIT_TEST(testFetchProfileUnauthorized); CPPUNIT_TEST_SUITE_END(); }; @@ -3686,6 +3698,409 @@ ConversationTest::testDeclineTrustRequestDoNotGenerateAnother() CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); } +void +ConversationTest::testUpdateProfile() +{ + 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; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + + messageAliceReceived = 0; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && messageAliceReceived == 1; + })); + + messageBobReceived = 0; + aliceAccount->updateConversationInfos(convId, {{"title", "My awesome swarm"}}); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return messageBobReceived == 1; })); + + auto infos = bobAccount->conversationInfos(convId); + CPPUNIT_ASSERT(infos["title"] == "My awesome swarm"); + CPPUNIT_ASSERT(infos["description"].empty()); + + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testCheckProfileInConversationRequest() +{ + 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; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + + auto convId = aliceAccount->startConversation(); + aliceAccount->updateConversationInfos(convId, {{"title", "My awesome swarm"}}); + + CPPUNIT_ASSERT(aliceAccount->addConversationMember(convId, bobUri)); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return requestReceived; })); + auto requests = bobAccount->getConversationRequests(); + CPPUNIT_ASSERT(requests.size() == 1); + CPPUNIT_ASSERT(requests.front()["title"] == "My awesome swarm"); + + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testCheckProfileInTrustRequest() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<DRing::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, requestReceived = false, memberMessageGenerated = false; + std::string convId = ""; + std::string vcard = "BEGIN:VCARD\n\ +VERSION:2.1\n\ +FN:TITLE\n\ +DESCRIPTION:DESC\n\ +END:VCARD"; + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>( + [&](const std::string& account_id, + const std::string& /*from*/, + const std::vector<uint8_t>& payload, + time_t /*received*/) { + if (account_id == bobId + && std::string(payload.data(), payload.data() + payload.size()) == vcard) + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == aliceId) { + convId = conversationId; + } else if (accountId == bobId) { + conversationReady = true; + } + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + aliceAccount->addContact(bobUri); + std::vector<uint8_t> payload(vcard.begin(), vcard.end()); + aliceAccount->sendTrustRequest(bobUri, payload); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { + return !convId.empty() && requestReceived; + })); +} + +void +ConversationTest::testMemberCannotUpdateProfile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + 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; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool errorDetected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 4) + errorDetected = 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; })); + + messageAliceReceived = 0; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && messageAliceReceived == 1; + })); + + messageBobReceived = 0; + bobAccount->updateConversationInfos(convId, {{"title", "My awesome swarm"}}); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(5), [&]() { return errorDetected; })); + + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testUpdateProfileWithBadFile() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + 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; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool errorDetected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 3) + errorDetected = 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; })); + + messageAliceReceived = 0; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && messageAliceReceived == 1; + })); + + // Update profile but with bad file + addFile(aliceAccount, convId, "BADFILE"); + std::string vcard = "BEGIN:VCARD\n\ +VERSION:2.1\n\ +FN:TITLE\n\ +DESCRIPTION:DESC\n\ +END:VCARD"; + addFile(aliceAccount, convId, "profile.vcf", vcard); + Json::Value root; + root["type"] = "application/update-profile"; + commit(aliceAccount, convId, root); + errorDetected = false; + aliceAccount->sendMessage(convId, "hi"s); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + + DRing::unregisterSignalHandlers(); +} + +void +ConversationTest::testFetchProfileUnauthorized() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + 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; + auto messageBobReceived = 0, messageAliceReceived = 0; + bool requestReceived = false; + bool conversationReady = false; + bool errorDetected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*message*/) { + if (accountId == bobId) { + messageBobReceived += 1; + } else { + messageAliceReceived += 1; + } + cv.notify_one(); + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == aliceId && conversationId == convId && code == 3) + errorDetected = 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; })); + + messageAliceReceived = 0; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return conversationReady && messageAliceReceived == 1; + })); + + // Fake realist profile update + std::string vcard = "BEGIN:VCARD\n\ +VERSION:2.1\n\ +FN:TITLE\n\ +DESCRIPTION:DESC\n\ +END:VCARD"; + addFile(bobAccount, convId, "profile.vcf", vcard); + Json::Value root; + root["type"] = "application/update-profile"; + commit(bobAccount, convId, root); + errorDetected = false; + bobAccount->sendMessage(convId, "hi"s); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { return errorDetected; })); + + DRing::unregisterSignalHandlers(); +} + } // namespace test } // namespace jami diff --git a/test/unitTest/conversationRepository/conversationRepository.cpp b/test/unitTest/conversationRepository/conversationRepository.cpp index f10834d0823271f7bbaa01fbe4ed2503c21c0170..86865e93e75cb4f2f683bfe3b458d6a0c2c0fc08 100644 --- a/test/unitTest/conversationRepository/conversationRepository.cpp +++ b/test/unitTest/conversationRepository/conversationRepository.cpp @@ -37,6 +37,7 @@ #include "account_const.h" #include <git2.h> +#include <filesystem> using namespace std::string_literals; using namespace DRing::Account; @@ -71,14 +72,20 @@ private: 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(); std::string addCommit(git_repository* repo, const std::shared_ptr<JamiAccount> account, const std::string& branch, const std::string& commit_msg); + void addAll(git_repository* repo); bool merge_in_main(const std::shared_ptr<JamiAccount> account, - git_repository* repo, - const std::string& commit_ref); + git_repository* repo, + const std::string& commit_ref); CPPUNIT_TEST_SUITE(ConversationRepositoryTest); CPPUNIT_TEST(testCreateRepository); @@ -89,6 +96,9 @@ private: CPPUNIT_TEST(testMerge); CPPUNIT_TEST(testFFMerge); CPPUNIT_TEST(testDiff); + CPPUNIT_TEST(testMergeProfileWithConflict); + // CPPUNIT_TEST(testCloneHugeRepo); + CPPUNIT_TEST_SUITE_END(); }; @@ -257,7 +267,8 @@ ConversationRepositoryTest::testCloneViaChannelSocket() aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { + [&](std::shared_ptr<ChannelSocket> socket, + const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -419,12 +430,11 @@ ConversationRepositoryTest::testFetch() std::shared_ptr<ChannelSocket> channelSocket = nullptr; std::shared_ptr<ChannelSocket> sendSocket = nullptr; - bobAccount->connectionManager().onChannelRequest( - [&](const DeviceId&, const std::string& name) { - successfullyReceive = name == "git://*"; - ccv.notify_one(); - return true; - }); + bobAccount->connectionManager().onChannelRequest([&](const DeviceId&, const std::string& name) { + successfullyReceive = name == "git://*"; + ccv.notify_one(); + return true; + }); aliceAccount->connectionManager().onChannelRequest( [&](const DeviceId&, const std::string&) { return true; }); @@ -438,7 +448,8 @@ ConversationRepositoryTest::testFetch() aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { + [&](std::shared_ptr<ChannelSocket> socket, + const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -471,7 +482,8 @@ ConversationRepositoryTest::testFetch() // Open a new channel to simulate the fact that we are later aliceAccount->connectionManager().connectDevice(DeviceId(bobDeviceId), "git://*", - [&](std::shared_ptr<ChannelSocket> socket, const DeviceId&) { + [&](std::shared_ptr<ChannelSocket> socket, + const DeviceId&) { if (socket) { successfullyConnected = true; sendSocket = socket; @@ -553,12 +565,26 @@ ConversationRepositoryTest::addCommit(git_repository* repo, } GitCommit head_commit {head_ptr, git_commit_free}; + // Retrieve current index + git_index* index_ptr = nullptr; + if (git_repository_index(&index_ptr, repo) < 0) { + JAMI_ERR("Could not open repository index"); + return {}; + } + GitIndex index {index_ptr, git_index_free}; + + git_oid tree_id; + if (git_index_write_tree(&tree_id, index.get()) < 0) { + JAMI_ERR("Unable to write initial tree from index"); + return {}; + } + git_tree* tree_ptr = nullptr; - if (git_commit_tree(&tree_ptr, head_commit.get()) < 0) { + if (git_tree_lookup(&tree_ptr, repo, &tree_id) < 0) { JAMI_ERR("Could not look up initial tree"); return {}; } - GitTree tree {tree_ptr, git_tree_free}; + GitTree tree = {tree_ptr, git_tree_free}; git_buf to_sign = {}; const git_commit* head_ref[1] = {head_commit.get()}; @@ -605,6 +631,19 @@ ConversationRepositoryTest::addCommit(git_repository* repo, return commit_str ? commit_str : ""; } +void +ConversationRepositoryTest::addAll(git_repository* repo) +{ + // git add -A + git_index* index_ptr = nullptr; + git_strarray array = {nullptr, 0}; + if (git_repository_index(&index_ptr, repo) < 0) + return; + GitIndex index {index_ptr, git_index_free}; + git_index_add_all(index.get(), &array, 0, nullptr, nullptr); + git_index_write(index.get()); +} + void ConversationRepositoryTest::testMerge() { @@ -696,6 +735,148 @@ ConversationRepositoryTest::testDiff() CPPUNIT_ASSERT(changedFiles[1] == "devices/" + aliceDeviceId + ".crt"); } +void +ConversationRepositoryTest::testMergeProfileWithConflict() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto repository = ConversationRepository::createConversation(aliceAccount->weak()); + + // Assert that repository exists + CPPUNIT_ASSERT(repository != nullptr); + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository->id(); + CPPUNIT_ASSERT(fileutils::isDirectory(repoPath)); + + // Assert that first commit is signed by alice + git_repository* repo; + CPPUNIT_ASSERT(git_repository_open(&repo, repoPath.c_str()) == 0); + + auto profile = std::ofstream(repoPath + DIR_SEPARATOR_STR + "profile.vcf"); + if (profile.is_open()) { + profile << "TITLE: SWARM\n"; + profile << "SUBTITLE: Some description\n"; + profile << "AVATAR: BASE64\n"; + profile.close(); + } + addAll(repo); + auto id1 = addCommit(repo, aliceAccount, "main", "add profile"); + profile = std::ofstream(repoPath + DIR_SEPARATOR_STR + "profile.vcf"); + if (profile.is_open()) { + profile << "TITLE: SWARM\n"; + profile << "SUBTITLE: New description\n"; + profile << "AVATAR: BASE64\n"; + profile.close(); + } + addAll(repo); + auto id2 = addCommit(repo, aliceAccount, "main", "modify profile"); + + git_reference* ref = nullptr; + git_commit* commit = nullptr; + git_oid commit_id; + git_oid_fromstr(&commit_id, id1.c_str()); + git_commit_lookup(&commit, repo, &commit_id); + git_branch_create(&ref, repo, "to_merge", commit, false); + git_reference_free(ref); + git_repository_set_head(repo, "refs/heads/to_merge"); + + profile = std::ofstream(repoPath + DIR_SEPARATOR_STR + "profile.vcf"); + if (profile.is_open()) { + profile << "TITLE: SWARM\n"; + profile << "SUBTITLE: Another description\n"; + profile << "AVATAR: BASE64\n"; + profile.close(); + } + addAll(repo); + auto id3 = addCommit(repo, aliceAccount, "to_merge", "modify profile merge"); + + // This will create a merge commit + repository->merge(id3); + 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 = std::string(aliceAccount->currentDeviceId()); + auto uri = aliceAccount->getUsername(); + auto bobDeviceId = 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 DeviceId&, const std::string& name) { + successfullyReceive = name == "git://*"; + return true; + }); + + aliceAccount->connectionManager().onChannelRequest( + [&successfullyReceive](const DeviceId&, 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(DeviceId(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