diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 307856c836a20da372a5522d3a3cfae8b0e2513b..92872d1477c0ec23237997347c056edc51bd1ae8 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 164bd6155d32d8371c8b2cc487eace0e58d8d009..5eac7e20946579f3e39016beab05487263c0d112 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 3fffa78e705174cb03cd4a282d56cf5d729c4e1e..3ebf647c7a6047f5e3f546312030348305168254 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 86357d49c25972fc5c23b02286d6f8d38a3893b8..1316b9a7236285c5499a91bdc102923d68a3d50e 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 d076d8206ad12bf593ffc9bc28ab62b3d622c71c..cfd130f80e3a306f0f88da201cfc78c4358fc854 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 80645527a225a48f3f524a017f2e6846d9c83bac..b0dcb92fcb94438576bf9c0dd6aa51e6fab9f336 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 c63ed3bf7aba6102cc2e6295ed609b63d1892e64..8ae3b712f7b61e33d53a93068b642f10f970334f 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 7084f08444be4770d65935cadbdfa2dfac8b902b..6c2021bf3a14c1033633956a68caf4e86aa1b623 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 9ce74993ebe52551c07330070cd90accc7883bdf..67a4a6404c39855688fb0a60404f61e6557c89a2 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 b31cc746f432fa2d3bd07be7fc76b762ec6e4e17..53fd6858e48a3a9237dc1a56a6350c3c2910cc29 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 931b1fc6336ae632f4ad010307e4afd40910c9f6..bc146e8bab47898f27ad4dab6ca922093d1bca4f 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 c9e11216af554271e856439b5ed41602987b9cfd..e8ad1b94465c73b15ae1e1c70df11dc8e9747a8a 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 592b96ff8df31dd4313cb5c71919cc84c29214ad..e9a914e2f8d14c49e535bec01d4a21854004449d 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 ded480f57b4843d02ecf0d0f5b2c8bac10d63132..4451b60fd0c23b63f72d2fa223dfba14d47637c4 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 f7e3c2e8868123e216ba89f739ec07b68caa0d78..4895641dfddd3d40ae864d2a4cc65a73d4b22f53 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 129420ddd2d954ce9fc3b80a135125324df65567..87f2c8f2828421a952383cb08848a596e9560f38 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 7d66e9b290a8f17f4f822a81374839a6e1a27545..90e7403c03b05613cc01444a3b49fc96173b4b20 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 99479959e44d871a76bce0b4bd30e8df7923ffb7..50557fb0c56dc2ce9680562efd1f76a4fbc2497c 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 40ad6d0bffaa86b7cf56946a4d848b41826ad0f6..6bb07fe232021e6a8e2293e2fb2ed646f99da062 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