From 0f6b74b1945951c45a7bcad4f8ff2bed1219f3b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Tue, 11 Oct 2022 16:22:23 -0400 Subject: [PATCH] conversation: add support for message edition For now, only be able to edit messages from our-self and with the text/plain type Change-Id: I20c6ed7dad3892f0fa655812242b9ca6e77e69b3 GitLab: #316 --- .../cx.ring.Ring.ConfigurationManager.xml | 9 +- bin/dbus/dbusconfigurationmanager.cpp | 5 +- bin/dbus/dbusconfigurationmanager.h | 3 +- bin/jni/conversation.i | 2 +- bin/nodejs/conversation.i | 2 +- configure.ac | 2 +- meson.build | 2 +- src/client/conversation_interface.cpp | 12 ++- src/jami/conversation_interface.h | 3 +- src/jamidht/conversation_module.cpp | 42 ++++++++ src/jamidht/conversation_module.h | 4 + src/jamidht/conversationrepository.cpp | 54 ++++++++++- test/unitTest/conversation/conversation.cpp | 96 +++++++++++++++++++ 13 files changed, 218 insertions(+), 18 deletions(-) diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 9b3dd77607..469048349e 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1774,14 +1774,17 @@ </method> <method name="sendMessage" tp:name-for-bindings="sendMessage"> - <tp:added version="10.0.0"/> + <tp:added version="13.6.0"/> <tp:docstring> - Add a message to a conversation + Add a message to a conversation. + Flag: 0 = reply (if commitId is not empty), else single message + 1 = edit message </tp:docstring> <arg type="s" name="accountId" direction="in"/> <arg type="s" name="conversationId" direction="in"/> <arg type="s" name="message" direction="in"/> - <arg type="s" name="replyTo" direction="in"/> + <arg type="s" name="commitId" direction="in"/> + <arg type="i" name="flag" direction="in"/> </method> <method name="loadConversationMessages" tp:name-for-bindings="loadConversationMessages"> diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 3effef201e..57466c0c9e 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -921,9 +921,10 @@ void DBusConfigurationManager::sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, - const std::string& replyTo) + const std::string& replyTo, + const int32_t& flag) { - DRing::sendMessage(accountId, conversationId, message, replyTo); + DRing::sendMessage(accountId, conversationId, message, replyTo, flag); } uint32_t diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index b145487307..f317cd4363 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -281,7 +281,8 @@ public: void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, - const std::string& replyTo); + const std::string& replyTo, + const int32_t& flag); uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index 4437faaf3e..6a628904cf 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -61,7 +61,7 @@ namespace DRing { std::vector<std::map<std::string, std::string>> getConversationMembers(const std::string& accountId, const std::string& conversationId); // Message send/load - void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& replyTo); + void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& replyTo, const int32_t& flag); 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); diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index f645253f66..ac254bcd26 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -61,7 +61,7 @@ namespace DRing { std::vector<std::map<std::string, std::string>> getConversationMembers(const std::string& accountId, const std::string& conversationId); // Message send/load - void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& replyTo); + void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, const std::string& replyTo, const int32_t& flag); 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); diff --git a/configure.ac b/configure.ac index 3988e089f5..27215d0446 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl Jami - configure.ac dnl Process this file with autoconf to produce a configure script. AC_PREREQ([2.69]) -AC_INIT([Jami Daemon],[13.5.0],[jami@gnu.org],[jami]) +AC_INIT([Jami Daemon],[13.6.0],[jami@gnu.org],[jami]) dnl Clear the implicit flags that default to '-g -O2', otherwise they dnl take precedence over the values we set via the diff --git a/meson.build b/meson.build index bd14e1b377..3312907c3c 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jami-daemon', ['c', 'cpp'], - version: '13.5.0', + version: '13.6.0', license: 'GPL3+', default_options: ['cpp_std=gnu++17', 'buildtype=debugoptimized'], meson_version:'>= 0.56' diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp index 47607790a7..7312f3f2d9 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -160,11 +160,17 @@ void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, - const std::string& replyTo) + const std::string& commitId, + const int32_t& flag) { if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) - if (auto convModule = acc->convModule()) - convModule->sendMessage(conversationId, message, replyTo); + if (auto convModule = acc->convModule()) { + if (flag == 0 /* Reply or simple commit */) { + convModule->sendMessage(conversationId, message, commitId); + } else if (flag == 1 /* message edition */) { + convModule->editMessage(conversationId, message, commitId); + } + } } uint32_t diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h index bb05c1920a..404b7332c9 100644 --- a/src/jami/conversation_interface.h +++ b/src/jami/conversation_interface.h @@ -69,7 +69,8 @@ DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConversationMemb DRING_PUBLIC void sendMessage(const std::string& accountId, const std::string& conversationId, const std::string& message, - const std::string& replyTo); + const std::string& replyTo, + const int32_t& flag = 0); DRING_PUBLIC uint32_t loadConversationMessages(const std::string& accountId, const std::string& conversationId, const std::string& fromMessage, diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index f9850f904a..6bebba879e 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -227,6 +227,10 @@ public: OnCommitCb&& onCommit = {}, OnDoneCb&& cb = {}); + void editMessage(const std::string& conversationId, + const std::string& newBody, + const std::string& editedId); + // The following informations are stored on the disk mutable std::mutex convInfosMtx_; // Note, should be locked after conversationsMtx_ if needed std::map<std::string, ConvInfo> convInfos_; @@ -842,6 +846,36 @@ ConversationModule::Impl::sendMessage(const std::string& conversationId, } } +void +ConversationModule::Impl::editMessage(const std::string& conversationId, + const std::string& newBody, + const std::string& editedId) +{ + // Check that editedId is a valid commit, from ourself and plain/text + auto validCommit = false; + { + std::lock_guard<std::mutex> lk(conversationsMtx_); + auto conversation = conversations_.find(conversationId); + if (conversation != conversations_.end() && conversation->second) { + auto commit = conversation->second->getCommit(editedId); + if (commit != std::nullopt) { + validCommit = commit->at("author") == username_ + && commit->at("type") == "text/plain"; + } + } + } + if (!validCommit) { + JAMI_ERROR("Cannot edit commit {:s}", editedId); + return; + } + // Commit message edition + Json::Value json; + json["body"] = newBody; + json["edit"] = editedId; + json["type"] = "application/edited-message"; + sendMessage(conversationId, std::move(json)); +} + //////////////////////////////////////////////////////////////// void @@ -1306,6 +1340,14 @@ ConversationModule::sendMessage(const std::string& conversationId, std::move(cb)); } +void +ConversationModule::editMessage(const std::string& conversationId, + const std::string& newBody, + const std::string& editedId) +{ + pimpl_->editMessage(conversationId, newBody, editedId); +} + void ConversationModule::addCallHistoryMessage(const std::string& uri, uint64_t duration_ms) { diff --git a/src/jamidht/conversation_module.h b/src/jamidht/conversation_module.h index 83f3fcd153..7dee281aaf 100644 --- a/src/jamidht/conversation_module.h +++ b/src/jamidht/conversation_module.h @@ -160,6 +160,10 @@ public: OnCommitCb&& onCommit = {}, OnDoneCb&& cb = {}); + void editMessage(const std::string& conversationId, + const std::string& newBody, + const std::string& editedId); + /** * Add to the related conversation the call history message * @param uri Peer number diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index b11d9cc19c..f88067231f 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -102,6 +102,7 @@ public: bool checkVote(const std::string& userDevice, const std::string& commitId, const std::string& parentId) const; + bool checkEdit(const std::string& userDevice, const ConversationCommit& commit) const; bool isValidUserAtCommit(const std::string& userDevice, const std::string& commitId) const; bool checkInitialCommit(const std::string& userDevice, const std::string& commitId, @@ -178,6 +179,15 @@ public: return members_; } + std::optional<ConversationCommit> getCommit(const std::string& commitId, + bool logIfNotFound = true) const + { + auto commits = log(commitId, "", 1, logIfNotFound); + if (commits.empty()) + return std::nullopt; + return std::move(commits[0]); + } + bool resolveConflicts(git_index* index, const std::string& other_id); std::vector<std::string> memberUris(std::string_view filter, @@ -862,6 +872,36 @@ ConversationRepository::Impl::checkValidUserDiff(const std::string& userDevice, return true; } +bool +ConversationRepository::Impl::checkEdit(const std::string& userDevice, + const ConversationCommit& commit) const +{ + auto userUri = uriFromDevice(userDevice); + // Check that edited commit is found, for the same author, and editable (plain/text) + auto commitMap = convCommitToMap(commit); + if (commitMap == std::nullopt) { + return false; + } + auto editedId = commitMap->at("edit"); + auto editedCommit = getCommit(editedId); + if (editedCommit == std::nullopt) { + JAMI_ERROR("Commit {:s} not found", editedId); + return false; + } + auto editedCommitMap = convCommitToMap(*editedCommit); + if (editedCommitMap == std::nullopt or editedCommitMap->at("author").empty() + or editedCommitMap->at("author") != commitMap->at("author") + or commitMap->at("author") != userUri) { + JAMI_ERROR("Edited commit {:s} got a different author ({:s})", editedId, commit.id); + return false; + } + if (editedCommitMap->at("type") != "text/plain") { + JAMI_ERROR("Edited commit {:s} is not text!", editedId); + return false; + } + return true; +} + bool ConversationRepository::Impl::checkVote(const std::string& userDevice, const std::string& commitId, @@ -2630,6 +2670,15 @@ ConversationRepository::Impl::validCommits( } return false; } + } else if (type == "application/edited-message") { + if (!checkEdit(userDevice, commit)) { + JAMI_ERROR("Commit {:s} malformed", commit.id); + if (auto shared = account_.lock()) { + emitSignal<DRing::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed edit commit"); + } + return false; + } } else { // Note: accept all mimetype here, as we can have new mimetypes // Just avoid to add weird files @@ -2970,10 +3019,7 @@ ConversationRepository::search(const Filter& filter) const std::optional<ConversationCommit> ConversationRepository::getCommit(const std::string& commitId, bool logIfNotFound) const { - auto commits = logN(commitId, 1, logIfNotFound); - if (commits.empty()) - return std::nullopt; - return std::move(commits[0]); + return pimpl_->getCommit(commitId, logIfNotFound); } std::pair<bool, std::string> diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 1f92807655..c5c7f9995a 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -116,6 +116,7 @@ private: void testConversationPreferencesMultiDevices(); void testFixContactDetails(); void testRemoveOneToOneNotInDetails(); + void testMessageEdition(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); @@ -162,6 +163,7 @@ private: CPPUNIT_TEST(testConversationPreferencesMultiDevices); CPPUNIT_TEST(testFixContactDetails); CPPUNIT_TEST(testRemoveOneToOneNotInDetails); + CPPUNIT_TEST(testMessageEdition); CPPUNIT_TEST_SUITE_END(); }; @@ -3397,6 +3399,100 @@ ConversationTest::testRemoveOneToOneNotInDetails() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationRemoved; })); } +void +ConversationTest::testMessageEdition() +{ + 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; + std::vector<std::map<std::string, std::string>> messageBobReceived; + bool conversationReady = false, memberMessageGenerated = 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.emplace_back(message); + JAMI_ERR("@@@ EMPLACE!"); + } else if (accountId == aliceId && message["type"] == "member") { + memberMessageGenerated = true; + } + cv.notify_one(); + })); + bool requestReceived = false; + confHandlers.insert( + DRing::exportable_callback<DRing::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == bobId) { + conversationReady = true; + cv.notify_one(); + } + })); + auto errorDetected = false; + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + auto convId = DRing::startConversation(aliceId); + DRing::addConversationMember(aliceId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); + memberMessageGenerated = false; + DRing::acceptConversationRequest(bobId, convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&]() { return conversationReady && memberMessageGenerated; })); + auto msgSize = messageBobReceived.size(); + JAMI_ERR("@@@@@@@@ %u", msgSize); + DRing::sendMessage(aliceId, convId, "hi"s, ""); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messageBobReceived.size() == msgSize + 1; })); + msgSize = messageBobReceived.size(); + auto editedId = messageBobReceived.rbegin()->at("id"); + DRing::sendMessage(aliceId, convId, "New body"s, editedId, 1); + CPPUNIT_ASSERT(cv.wait_for(lk, 10s, [&]() { return messageBobReceived.size() == msgSize + 1; })); + CPPUNIT_ASSERT(messageBobReceived.rbegin()->at("edit") == editedId); + CPPUNIT_ASSERT(messageBobReceived.rbegin()->at("body") == "New body"); + // Not an existing message + msgSize = messageBobReceived.size(); + DRing::sendMessage(aliceId, convId, "New body"s, "invalidId", 1); + CPPUNIT_ASSERT( + !cv.wait_for(lk, 10s, [&]() { return messageBobReceived.size() == msgSize + 1; })); + // Invalid author + DRing::sendMessage(aliceId, convId, "New body"s, convId, 1); + CPPUNIT_ASSERT( + !cv.wait_for(lk, 10s, [&]() { return messageBobReceived.size() == msgSize + 1; })); + // Add invalid edition + Json::Value root; + root["type"] = "application/edited-message"; + root["edit"] = convId; + root["body"] = "new"; + Json::StreamWriterBuilder wbuilder; + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + auto repoPath = fileutils::get_data_dir() + DIR_SEPARATOR_STR + aliceAccount->getAccountID() + + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + convId; + auto message = Json::writeString(wbuilder, root); + commitInRepo(repoPath, aliceAccount, message); + errorDetected = false; + DRing::sendMessage(aliceId, convId, "trigger"s, ""); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return errorDetected; })); +} + } // namespace test } // namespace jami -- GitLab