From 5af1041bf8d5954530fcb3c2e524620a1a28b263 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Tue, 11 Jan 2022 17:02:42 -0500 Subject: [PATCH] proposal: swarm: use log to search messages in conversations This gives to clients the ability to perform search for messages with several parameters (account's id, conversation's id, author, period, max number). (To discuss) This patch introduces the search API, and a signal (MessagesFound) to return a result. GitLab: https://git.jami.net/savoirfairelinux/jami-project/-/issues/1382 Change-Id: Ibc4665449fa0da71a015d1d18d6d0d3209331d43 --- .../cx.ring.Ring.ConfigurationManager.xml | 45 +++ bin/dbus/dbusclient.cpp | 2 + bin/dbus/dbusconfigurationmanager.cpp | 22 ++ bin/dbus/dbusconfigurationmanager.h | 9 + bin/jni/conversation.i | 11 + bin/jni/jni_interface.i | 1 + bin/nodejs/conversation.i | 11 + bin/nodejs/nodejs_interface.i | 1 + src/client/conversation_interface.cpp | 26 ++ src/client/ring_signal.cpp | 1 + src/jami/conversation_interface.h | 17 ++ src/jamidht/conversation.cpp | 117 +++----- src/jamidht/conversation.h | 12 + src/jamidht/conversation_module.cpp | 20 ++ src/jamidht/conversation_module.h | 9 + src/jamidht/conversationrepository.cpp | 280 +++++++++++++++--- src/jamidht/conversationrepository.h | 25 ++ test/agent/src/bindings/signal.cpp | 21 +- test/unitTest/conversation/conversation.cpp | 90 ++++++ 19 files changed, 585 insertions(+), 135 deletions(-) diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 307856c836..92872d1477 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1799,6 +1799,23 @@ <arg type="u" name="count" direction="out"/> </method> + <method name="searchConversation" tp:name-for-bindings="searchConversation"> + <tp:added version="13.4.0"/> + <tp:docstring> + Get how many messages there is since an interaction ("" for initial commit) + </tp:docstring> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="conversationId" direction="in"/> + <arg type="s" name="author" direction="in"/> + <arg type="s" name="lastId" direction="in"/> + <arg type="s" name="regexSearch" direction="in"/> + <arg type="s" name="type" direction="in"/> + <arg type="x" name="after" direction="in"/> + <arg type="x" name="before" direction="in"/> + <arg type="u" name="maxResult" direction="in"/> + <arg type="u" name="searchId" direction="out"/> + </method> + <signal name="mediaParametersChanged" tp:name-for-bindings="mediaParametersChanged"> <tp:added version="2.3.0"/> <tp:docstring> @@ -1900,6 +1917,34 @@ </arg> </signal> + <signal name="messagesFound" tp:name-for-bindings="messagesFound"> + <tp:added version="10.0.0"/> + <tp:docstring> + Notify clients when messages matching a regex are found + </tp:docstring> + <arg type="u" name="id"> + <tp:docstring> + Id of the related loadConversationMessages's request + </tp:docstring> + </arg> + <arg type="s" name="account_id"> + <tp:docstring> + Account id related + </tp:docstring> + </arg> + <arg type="s" name="conversation_id"> + <tp:docstring> + Conversation id + </tp:docstring> + </arg> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out3" value="VectorMapStringString"/> + <arg type="aa{ss}" name="messages"> + <tp:docstring> + Messages of the conversation + </tp:docstring> + </arg> + </signal> + <signal name="messageReceived" tp:name-for-bindings="messageReceived"> <tp:added version="10.0.0"/> <tp:docstring> diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index 164bd6155d..5eac7e2094 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -299,6 +299,8 @@ DBusClient::initLibrary(int flags) const std::map<std::string, SharedCallback> convEvHandlers = { exportable_callback<ConversationSignal::ConversationLoaded>( bind(&DBusConfigurationManager::conversationLoaded, confM, _1, _2, _3, _4)), + exportable_callback<ConversationSignal::MessagesFound>( + bind(&DBusConfigurationManager::messagesFound, confM, _1, _2, _3, _4)), exportable_callback<ConversationSignal::MessageReceived>( bind(&DBusConfigurationManager::messageReceived, confM, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationProfileUpdated>( diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 3fffa78e70..3ebf647c7a 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -939,6 +939,28 @@ DBusConfigurationManager::countInteractions(const std::string& accountId, return DRing::countInteractions(accountId, conversationId, toId, fromId, authorUri); } +uint32_t +DBusConfigurationManager::searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult) +{ + return DRing::searchConversation(accountId, + conversationId, + author, + lastId, + regexSearch, + type, + after, + before, + maxResult); +} + bool DBusConfigurationManager::isAudioMeterActive(const std::string& id) { diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index 86357d49c2..1316b9a723 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -290,6 +290,15 @@ public: const std::string& toId, const std::string& fromId, const std::string& authorUri); + uint32_t searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult); }; #endif // __RING_DBUSCONFIGURATIONMANAGER_H__ diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index d076d8206a..cfd130f80e 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -26,6 +26,7 @@ class ConversationCallback { public: virtual ~ConversationCallback(){} virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} + virtual void messagesFound(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){} virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){} virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){} @@ -61,12 +62,22 @@ namespace DRing { uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, size_t n); uint32_t loadConversationUntil(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, const std::string& toMessage); uint32_t countInteractions(const std::string& accountId, const std::string& conversationId, const std::string& toId, const std::string& fromId, const std::string& authorUri); + uint32_t searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult); } class ConversationCallback { public: virtual ~ConversationCallback(){} virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} + virtual void messagesFound(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){} virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){} virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){} diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index 80645527a2..b0dcb92fcb 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -320,6 +320,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM const std::map<std::string, SharedCallback> conversationHandlers = { exportable_callback<ConversationSignal::ConversationLoaded>(bind(&ConversationCallback::conversationLoaded, convM, _1, _2, _3, _4)), + exportable_callback<ConversationSignal::MessagesFound>(bind(&ConversationCallback::messagesFound, convM, _1, _2, _3, _4)), exportable_callback<ConversationSignal::MessageReceived>(bind(&ConversationCallback::messageReceived, convM, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationProfileUpdated>(bind(&ConversationCallback::conversationProfileUpdated, convM, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&ConversationCallback::conversationRequestReceived, convM, _1, _2, _3)), diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index c63ed3bf7a..8ae3b712f7 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -26,6 +26,7 @@ class ConversationCallback { public: virtual ~ConversationCallback(){} virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} + virtual void messagesFound(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){} virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){} virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){} @@ -61,6 +62,15 @@ namespace DRing { uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, size_t n); uint32_t loadConversationUntil(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, const std::string& toMessage); uint32_t countInteractions(const std::string& accountId, const std::string& conversationId, const std::string& toId, const std::string& fromId, const std::string& authorUri); + uint32_t searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult); } @@ -68,6 +78,7 @@ class ConversationCallback { public: virtual ~ConversationCallback(){} virtual void conversationLoaded(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} + virtual void messagesFound(uint32_t /* id */, const std::string& /*accountId*/, const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/){} virtual void messageReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*message*/){} virtual void conversationProfileUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*profile*/){} virtual void conversationRequestReceived(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map<std::string, std::string> /*metadatas*/){} diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i index 7084f08444..6c2021bf3a 100644 --- a/bin/nodejs/nodejs_interface.i +++ b/bin/nodejs/nodejs_interface.i @@ -146,6 +146,7 @@ void init(const SWIGV8_VALUE& funcMap){ const std::map<std::string, SharedCallback> conversationHandlers = { exportable_callback<ConversationSignal::ConversationLoaded>(bind(&conversationLoaded, _1, _2, _3, _4)), + exportable_callback<ConversationSignal::MessagesFound>(bind(&messagesFound, _1, _2, _3, _4)), exportable_callback<ConversationSignal::MessageReceived>(bind(&messageReceived, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationProfileUpdated>(bind(&conversationProfileUpdated, _1, _2, _3)), exportable_callback<ConversationSignal::ConversationRequestReceived>(bind(&conversationRequestReceived, _1, _2, _3)), diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp index 9ce74993eb..67a4a6404c 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -185,4 +185,30 @@ countInteractions(const std::string& accountId, return 0; } +uint32_t +searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult) +{ + uint32_t res = 0; + jami::Filter filter {author, lastId, regexSearch, type, after, before, maxResult}; + for (const auto& accId : jami::Manager::instance().getAccountList()) { + if (!accountId.empty() && accId != accountId) + continue; + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accId)) { + res = std::uniform_int_distribution<uint32_t>()(acc->rand); + if (auto convModule = acc->convModule()) { + convModule->search(res, conversationId, filter); + } + } + } + return res; +} + } // namespace DRing diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index b31cc746f4..53fd6858e4 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -127,6 +127,7 @@ getSignalHandlers() /* Conversation */ exported_callback<DRing::ConversationSignal::ConversationLoaded>(), + exported_callback<DRing::ConversationSignal::MessagesFound>(), exported_callback<DRing::ConversationSignal::MessageReceived>(), exported_callback<DRing::ConversationSignal::ConversationProfileUpdated>(), exported_callback<DRing::ConversationSignal::ConversationRequestReceived>(), diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h index 931b1fc633..bc146e8bab 100644 --- a/src/jami/conversation_interface.h +++ b/src/jami/conversation_interface.h @@ -78,6 +78,15 @@ DRING_PUBLIC uint32_t countInteractions(const std::string& accountId, const std::string& toId, const std::string& fromId, const std::string& authorUri); +DRING_PUBLIC uint32_t searchConversation(const std::string& accountId, + const std::string& conversationId, + const std::string& author, + const std::string& lastId, + const std::string& regexSearch, + const std::string& type, + const int64_t& after, + const int64_t& before, + const uint32_t& maxResult); struct DRING_PUBLIC ConversationSignal { @@ -89,6 +98,14 @@ struct DRING_PUBLIC ConversationSignal const std::string& /* conversationId */, std::vector<std::map<std::string, std::string>> /*messages*/); }; + struct DRING_PUBLIC MessagesFound + { + constexpr static const char* name = "MessagesFound"; + using cb_type = void(uint32_t /* id */, + const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::vector<std::map<std::string, std::string>> /*messages*/); + }; struct DRING_PUBLIC MessageReceived { constexpr static const char* name = "MessageReceived"; diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index c9e11216af..e8ad1b9446 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -193,7 +193,7 @@ public: convcommits.emplace_back(*commit); } } - announce(convCommitToMap(convcommits)); + announce(repository_->convCommitToMap(convcommits)); } void announce(const std::vector<std::map<std::string, std::string>>& commits) const @@ -347,10 +347,6 @@ public: std::unique_ptr<ConversationRepository> repository_; std::weak_ptr<JamiAccount> account_; std::atomic_bool isRemoving_ {false}; - std::vector<std::map<std::string, std::string>> convCommitToMap( - const std::vector<ConversationCommit>& commits) const; - std::optional<std::map<std::string, std::string>> convCommitToMap( - const ConversationCommit& commit) const; std::vector<std::map<std::string, std::string>> loadMessages(const std::string& fromMessage = "", const std::string& toMessage = "", size_t n = 0); @@ -397,81 +393,6 @@ Conversation::Impl::repoPath() const + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository_->id(); } -std::optional<std::map<std::string, std::string>> -Conversation::Impl::convCommitToMap(const ConversationCommit& commit) const -{ - if (!repository_) - return std::nullopt; - auto authorDevice = commit.author.email; - auto authorId = repository_->uriFromDevice(authorDevice); - if (authorId == "") - return std::nullopt; - - std::string parents; - auto parentsSize = commit.parents.size(); - for (std::size_t i = 0; i < parentsSize; ++i) { - parents += commit.parents[i]; - if (i != parentsSize - 1) - parents += ","; - } - std::string type; - if (parentsSize > 1) - type = "merge"; - std::string body {}; - std::map<std::string, std::string> message; - if (type.empty()) { - std::string err; - Json::Value cm; - Json::CharReaderBuilder rbuilder; - auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); - if (reader->parse(commit.commit_msg.data(), - commit.commit_msg.data() + commit.commit_msg.size(), - &cm, - &err)) { - for (auto const& id : cm.getMemberNames()) { - if (id == "type") { - type = cm[id].asString(); - continue; - } - message.insert({id, cm[id].asString()}); - } - } else { - JAMI_WARN("%s", err.c_str()); - } - } - if (type == "application/data-transfer+json") { - // Avoid the client to do the concatenation - message["fileId"] = commit.id + "_" + message["tid"]; - auto extension = fileutils::getFileExtension(message["displayName"]); - if (!extension.empty()) - message["fileId"] += "." + extension; - } - message[ConversationMapKeys::ID] = commit.id; - message["parents"] = parents; - message["linearizedParent"] = commit.linearized_parent; - message["author"] = authorId; - if (type.empty()) - return std::nullopt; - message["type"] = type; - message["timestamp"] = std::to_string(commit.timestamp); - - return message; -} - -std::vector<std::map<std::string, std::string>> -Conversation::Impl::convCommitToMap(const std::vector<ConversationCommit>& commits) const -{ - std::vector<std::map<std::string, std::string>> result = {}; - result.reserve(commits.size()); - for (const auto& commit : commits) { - auto message = convCommitToMap(commit); - if (message == std::nullopt) - break; - result.emplace_back(*message); - } - return result; -} - std::vector<std::map<std::string, std::string>> Conversation::Impl::loadMessages(const std::string& fromMessage, const std::string& toMessage, @@ -484,7 +405,7 @@ Conversation::Impl::loadMessages(const std::string& fromMessage, convCommits = repository_->logN(fromMessage, n); else convCommits = repository_->log(fromMessage, toMessage); - return convCommitToMap(convCommits); + return repository_->convCommitToMap(convCommits); } Conversation::Conversation(const std::weak_ptr<JamiAccount>& account, @@ -848,7 +769,7 @@ Conversation::getCommit(const std::string& commitId) const auto commit = pimpl_->repository_->getCommit(commitId); if (commit == std::nullopt) return std::nullopt; - return pimpl_->convCommitToMap(*commit); + return pimpl_->repository_->convCommitToMap(*commit); } void @@ -917,7 +838,7 @@ Conversation::Impl::mergeHistory(const std::string& uri) } JAMI_DBG("Successfully merge history with %s", uri.c_str()); - auto result = convCommitToMap(newCommits); + auto result = repository_->convCommitToMap(newCommits); for (const auto& commit : result) { auto it = commit.find("type"); if (it != commit.end() && it->second == "member") { @@ -1349,4 +1270,34 @@ Conversation::countInteractions(const std::string& toId, return pimpl_->repository_->log(fromId, toId, false, true, authorUri).size(); } +void +Conversation::search(uint32_t req, + const Filter& filter, + const std::shared_ptr<std::atomic_int>& flag) const +{ + // Because logging a conversation can take quite some time, + // do it asynchronously + dht::ThreadPool::io().run([w = weak(), req, filter, flag] { + if (auto sthis = w.lock()) { + auto acc = sthis->pimpl_->account_.lock(); + if (!acc) + return; + auto commits = sthis->pimpl_->repository_->search(filter); + if (commits.size() > 0) + emitSignal<DRing::ConversationSignal::MessagesFound>(req, + acc->getAccountID(), + sthis->id(), + std::move(commits)); + // If we're the latest thread, inform client that the search is finished + if ((*flag)-- == 1 /* decrement return the old value */) { + emitSignal<DRing::ConversationSignal::MessagesFound>( + req, + acc->getAccountID(), + std::string {}, + std::vector<std::map<std::string, std::string>> {}); + } + } + }); +} + } // namespace jami diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 592b96ff8d..e9a914e2f8 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -27,6 +27,7 @@ #include <json/json.h> #include <msgpack.hpp> +#include "jamidht/conversationrepository.h" #include "jami/datatransfer_interface.h" #include "conversationrepository.h" @@ -371,6 +372,17 @@ public: const std::string& fromId = "", const std::string& authorUri = "") const; + /** + * Search in the conversation via a filter + * @param req Id of the request + * @param filter Parameters for the search + * @param flag To check when search is finished + * @note triggers messagesFound + */ + void search(uint32_t req, + const Filter& filter, + const std::shared_ptr<std::atomic_int>& flag) const; + private: std::shared_ptr<Conversation> shared() { diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index ded480f57b..4451b60fd0 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -1620,6 +1620,26 @@ ConversationModule::countInteractions(const std::string& convId, return 0; } +void +ConversationModule::search(uint32_t req, const std::string& convId, const Filter& filter) const +{ + std::unique_lock<std::mutex> lk(pimpl_->conversationsMtx_); + auto finishedFlag = std::make_shared<std::atomic_int>(pimpl_->conversations_.size()); + for (const auto& [cid, conversation] : pimpl_->conversations_) { + if (!conversation || (!convId.empty() && convId != cid)) { + if ((*finishedFlag)-- == 1) { + emitSignal<DRing::ConversationSignal::MessagesFound>( + req, + pimpl_->accountId_, + std::string {}, + std::vector<std::map<std::string, std::string>> {}); + } + continue; + } + conversation->search(req, filter, finishedFlag); + } +} + void ConversationModule::updateConversationInfos(const std::string& conversationId, const std::map<std::string, std::string>& infos, diff --git a/src/jamidht/conversation_module.h b/src/jamidht/conversation_module.h index f7e3c2e886..4895641dfd 100644 --- a/src/jamidht/conversation_module.h +++ b/src/jamidht/conversation_module.h @@ -295,6 +295,15 @@ public: const std::string& fromId, const std::string& authorUri) const; + /** + * Search in conversations via a filter + * @param req Id of the request + * @param convId Leave empty to search in all conversation, else add the conversation's id + * @param filter Parameters for the search + * @note triggers messagesFound + */ + void search(uint32_t req, const std::string& convId, const Filter& filter) const; + // Conversation's infos management /** * Update metadata from conversations (like title, avatar, etc) diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index 129420ddd2..87f2c8f282 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -54,6 +54,13 @@ as_view(const GitObject& blob) return as_view(reinterpret_cast<git_blob*>(blob.get())); } +enum class CallbackResult { Skip, Break, Ok }; + +using PreConditionCb + = std::function<CallbackResult(const std::string&, const GitAuthor&, const GitCommit&)>; +using PostConditionCb + = std::function<bool(const std::string&, const GitAuthor&, ConversationCommit&)>; + class ConversationRepository::Impl { public: @@ -133,12 +140,18 @@ public: std::string diffStats(const GitDiff& diff) const; std::vector<ConversationCommit> behind(const std::string& from) const; + void forEachCommit(PreConditionCb&& preCondition, + std::function<void(ConversationCommit&&)>&& emplaceCb, + PostConditionCb&& postCondition, + const std::string& from = "", + bool logIfNotFound = true) const; std::vector<ConversationCommit> log(const std::string& from, const std::string& to, unsigned n, bool logIfNotFound = false, bool fastLog = false, const std::string& authorUri = "") const; + std::vector<std::map<std::string, std::string>> search(const Filter& filter) const; GitObject fileAtTree(const std::string& path, const GitTree& tree) const; GitObject memberCertificate(std::string_view memberUri, const GitTree& tree) const; @@ -183,6 +196,9 @@ public: void initMembers(); + std::optional<std::map<std::string, std::string>> convCommitToMap( + const ConversationCommit& commit) const; + // Permissions MemberRole updateProfilePermLvl_ {MemberRole::ADMIN}; @@ -1867,22 +1883,20 @@ ConversationRepository::Impl::behind(const std::string& from) const return log(from, to, 0); } -std::vector<ConversationCommit> -ConversationRepository::Impl::log(const std::string& from, - const std::string& to, - unsigned n, - bool logIfNotFound, - bool fastLog, - const std::string& authorUri) const +void +ConversationRepository::Impl::forEachCommit(PreConditionCb&& preCondition, + std::function<void(ConversationCommit&&)>&& emplaceCb, + PostConditionCb&& postCondition, + const std::string& from, + bool logIfNotFound) const { - std::vector<ConversationCommit> commits {}; git_oid oid, oidFrom, oidMerge; // Note: Start from head to get all merge possibilities and correct linearized parent. auto repo = repository(); if (!repo or git_reference_name_to_id(&oid, repo.get(), "HEAD") < 0) { JAMI_ERR("Cannot get reference for HEAD"); - return commits; + return; } if (from != "" && git_oid_fromstr(&oidFrom, from.c_str()) == 0) { @@ -1901,51 +1915,26 @@ ConversationRepository::Impl::log(const std::string& from, // there). only log if the fail is unwanted. if (logIfNotFound) JAMI_DBG("Couldn't init revwalker for conversation %s", id_.c_str()); - return commits; + return; } GitRevWalker walker {walker_ptr, git_revwalk_free}; git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME); - auto startLogging = from == ""; for (auto idx = 0u; !git_revwalk_next(&oid, walker.get()); ++idx) { git_commit* commit_ptr = nullptr; std::string id = git_oid_tostr_s(&oid); - if (!commits.empty()) { - // Set linearized parent - commits.rbegin()->linearized_parent = id; - } if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) { JAMI_WARN("Failed to look up commit %s", id.c_str()); break; } GitCommit commit {commit_ptr, git_commit_free}; - if ((n != 0 && commits.size() == n) || (id == to)) - break; - - if (!startLogging && from != "" && from == id) - startLogging = true; - if (!startLogging) - continue; const git_signature* sig = git_commit_author(commit.get()); GitAuthor author; author.name = sig->name; author.email = sig->email; - if (fastLog) { - if (authorUri != "") { - auto cert = tls::CertificateStore::instance().getCertificate(author.email); - if (cert && cert->issuer) { - if (authorUri == cert->issuer->getId().toString()) - break; - } - } - // Used to only count commit - commits.emplace(commits.end(), ConversationCommit {}); - continue; - } - std::vector<std::string> parents; auto parentsCount = git_commit_parentcount(commit.get()); for (unsigned int p = 0; p < parentsCount; ++p) { @@ -1957,25 +1946,145 @@ ConversationRepository::Impl::log(const std::string& from, } } - auto cc = commits.emplace(commits.end(), ConversationCommit {}); - cc->id = std::move(id); - cc->commit_msg = git_commit_message(commit.get()); - cc->author = std::move(author); - cc->parents = std::move(parents); + auto result = preCondition(id, author, commit); + if (result == CallbackResult::Skip) + continue; + else if (result == CallbackResult::Break) + break; + + ConversationCommit cc; + cc.id = id; + cc.commit_msg = git_commit_message(commit.get()); + cc.author = std::move(author); + cc.parents = std::move(parents); git_buf signature = {}, signed_data = {}; 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 { - cc->signature = base64::decode( + cc.signature = base64::decode( std::string(signature.ptr, signature.ptr + signature.size)); - cc->signed_content = std::vector<uint8_t>(signed_data.ptr, - signed_data.ptr + signed_data.size); + cc.signed_content = std::vector<uint8_t>(signed_data.ptr, + signed_data.ptr + signed_data.size); } git_buf_dispose(&signature); git_buf_dispose(&signed_data); - cc->timestamp = git_commit_time(commit.get()); + cc.timestamp = git_commit_time(commit.get()); + + auto post = postCondition(id, author, cc); + emplaceCb(std::move(cc)); + + if (post) + break; } +} + +std::vector<ConversationCommit> +ConversationRepository::Impl::log(const std::string& from, + const std::string& to, + unsigned n, + bool logIfNotFound, + bool fastLog, + const std::string& authorUri) const +{ + std::vector<ConversationCommit> commits {}; + auto startLogging = from == ""; + forEachCommit( + [&](const auto& id, const auto& author, const auto&) { + if (!commits.empty()) { + // Set linearized parent + commits.rbegin()->linearized_parent = id; + } + + if ((n != 0 && commits.size() == n) || (id == to)) + return CallbackResult::Break; // Stop logging + + if (!startLogging && from != "" && from == id) + startLogging = true; + if (!startLogging) + return CallbackResult::Skip; // Start logging after this one + + if (fastLog) { + if (authorUri != "") { + if (authorUri == uriFromDevice(author.email)) { + return CallbackResult::Break; // Found author, stop + } + } + // Used to only count commit + commits.emplace(commits.end(), ConversationCommit {}); + return CallbackResult::Skip; + } + + return CallbackResult::Ok; // Continue + }, + [&](auto&& cc) { commits.emplace(commits.end(), std::forward<decltype(cc)>(cc)); }, + [](auto, auto, auto) { return false; }, + from, + logIfNotFound); + return commits; +} + +std::vector<std::map<std::string, std::string>> +ConversationRepository::Impl::search(const Filter& filter) const +{ + std::vector<std::map<std::string, std::string>> commits {}; + forEachCommit( + [&](const auto& id, const auto& author, auto& commit) { + if (!commits.empty()) { + // Set linearized parent + commits.rbegin()->at("linearizedParent") = id; + } + + if (!filter.author.empty() && filter.author != uriFromDevice(author.email)) { + // Filter author + return CallbackResult::Skip; + } + auto commitTime = git_commit_time(commit.get()); + if (filter.before && filter.before < commitTime) { + // Only get commits before this date + return CallbackResult::Skip; + } + if (filter.after && filter.after > commitTime) { + // Only get commits before this date + if (git_commit_parentcount(commit.get()) <= 1) + return CallbackResult::Break; + else + return CallbackResult::Skip; // Because we are sorting it with + // GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME + } + + return CallbackResult::Ok; // Continue + }, + [&](auto&& cc) { + auto content = convCommitToMap(cc); + auto contentType = content ? content->at("type") : ""; + auto isSearchable = contentType == "text/plain" + || contentType == "application/data-transfer+json"; + if (filter.type.empty() && !isSearchable) { + // Not searchable, at least for now + return; + } else if (contentType == filter.type) { + if (isSearchable) { + // If it's a text match the body, else the display name + auto body = contentType == "text/plain" ? content->at("body") + : content->at("displayName"); + std::smatch body_match; + if (std::regex_search(body, body_match, std::regex(filter.regexSearch))) { + commits.emplace(commits.end(), std::move(*content)); + } + } else { + // Matching type, just add it to the results + commits.emplace(commits.end(), std::move(*content)); + } + } + }, + [&](auto id, auto, auto) { + if (filter.maxResult != 0 && commits.size() == filter.maxResult) + return true; + if (id == filter.lastId) + return true; + return false; + }); return commits; } @@ -2182,6 +2291,63 @@ ConversationRepository::Impl::initMembers() } } +std::optional<std::map<std::string, std::string>> +ConversationRepository::Impl::convCommitToMap(const ConversationCommit& commit) const +{ + auto authorId = uriFromDevice(commit.author.email); + if (authorId.empty()) + return std::nullopt; + std::string parents; + auto parentsSize = commit.parents.size(); + for (std::size_t i = 0; i < parentsSize; ++i) { + parents += commit.parents[i]; + if (i != parentsSize - 1) + parents += ","; + } + std::string type {}; + if (parentsSize > 1) + type = "merge"; + std::string body {}; + std::map<std::string, std::string> message; + if (type.empty()) { + std::string err; + Json::Value cm; + Json::CharReaderBuilder rbuilder; + auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader()); + if (reader->parse(commit.commit_msg.data(), + commit.commit_msg.data() + commit.commit_msg.size(), + &cm, + &err)) { + for (auto const& id : cm.getMemberNames()) { + if (id == "type") { + type = cm[id].asString(); + continue; + } + message.insert({id, cm[id].asString()}); + } + } else { + JAMI_WARN("%s", err.c_str()); + } + } + if (type.empty()) { + return std::nullopt; + } else if (type == "application/data-transfer+json") { + // Avoid the client to do the concatenation + message["fileId"] = commit.id + "_" + message["tid"]; + auto extension = fileutils::getFileExtension(message["displayName"]); + if (!extension.empty()) + message["fileId"] += "." + extension; + } + message["id"] = commit.id; + message["parents"] = parents; + message["linearizedParent"] = commit.linearized_parent; + message["author"] = authorId; + message["type"] = type; + message["timestamp"] = std::to_string(commit.timestamp); + + return message; +} + std::string ConversationRepository::Impl::diffStats(const GitDiff& diff) const { @@ -2795,6 +2961,12 @@ ConversationRepository::log(const std::string& from, return pimpl_->log(from, to, 0, logIfNotFound, fastLog, authorUri); } +std::vector<std::map<std::string, std::string>> +ConversationRepository::search(const Filter& filter) const +{ + return pimpl_->search(filter); +} + std::optional<ConversationCommit> ConversationRepository::getCommit(const std::string& commitId, bool logIfNotFound) const { @@ -3594,4 +3766,24 @@ ConversationRepository::getHead() const return {}; } +std::optional<std::map<std::string, std::string>> +ConversationRepository::convCommitToMap(const ConversationCommit& commit) const +{ + return pimpl_->convCommitToMap(commit); +} + +std::vector<std::map<std::string, std::string>> +ConversationRepository::convCommitToMap(const std::vector<ConversationCommit>& commits) const +{ + std::vector<std::map<std::string, std::string>> result = {}; + result.reserve(commits.size()); + for (const auto& commit : commits) { + auto message = pimpl_->convCommitToMap(commit); + if (message == std::nullopt) + continue; + result.emplace_back(*message); + } + return result; +} + } // namespace jami diff --git a/src/jamidht/conversationrepository.h b/src/jamidht/conversationrepository.h index 7d66e9b290..90e7403c03 100644 --- a/src/jamidht/conversationrepository.h +++ b/src/jamidht/conversationrepository.h @@ -53,6 +53,17 @@ constexpr auto EUNAUTHORIZED = 4; class JamiAccount; class ChannelSocket; +struct Filter +{ + std::string author; + std::string lastId; + std::string regexSearch; + std::string type; + int64_t after {0}; + int64_t before {0}; + uint32_t maxResult {0}; +}; + struct GitAuthor { std::string name {}; @@ -205,6 +216,13 @@ public: std::optional<ConversationCommit> getCommit(const std::string& commitId, bool logIfNotFound = true) const; + /** + * Search in the conversation via a filter + * @param filter Parameters for the search + * @return matching commits + */ + std::vector<std::map<std::string, std::string>> search(const Filter& filter) const; + /** * Get parent via topological + date sort in branch main of a commit * @param commitId id to choice @@ -366,6 +384,13 @@ public: * @return account's URI */ std::string uriFromDevice(const std::string& deviceId) const; + /** + * Convert ConversationCommit to MapStringString for the client + */ + std::vector<std::map<std::string, std::string>> convCommitToMap( + const std::vector<ConversationCommit>& commits) const; + std::optional<std::map<std::string, std::string>> convCommitToMap( + const ConversationCommit& commit) const; /** * Get current HEAD hash diff --git a/test/agent/src/bindings/signal.cpp b/test/agent/src/bindings/signal.cpp index 99479959e4..50557fb0c5 100644 --- a/test/agent/src/bindings/signal.cpp +++ b/test/agent/src/bindings/signal.cpp @@ -209,20 +209,19 @@ install_signal_primitives(void*) add_handler<DRing::CallSignal::RecordPlaybackFilepath, const std::string&, const std::string&>( handlers, "record-playback-filepath"); - add_handler<DRing::CallSignal::ConferenceCreated, - const std::string&, const std::string&>(handlers, - "conference-created"); + add_handler<DRing::CallSignal::ConferenceCreated, const std::string&, const std::string&>( + handlers, "conference-created"); add_handler<DRing::CallSignal::ConferenceChanged, - const std::string&, const std::string&, const std::string&>( - handlers, "conference-changed"); + const std::string&, + const std::string&, + const std::string&>(handlers, "conference-changed"); add_handler<DRing::CallSignal::UpdatePlaybackScale, const std::string&, unsigned, unsigned>( handlers, "update-playback-scale"); - add_handler<DRing::CallSignal::ConferenceRemoved, - const std::string&, const std::string&>(handlers, - "conference-removed"); + add_handler<DRing::CallSignal::ConferenceRemoved, const std::string&, const std::string&>( + handlers, "conference-removed"); add_handler<DRing::CallSignal::RecordingStateChanged, const std::string&, int>( handlers, "recording-state-changed"); @@ -500,6 +499,12 @@ install_signal_primitives(void*) const std::string&, std::vector<std::map<std::string, std::string>>>(handlers, "conversation-loaded"); + add_handler<DRing::ConversationSignal::MessagesFound, + uint32_t, + const std::string&, + const std::string&, + std::vector<std::map<std::string, std::string>>>(handlers, "messages-found"); + add_handler<DRing::ConversationSignal::MessageReceived, const std::string&, const std::string&, diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 40ad6d0bff..6bb07fe232 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -110,6 +110,7 @@ private: void testImportMalformedContacts(); void testRemoveReaddMultipleDevice(); void testSendReply(); + void testSearchInConv(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); @@ -150,6 +151,7 @@ private: CPPUNIT_TEST(testImportMalformedContacts); CPPUNIT_TEST(testRemoveReaddMultipleDevice); CPPUNIT_TEST(testSendReply); + CPPUNIT_TEST(testSearchInConv); CPPUNIT_TEST_SUITE_END(); }; @@ -3006,6 +3008,94 @@ ConversationTest::testSendReply() CPPUNIT_ASSERT(!cv.wait_for(lk, 10s, [&]() { return messageBobReceived.size() == 3; })); } +void +ConversationTest::testSearchInConv() +{ + 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, + messageReceived = false; + std::vector<std::string> bobMessages; + std::string convId = ""; + confHandlers.insert(DRing::exportable_callback<DRing::ConfigurationSignal::IncomingTrustRequest>( + [&](const std::string& account_id, + const std::string& /*from*/, + const std::string& /*conversationId*/, + const std::vector<uint8_t>& /*payload*/, + time_t /*received*/) { + if (account_id == bobId) + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == aliceId) { + convId = conversationId; + } else if (accountId == bobId) { + conversationReady = true; + } + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId) { + if (message["type"] == "member") + memberMessageGenerated = true; + } else if (accountId == bobId) { + messageReceived = true; + } + cv.notify_one(); + })); + std::vector<std::map<std::string, std::string>> messages; + bool finished = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessagesFound>( + [&](uint32_t, + const std::string&, + const std::string& conversationId, + std::vector<std::map<std::string, std::string>> msg) { + if (conversationId == convId) + messages = msg; + finished = conversationId.empty(); + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + requestReceived = false; + aliceAccount->addContact(bobUri); + aliceAccount->sendTrustRequest(bobUri, {}); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); + CPPUNIT_ASSERT(bobAccount->acceptTrustRequest(aliceUri)); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&]() { return conversationReady && memberMessageGenerated; })); + // Add some messages + messageReceived = false; + DRing::sendMessage(aliceId, convId, "message 1"s, ""); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messageReceived; })); + messageReceived = false; + DRing::sendMessage(aliceId, convId, "message 2"s, ""); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messageReceived; })); + messageReceived = false; + DRing::sendMessage(aliceId, convId, "message 3"s, ""); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messageReceived; })); + DRing::searchConversation(aliceId, convId, "", "", "message", 0, 0, 0); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messages.size() == 3 && finished; })); + messages.clear(); + finished = false; + DRing::searchConversation(aliceId, convId, "", "", "message 2", 0, 0, 0); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messages.size() == 1 && finished; })); + messages.clear(); + finished = false; + DRing::searchConversation(aliceId, convId, "", "", "foo", 0, 0, 0); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messages.size() == 0 && finished; })); +} + } // namespace test } // namespace jami -- GitLab