diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 92872d147..9b3dd7760 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -1720,6 +1720,28 @@ + + + + Update conversation's preferences (synced across devices) such as color, notifications, etc. + + + + + + + + + + + Get conversation's preferences + + + + + + + @@ -2119,6 +2141,29 @@ + + + + Notify clients when preferences for a conversation are updated + + + + Account id related + + + + + Conversation id + + + + + + New preferences + + + + diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index 1f822c24c..5170d948d 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -317,6 +317,8 @@ DBusClient::initLibrary(int flags) bind(&DBusConfigurationManager::conversationMemberEvent, confM, _1, _2, _3, _4)), exportable_callback( bind(&DBusConfigurationManager::onConversationError, confM, _1, _2, _3, _4)), + exportable_callback( + bind(&DBusConfigurationManager::conversationPreferencesUpdated, confM, _1, _2, _3)), }; #ifdef ENABLE_VIDEO diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 3ebf647c7..3effef201 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -879,6 +879,21 @@ DBusConfigurationManager::conversationInfos(const std::string& accountId, return DRing::conversationInfos(accountId, conversationId); } +void +DBusConfigurationManager::setConversationPreferences(const std::string& accountId, + const std::string& conversationId, + const std::map& infos) +{ + DRing::setConversationPreferences(accountId, conversationId, infos); +} + +std::map +DBusConfigurationManager::getConversationPreferences(const std::string& accountId, + const std::string& conversationId) +{ + return DRing::getConversationPreferences(accountId, conversationId); +} + void DBusConfigurationManager::addConversationMember(const std::string& accountId, const std::string& conversationId, diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index 1316b9a72..b14548730 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -265,6 +265,11 @@ public: const std::map& infos); std::map conversationInfos(const std::string& accountId, const std::string& conversationId); + void setConversationPreferences(const std::string& accountId, + const std::string& conversationId, + const std::map& prefs); + std::map getConversationPreferences(const std::string& accountId, + const std::string& conversationId); void addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index cfd130f80..4437faaf3 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -35,6 +35,7 @@ public: virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} + virtual void conversationPreferencesUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map /* preferences */){} }; %} @@ -51,6 +52,8 @@ namespace DRing { std::vector> getConversationRequests(const std::string& accountId); void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map& infos); std::map conversationInfos(const std::string& accountId, const std::string& conversationId); + void setConversationPreferences(const std::string& accountId, const std::string& conversationId, const std::map& prefs); + std::map getConversationPreferences(const std::string& accountId, const std::string& conversationId); // Member management void addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); @@ -86,4 +89,5 @@ public: virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} + virtual void conversationPreferencesUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map /* preferences */){} }; \ No newline at end of file diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index b0dcb92fc..120ec30a5 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -328,7 +328,8 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM exportable_callback(bind(&ConversationCallback::conversationReady, convM, _1, _2)), exportable_callback(bind(&ConversationCallback::conversationRemoved, convM, _1, _2)), exportable_callback(bind(&ConversationCallback::conversationMemberEvent, convM, _1, _2, _3, _4)), - exportable_callback(bind(&ConversationCallback::onConversationError, convM, _1, _2, _3, _4)) + exportable_callback(bind(&ConversationCallback::onConversationError, convM, _1, _2, _3, _4)), + exportable_callback(bind(&ConversationCallback::conversationPreferencesUpdated, convM, _1, _2, _3)) }; if (!DRing::init(static_cast(DRing::DRING_FLAG_DEBUG))) diff --git a/bin/nodejs/callback.h b/bin/nodejs/callback.h index cb1c5d759..5feaf731f 100644 --- a/bin/nodejs/callback.h +++ b/bin/nodejs/callback.h @@ -42,6 +42,7 @@ Persistent conferenceCreatedCb; Persistent conferenceChangedCb; Persistent conferenceRemovedCb; Persistent onConferenceInfosUpdatedCb; +Persistent conversationPreferencesUpdatedCb; std::queue> pendingSignals; std::mutex pendingSignalsLock; @@ -117,6 +118,8 @@ getPresistentCb(std::string_view signal) return &conferenceRemovedCb; else if (signal == "OnConferenceInfosUpdated") return &onConferenceInfosUpdatedCb; + else if (signal == "ConversationPreferencesUpdated") + return &conversationPreferencesUpdatedCb; else return nullptr; } @@ -131,7 +134,7 @@ getPresistentCb(std::string_view signal) inline std::string_view toView(const String::Utf8Value& utf8) { - return {*utf8, (size_t)utf8.length()}; + return {*utf8, (size_t) utf8.length()}; } inline SWIGV8_ARRAY @@ -237,8 +240,7 @@ composingStatusChanged(const std::string& accountId, { std::lock_guard lock(pendingSignalsLock); pendingSignals.emplace([accountId, conversationId, from, state]() { - Local func = Local::New(Isolate::GetCurrent(), - composingStatusChangedCb); + Local func = Local::New(Isolate::GetCurrent(), composingStatusChangedCb); if (!func.IsEmpty()) { SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId), V8_STRING_NEW_LOCAL(conversationId), @@ -592,7 +594,10 @@ conversationLoaded(uint32_t id, } void -messagesFound(uint32_t id, const std::string& accountId, const std::string& conversationId, const std::vector>& messages) +messagesFound(uint32_t id, + const std::string& accountId, + const std::string& conversationId, + const std::vector>& messages) { std::lock_guard lock(pendingSignalsLock); pendingSignals.emplace([id, accountId, conversationId, messages]() { @@ -628,12 +633,13 @@ messageReceived(const std::string& accountId, void conversationProfileUpdated(const std::string& accountId, - const std::string& conversationId , - const std::map& profile) + const std::string& conversationId, + const std::map& profile) { std::lock_guard lock(pendingSignalsLock); pendingSignals.emplace([accountId, conversationId, profile]() { - Local func = Local::New(Isolate::GetCurrent(), conversationProfileUpdatedCb); + Local func = Local::New(Isolate::GetCurrent(), + conversationProfileUpdatedCb); if (!func.IsEmpty()) { SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId), V8_STRING_NEW_LOCAL(conversationId), @@ -818,3 +824,22 @@ onConferenceInfosUpdated(const std::string& accountId, }); uv_async_send(&signalAsync); } + +void +conversationPreferencesUpdated(const std::string& accountId, + const std::string& convId, + const std::map& preferences) +{ + std::lock_guard lock(pendingSignalsLock); + pendingSignals.emplace([accountId, convId, preferences]() { + Local func = Local::New(Isolate::GetCurrent(), + conversationPreferencesUpdatedCb); + if (!func.IsEmpty()) { + SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(accountId), + V8_STRING_NEW_LOCAL(convId), + stringMapToJsMap(preferences)}; + func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 3, callback_args); + } + }); + uv_async_send(&signalAsync); +} diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index 8ae3b712f..f645253f6 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -35,6 +35,7 @@ public: virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} + virtual void conversationPreferencesUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map /* preferences */){} }; %} @@ -51,6 +52,8 @@ namespace DRing { std::vector> getConversationRequests(const std::string& accountId); void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map& infos); std::map conversationInfos(const std::string& accountId, const std::string& conversationId); + void setConversationPreferences(const std::string& accountId, const std::string& conversationId, const std::map& prefs); + std::map getConversationPreferences(const std::string& accountId, const std::string& conversationId); // Member management void addConversationMember(const std::string& accountId, const std::string& conversationId, const std::string& contactUri); @@ -87,4 +90,5 @@ public: virtual void conversationRemoved(const std::string& /*accountId*/, const std::string& /* conversationId */){} virtual void conversationMemberEvent(const std::string& /*accountId*/, const std::string& /* conversationId */, const std::string& /* memberUri */, int /* event */){} virtual void onConversationError(const std::string& /*accountId*/, const std::string& /* conversationId */, uint32_t /* code */, const std::string& /* what */){} + virtual void conversationPreferencesUpdated(const std::string& /*accountId*/, const std::string& /* conversationId */, std::map /* preferences */){} }; \ No newline at end of file diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i index bbc909576..8da4ace3f 100644 --- a/bin/nodejs/nodejs_interface.i +++ b/bin/nodejs/nodejs_interface.i @@ -155,7 +155,8 @@ void init(const SWIGV8_VALUE& funcMap){ exportable_callback(bind(&conversationReady, _1, _2)), exportable_callback(bind(&conversationRemoved, _1, _2)), exportable_callback(bind(&conversationMemberEvent, _1, _2, _3, _4)), - exportable_callback(bind(&onConversationError, _1, _2, _3, _4)) + exportable_callback(bind(&onConversationError, _1, _2, _3, _4)), + exportable_callback(bind(&ConversationCallback::conversationPreferencesUpdated, convM, _1, _2, _3)) }; if (!DRing::init(static_cast(DRing::DRING_FLAG_DEBUG))) diff --git a/configure.ac b/configure.ac index 80e73d235..3b287c9ed 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.4.0],[jami@gnu.org],[jami]) +AC_INIT([Jami Daemon],[13.5.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 65f560514..bd14e1b37 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jami-daemon', ['c', 'cpp'], - version: '13.4.0', + version: '13.5.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 67a4a6404..47607790a 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -106,6 +106,25 @@ conversationInfos(const std::string& accountId, const std::string& conversationI return {}; } +void +setConversationPreferences(const std::string& accountId, + const std::string& conversationId, + const std::map& prefs) +{ + if (auto acc = jami::Manager::instance().getAccount(accountId)) + if (auto convModule = acc->convModule()) + convModule->setConversationPreferences(conversationId, prefs); +} + +std::map +getConversationPreferences(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount(accountId)) + if (auto convModule = acc->convModule()) + return convModule->getConversationPreferences(conversationId); + return {}; +} + // Member management void addConversationMember(const std::string& accountId, diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index 53fd6858e..274a3e73a 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -138,6 +138,7 @@ getSignalHandlers() exported_callback(), exported_callback(), exported_callback(), + exported_callback(), #ifdef ENABLE_PLUGIN exported_callback(), diff --git a/src/fileutils.cpp b/src/fileutils.cpp index 324e26b2d..aa7ff713b 100644 --- a/src/fileutils.cpp +++ b/src/fileutils.cpp @@ -1119,5 +1119,20 @@ accessFile(const std::string& file, int mode) #endif } +uint64_t +lastWriteTime(const std::string& p) +{ +#if USE_STD_FILESYSTEM + return std::chrono::duration_cast( + std::filesystem::last_write_time(std::filesystem::path(p)).time_since_epoch()) + .count(); +#else + struct stat result; + if (stat(p.c_str(), &result) == 0) + return result.st_mtime; + return 0; +#endif +} + } // namespace fileutils } // namespace jami diff --git a/src/fileutils.h b/src/fileutils.h index 3b798758b..baad2dda8 100644 --- a/src/fileutils.h +++ b/src/fileutils.h @@ -155,5 +155,7 @@ std::string sha3sum(const std::vector& buffer); */ int accessFile(const std::string& file, int mode); +uint64_t lastWriteTime(const std::string& p); + } // namespace fileutils } // namespace jami diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h index bc146e8ba..bb05c1920 100644 --- a/src/jami/conversation_interface.h +++ b/src/jami/conversation_interface.h @@ -49,6 +49,11 @@ DRING_PUBLIC void updateConversationInfos(const std::string& accountId, const std::map& infos); DRING_PUBLIC std::map conversationInfos(const std::string& accountId, const std::string& conversationId); +DRING_PUBLIC void setConversationPreferences(const std::string& accountId, + const std::string& conversationId, + const std::map& prefs); +DRING_PUBLIC std::map getConversationPreferences( + const std::string& accountId, const std::string& conversationId); // Member management DRING_PUBLIC void addConversationMember(const std::string& accountId, @@ -176,6 +181,15 @@ struct DRING_PUBLIC ConversationSignal int code, const std::string& what); }; + + // Preferences + struct DRING_PUBLIC ConversationPreferencesUpdated + { + constexpr static const char* name = "ConversationPreferencesUpdated"; + using cb_type = void(const std::string& /*accountId*/, + const std::string& /*conversationId*/, + std::map /*preferences*/); + }; }; } // namespace DRing diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 0f6c193ac..bf281b4e2 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -38,6 +38,8 @@ namespace jami { +static const char* const LAST_MODIFIED = "lastModified"; + ConvInfo::ConvInfo(const Json::Value& json) { id = json[ConversationMapKeys::ID].asString(); @@ -157,6 +159,7 @@ public: void init() { if (auto shared = account_.lock()) { + accountId_ = shared->getAccountID(); transferManager_ = std::make_shared(shared->getAccountID(), repository_->id()); conversationDataPath_ = fileutils::get_data_dir() + DIR_SEPARATOR_STR @@ -166,6 +169,8 @@ public: sendingPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + "sending"; lastDisplayedPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + ConversationMapKeys::LAST_DISPLAYED; + preferencesPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + + ConversationMapKeys::PREFERENCES; loadFetched(); loadSending(); loadLastDisplayed(); @@ -202,9 +207,8 @@ public: void announce(const std::vector>& commits) const { auto shared = account_.lock(); - if (!shared or !repository_) { + if (!shared or !repository_) return; - } auto convId = repository_->id(); auto ok = !commits.empty(); auto lastId = ok ? commits.rbegin()->at(ConversationMapKeys::ID) : ""; @@ -229,8 +233,10 @@ public: action = 4; if (action != -1) { announceMember = true; - emitSignal( - shared->getAccountID(), convId, uri, action); + emitSignal(accountId_, + convId, + uri, + action); } } } @@ -238,7 +244,7 @@ public: auto& pluginChatManager = Manager::instance().getJamiPluginManager().getChatServicesManager(); if (pluginChatManager.hasHandlers()) { - auto cm = std::make_shared(shared->getAccountID(), + auto cm = std::make_shared(accountId_, convId, c.at("author") != shared->getUsername(), c, @@ -248,9 +254,7 @@ public: } #endif // announce message - emitSignal(shared->getAccountID(), - convId, - c); + emitSignal(accountId_, convId, c); // check if we should update lastDisplayed // ignore merge commits as it's not generated by the user if (c.at("type") == "merge") @@ -348,9 +352,6 @@ public: std::string bannedType(const std::string& uri) const { - auto shared = account_.lock(); - if (!shared) - return {}; auto bannedMember = fmt::format("{}/banned/members/{}.crt", repoPath(), uri); if (fileutils::isFile(bannedMember)) return "members"; @@ -386,7 +387,10 @@ public: // Manage last message displayed and status std::string sendingPath_ {}; std::vector sending_ {}; + // Manage last message displayed + std::string accountId_ {}; std::string lastDisplayedPath_ {}; + std::string preferencesPath_ {}; mutable std::mutex lastDisplayedMtx_ {}; // for lastDisplayed_ mutable std::map lastDisplayed_ {}; std::function lastDisplayedUpdatedCb_ {}; @@ -410,11 +414,8 @@ Conversation::Impl::isAdmin() const std::string Conversation::Impl::repoPath() const { - auto shared = account_.lock(); - if (!shared) - return {}; - return fileutils::get_data_dir() + DIR_SEPARATOR_STR + shared->getAccountID() - + DIR_SEPARATOR_STR + "conversations" + DIR_SEPARATOR_STR + repository_->id(); + return fileutils::get_data_dir() + DIR_SEPARATOR_STR + accountId_ + DIR_SEPARATOR_STR + + "conversations" + DIR_SEPARATOR_STR + repository_->id(); } std::vector> @@ -655,19 +656,12 @@ Conversation::memberUris(std::string_view filter, const std::set& fi std::string Conversation::join() { - auto shared = pimpl_->account_.lock(); - if (!shared) - return {}; return pimpl_->repository_->join(); } bool Conversation::isMember(const std::string& uri, bool includeInvited) const { - auto shared = pimpl_->account_.lock(); - if (!shared) - return false; - auto invitedPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "invited"; auto adminsPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "admins"; auto membersPath = pimpl_->repoPath() + DIR_SEPARATOR_STR + "members"; @@ -729,8 +723,8 @@ Conversation::sendMessage(Json::Value&& value, const std::string& replyTo, OnDon } dht::ThreadPool::io().run([w = weak(), value = std::move(value), cb = std::move(cb)] { if (auto sthis = w.lock()) { - auto shared = sthis->pimpl_->account_.lock(); - if (!shared) + auto acc = sthis->pimpl_->account_.lock(); + if (!acc) return; std::unique_lock lk(sthis->pimpl_->writeMtx_); Json::StreamWriterBuilder wbuilder; @@ -744,9 +738,9 @@ Conversation::sendMessage(Json::Value&& value, const std::string& replyTo, OnDon lk.unlock(); sthis->pimpl_->announce(commit); emitSignal( - shared->getAccountID(), + acc->getAccountID(), sthis->id(), - shared->getUsername(), + acc->getUsername(), commit, static_cast(DRing::Account::MessageStates::SENDING)); if (cb) @@ -760,9 +754,6 @@ Conversation::sendMessages(std::vector&& messages, OnMultiDoneCb&& { dht::ThreadPool::io().run([w = weak(), messages = std::move(messages), cb = std::move(cb)] { if (auto sthis = w.lock()) { - auto shared = sthis->pimpl_->account_.lock(); - if (!shared) - return; std::vector commits; commits.reserve(messages.size()); Json::StreamWriterBuilder wbuilder; @@ -1095,6 +1086,49 @@ Conversation::infos() const return pimpl_->repository_->infos(); } +void +Conversation::updatePreferences(const std::map& map) +{ + auto filePath = fmt::format("{}/preferences", pimpl_->conversationDataPath_); + auto prefs = map; + auto itLast = prefs.find(LAST_MODIFIED); + if (itLast != prefs.end()) { + if (fileutils::isFile(filePath)) { + auto lastModified = fileutils::lastWriteTime(filePath); + try { + if (lastModified >= std::stoul(itLast->second)) + return; + } catch (...) { + return; + } + } + prefs.erase(itLast); + } + + std::ofstream file(filePath, std::ios::trunc | std::ios::binary); + msgpack::pack(file, prefs); + emitSignal(pimpl_->accountId_, + id(), + std::move(prefs)); +} + +std::map +Conversation::preferences(bool includeLastModified) const +{ + try { + std::map preferences; + auto filePath = fmt::format("{}/preferences", pimpl_->conversationDataPath_); + auto file = fileutils::loadFile(filePath); + msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); + oh.get().convert(preferences); + if (includeLastModified) + preferences[LAST_MODIFIED] = fmt::format("{}", fileutils::lastWriteTime(filePath)); + return preferences; + } catch (const std::exception& e) { + } + return {}; +} + std::vector Conversation::vCard() const { @@ -1119,10 +1153,6 @@ Conversation::onFileChannelRequest(const std::string& member, if (!isMember(member)) return false; - auto account = pimpl_->account_.lock(); - if (!account) - return false; - auto sep = fileId.find('_'); if (sep == std::string::npos) return false; @@ -1142,7 +1172,7 @@ Conversation::onFileChannelRequest(const std::string& member, fileutils::remove(path, true); } JAMI_DBG("[Account %s] %s asked for non existing file %s in %s", - account->getAccountID().c_str(), + pimpl_->accountId_.c_str(), member.c_str(), fileId.c_str(), id().c_str()); @@ -1151,7 +1181,7 @@ Conversation::onFileChannelRequest(const std::string& member, // Check that our file is correct before sending if (verifyShaSum && commit->at("sha3sum") != fileutils::sha3File(path)) { JAMI_DBG("[Account %s] %s asked for file %s in %s, but our version is not complete", - account->getAccountID().c_str(), + pimpl_->accountId_.c_str(), member.c_str(), fileId.c_str(), id().c_str()); @@ -1285,7 +1315,7 @@ Conversation::updateLastDisplayed(const std::string& lastDisplayed) pimpl_->saveLastDisplayed(); lk.unlock(); if (pimpl_->lastDisplayedUpdatedCb_) - pimpl_->lastDisplayedUpdatedCb_(pimpl_->repository_->id(), lastDisplayed); + pimpl_->lastDisplayedUpdatedCb_(id(), lastDisplayed); }; auto hasCommit = pimpl_->repository_->getCommit(lastDisplayed, false) != std::nullopt; diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 01d4e1488..0c6003f07 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -40,6 +40,7 @@ static constexpr const char* REMOVED = "removed"; static constexpr const char* ERASED = "erased"; static constexpr const char* MEMBERS = "members"; static constexpr const char* LAST_DISPLAYED = "lastDisplayed"; +static constexpr const char* PREFERENCES = "preferences"; static constexpr const char* CACHED = "cached"; static constexpr const char* RECEIVED = "received"; static constexpr const char* DECLINED = "declined"; @@ -288,11 +289,23 @@ public: */ void updateInfos(const std::map& map, const OnDoneCb& cb = {}); + /** + * Change user's preferences + * @param map New preferences + */ + void updatePreferences(const std::map& map); + /** * Retrieve current infos (title, description, avatar, mode) * @return infos */ std::map infos() const; + /** + * Retrieve current preferences (color, notification, etc) + * @param includeLastModified If we want to know when the preferences were modified + * @return preferences + */ + std::map preferences(bool includeLastModified) const; std::vector vCard() const; /////// File transfer diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index f06459686..382d5b75a 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -557,7 +557,7 @@ ConversationModule::Impl::handlePendingConversation(const std::string& conversat sendMessageNotification(conversationId, commitId, false); // Inform user that the conversation is ready emitSignal(accountId_, conversationId); - needsSyncingCb_(); + needsSyncingCb_({}); std::vector values; values.reserve(messages.size()); for (const auto& message : messages) { @@ -696,7 +696,7 @@ ConversationModule::Impl::removeRepository(const std::string& conversationId, bo auto convIt = convInfos_.find(conversationId); if (convIt != convInfos_.end()) { convIt->second.erased = std::time(nullptr); - needsSyncingCb_(); + needsSyncingCb_({}); } saveConvInfos(); } @@ -722,7 +722,7 @@ ConversationModule::Impl::removeConversation(const std::string& conversationId) itConv->second.erased = std::time(nullptr); // Sync now, because it can take some time to really removes the datas if (hasMembers) - needsSyncingCb_(); + needsSyncingCb_({}); saveConvInfos(); lockCi.unlock(); emitSignal(accountId_, conversationId); @@ -1124,7 +1124,7 @@ ConversationModule::declineConversationRequest(const std::string& conversationId } emitSignal(pimpl_->accountId_, conversationId); - pimpl_->needsSyncingCb_(); + pimpl_->needsSyncingCb_({}); } std::string @@ -1157,7 +1157,7 @@ ConversationModule::startConversation(ConversationMode mode, const std::string& info.members.emplace_back(otherMember); addConvInfo(info); - pimpl_->needsSyncingCb_(); + pimpl_->needsSyncingCb_({}); emitSignal(pimpl_->accountId_, convId); return convId; @@ -1464,6 +1464,14 @@ ConversationModule::onSyncData(const SyncMsg& msg, convId, req.toMap()); } + + // Updates preferences for conversations + std::lock_guard lk(pimpl_->conversationsMtx_); + for (const auto& [convId, p] : msg.p) { + auto itConv = pimpl_->conversations_.find(convId); + if (itConv != pimpl_->conversations_.end() && itConv->second) + itConv->second->updatePreferences(p); + } } bool @@ -1648,7 +1656,6 @@ ConversationModule::updateConversationInfos(const std::string& conversationId, bool sync) { std::lock_guard lk(pimpl_->conversationsMtx_); - // Add a new member in the conversation auto it = pimpl_->conversations_.find(conversationId); if (it == pimpl_->conversations_.end()) { JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); @@ -1674,7 +1681,6 @@ ConversationModule::conversationInfos(const std::string& conversationId) const return itReq->second.metadatas; } std::lock_guard lk(pimpl_->conversationsMtx_); - // Add a new member in the conversation auto it = pimpl_->conversations_.find(conversationId); if (it == pimpl_->conversations_.end() or not it->second) { std::lock_guard lkCi(pimpl_->convInfosMtx_); @@ -1689,6 +1695,52 @@ ConversationModule::conversationInfos(const std::string& conversationId) const return it->second->infos(); } +void +ConversationModule::setConversationPreferences(const std::string& conversationId, + const std::map& prefs) +{ + std::unique_lock lk(pimpl_->conversationsMtx_); + auto it = pimpl_->conversations_.find(conversationId); + if (it == pimpl_->conversations_.end()) { + JAMI_ERR("Conversation %s doesn't exist", conversationId.c_str()); + return; + } + + it->second->updatePreferences(prefs); + auto msg = std::make_shared(); + std::map> p; + p[conversationId] = it->second->preferences(true); + msg->p = std::move(p); + lk.unlock(); + pimpl_->needsSyncingCb_(std::move(msg)); +} + +std::map +ConversationModule::getConversationPreferences(const std::string& conversationId) const +{ + std::lock_guard lk(pimpl_->conversationsMtx_); + auto it = pimpl_->conversations_.find(conversationId); + if (it == pimpl_->conversations_.end() or not it->second) + return {}; + + return it->second->preferences(false); +} + +std::map> +ConversationModule::getAllConversationsPreferences() const +{ + std::map> p; + std::lock_guard lk(pimpl_->conversationsMtx_); + for (const auto& [id, conv] : pimpl_->conversations_) { + if (conv) { + auto prefs = conv->preferences(true); + if (!prefs.empty()) + p[id] = std::move(prefs); + } + } + return p; +} + std::vector ConversationModule::conversationVCard(const std::string& conversationId) const { @@ -1702,7 +1754,6 @@ ConversationModule::conversationVCard(const std::string& conversationId) const return it->second->vCard(); } - bool ConversationModule::isBannedDevice(const std::string& convId, const std::string& deviceId) const { diff --git a/src/jamidht/conversation_module.h b/src/jamidht/conversation_module.h index 0875367b2..20c4c803e 100644 --- a/src/jamidht/conversation_module.h +++ b/src/jamidht/conversation_module.h @@ -35,14 +35,17 @@ struct SyncMsg jami::DeviceSync ds; std::map c; std::map cr; - MSGPACK_DEFINE(ds, c, cr) + // p is conversation's preferences. It's not stored in c, as + // we can update the preferences without touching any confInfo. + std::map> p; + MSGPACK_DEFINE(ds, c, cr, p) }; using ChannelCb = std::function&)>; using NeedSocketCb = std::function; using SengMsgCb = std::function&&, uint64_t)>; -using NeedsSyncingCb = std::function; +using NeedsSyncingCb = std::function&&)>; using UpdateConvReq = std::function; class ConversationModule @@ -309,7 +312,7 @@ public: // Conversation's infos management /** - * Update metadata from conversations (like title, avatar, etc) + * Update metadatas from conversations (like title, avatar, etc) * @param conversationId * @param infos * @param sync If we need to sync with others (used for tests) @@ -318,6 +321,19 @@ public: const std::map& infos, bool sync = true); std::map conversationInfos(const std::string& conversationId) const; + /** + * Update user's preferences (like color, notifications, etc) to be synced across devices + * @param conversationId + * @param preferences + */ + void setConversationPreferences(const std::string& conversationId, + const std::map& prefs); + std::map getConversationPreferences( + const std::string& conversationId) const; + /** + * Retrieve all conversation preferences to sync with other devices + */ + std::map> getAllConversationsPreferences() const; // Get the map into a VCard format for storing std::vector conversationVCard(const std::string& conversationId) const; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 24061b8e4..66b3f37e8 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -2337,10 +2337,10 @@ JamiAccount::convModule() if (!convModule_) { convModule_ = std::make_unique( weak(), - [this] { - runOnMainThread([w = weak()] { + [this](auto&& syncMsg) { + runOnMainThread([w = weak(), syncMsg] { if (auto shared = w.lock()) - shared->syncModule()->syncWithConnected(); + shared->syncModule()->syncWithConnected(syncMsg); }); }, [this](auto&& uri, auto&& msg, auto token = 0) { diff --git a/src/jamidht/sync_module.cpp b/src/jamidht/sync_module.cpp index 61dbce5af..348a1d629 100644 --- a/src/jamidht/sync_module.cpp +++ b/src/jamidht/sync_module.cpp @@ -43,7 +43,8 @@ public: * Build SyncMsg and send it on socket * @param socket */ - void syncInfos(const std::shared_ptr& socket); + void syncInfos(const std::shared_ptr& socket, + const std::shared_ptr& syncMsg); }; SyncModule::Impl::Impl(std::weak_ptr&& account) @@ -51,7 +52,8 @@ SyncModule::Impl::Impl(std::weak_ptr&& account) {} void -SyncModule::Impl::syncInfos(const std::shared_ptr& socket) +SyncModule::Impl::syncInfos(const std::shared_ptr& socket, + const std::shared_ptr& syncMsg) { auto acc = account_.lock(); if (!acc) @@ -59,13 +61,30 @@ SyncModule::Impl::syncInfos(const std::shared_ptr& socket) Json::Value syncValue; msgpack::sbuffer buffer(UINT16_MAX); // Use max pkt size std::error_code ec; - // Send contacts infos - // This message can be big. TODO rewrite to only take UINT16_MAX bytes max or split it multiple - // messages. For now, write 3 messages (UINT16_MAX*3 should be enough for all informations). - if (auto info = acc->accountManager()->getInfo()) { - if (info->contacts) { + if (!syncMsg) { + // Send contacts infos + // This message can be big. TODO rewrite to only take UINT16_MAX bytes max or split it multiple + // messages. For now, write 3 messages (UINT16_MAX*3 should be enough for all informations). + if (auto info = acc->accountManager()->getInfo()) { + if (info->contacts) { + SyncMsg msg; + msg.ds = info->contacts->getSyncData(); + msgpack::pack(buffer, msg); + socket->write(reinterpret_cast(buffer.data()), + buffer.size(), + ec); + if (ec) { + JAMI_ERR("%s", ec.message().c_str()); + return; + } + } + } + buffer.clear(); + // Sync conversations + auto c = ConversationModule::convInfos(acc->getAccountID()); + if (!c.empty()) { SyncMsg msg; - msg.ds = info->contacts->getSyncData(); + msg.c = std::move(c); msgpack::pack(buffer, msg); socket->write(reinterpret_cast(buffer.data()), buffer.size(), ec); if (ec) { @@ -73,32 +92,24 @@ SyncModule::Impl::syncInfos(const std::shared_ptr& socket) return; } } - } - buffer.clear(); - // Sync conversations - auto c = ConversationModule::convInfos(acc->getAccountID()); - if (!c.empty()) { - SyncMsg msg; - msg.c = std::move(c); - msgpack::pack(buffer, msg); - socket->write(reinterpret_cast(buffer.data()), buffer.size(), ec); - if (ec) { - JAMI_ERR("%s", ec.message().c_str()); - return; + buffer.clear(); + // Sync requests + auto cr = ConversationModule::convRequests(acc->getAccountID()); + if (!cr.empty()) { + SyncMsg msg; + msg.cr = std::move(cr); + msgpack::pack(buffer, msg); + socket->write(reinterpret_cast(buffer.data()), buffer.size(), ec); + if (ec) { + JAMI_ERR("%s", ec.message().c_str()); + return; + } } - } - buffer.clear(); - // Sync requests - auto cr = ConversationModule::convRequests(acc->getAccountID()); - if (!cr.empty()) { - SyncMsg msg; - msg.cr = std::move(cr); - msgpack::pack(buffer, msg); + } else { + msgpack::pack(buffer, *syncMsg); socket->write(reinterpret_cast(buffer.data()), buffer.size(), ec); - if (ec) { + if (ec) JAMI_ERR("%s", ec.message().c_str()); - return; - } } } @@ -149,14 +160,16 @@ SyncModule::cacheSyncConnection(std::shared_ptr&& socket, if (auto manager = dynamic_cast(acc->accountManager())) manager->onSyncData(std::move(msg.ds), false); - if (!msg.c.empty() || !msg.cr.empty()) + if (!msg.c.empty() || !msg.cr.empty() || !msg.p.empty()) acc->convModule()->onSyncData(msg, peerId, device.toString()); return len; }); } void -SyncModule::syncWith(const DeviceId& deviceId, const std::shared_ptr& socket) +SyncModule::syncWith(const DeviceId& deviceId, + const std::shared_ptr& socket, + const std::shared_ptr& syncMsg) { if (!socket) return; @@ -182,16 +195,16 @@ SyncModule::syncWith(const DeviceId& deviceId, const std::shared_ptrsyncConnections_[deviceId].emplace_back(socket); } - pimpl_->syncInfos(socket); + pimpl_->syncInfos(socket, syncMsg); } void -SyncModule::syncWithConnected() +SyncModule::syncWithConnected(const std::shared_ptr& syncMsg) { std::lock_guard lk(pimpl_->syncConnectionsMtx_); for (auto& [_deviceId, sockets] : pimpl_->syncConnections_) { if (not sockets.empty()) - pimpl_->syncInfos(sockets[0]); + pimpl_->syncInfos(sockets[0], syncMsg); } } } // namespace jami \ No newline at end of file diff --git a/src/jamidht/sync_module.h b/src/jamidht/sync_module.h index 21d774fb3..bde491537 100644 --- a/src/jamidht/sync_module.h +++ b/src/jamidht/sync_module.h @@ -44,13 +44,17 @@ public: * Send sync informations to connected device * @param deviceId Connected device * @param socket Related socket + * @param syncMsg Default message */ - void syncWith(const DeviceId& deviceId, const std::shared_ptr& socket); + void syncWith(const DeviceId& deviceId, + const std::shared_ptr& socket, + const std::shared_ptr& syncMsg = nullptr); /** * Send sync to all connected devices + * @param syncMsg Default message */ - void syncWithConnected(); + void syncWithConnected(const std::shared_ptr& syncMsg = nullptr); private: class Impl; diff --git a/test/agent/src/bindings/signal.cpp b/test/agent/src/bindings/signal.cpp index d71bb0efc..8b509ec46 100644 --- a/test/agent/src/bindings/signal.cpp +++ b/test/agent/src/bindings/signal.cpp @@ -540,5 +540,10 @@ install_signal_primitives(void*) int, const std::string&>(handlers, "conversation-error"); + add_handler>(handlers, "conversation-preferences-updated"); + DRing::registerSignalHandlers(handlers); } diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index 9ced2bdb6..4be659480 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -112,6 +112,8 @@ private: void testRemoveReaddMultipleDevice(); void testSendReply(); void testSearchInConv(); + void testConversationPreferences(); + void testConversationPreferencesMultiDevices(); CPPUNIT_TEST_SUITE(ConversationTest); CPPUNIT_TEST(testCreateConversation); @@ -154,6 +156,8 @@ private: CPPUNIT_TEST(testRemoveReaddMultipleDevice); CPPUNIT_TEST(testSendReply); CPPUNIT_TEST(testSearchInConv); + CPPUNIT_TEST(testConversationPreferences); + CPPUNIT_TEST(testConversationPreferencesMultiDevices); CPPUNIT_TEST_SUITE_END(); }; @@ -3175,6 +3179,140 @@ ConversationTest::testSearchInConv() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return messages.size() == 0 && finished; })); } +void +ConversationTest::testConversationPreferences() +{ + auto aliceAccount = Manager::instance().getAccount(aliceId); + auto uri = aliceAccount->getUsername(); + std::mutex mtx; + std::unique_lock lk {mtx}; + std::condition_variable cv; + std::map> confHandlers; + bool conversationReady = false, conversationRemoved = false; + confHandlers.insert(DRing::exportable_callback( + [&](const std::string& accountId, const std::string& /* conversationId */) { + if (accountId == aliceId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback( + [&](const std::string& accountId, const std::string&) { + if (accountId == aliceId) + conversationRemoved = true; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + // Start conversation and set preferences + auto convId = DRing::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return conversationReady; }); + CPPUNIT_ASSERT(DRing::getConversationPreferences(aliceId, convId).size() == 0); + DRing::setConversationPreferences(aliceId, convId, {{"foo", "bar"}}); + auto preferences = DRing::getConversationPreferences(aliceId, convId); + CPPUNIT_ASSERT(preferences.size() == 1); + CPPUNIT_ASSERT(preferences["foo"] == "bar"); + // Update + DRing::setConversationPreferences(aliceId, convId, {{"foo", "bar2"}, {"bar", "foo"}}); + preferences = DRing::getConversationPreferences(aliceId, convId); + CPPUNIT_ASSERT(preferences.size() == 2); + CPPUNIT_ASSERT(preferences["foo"] == "bar2"); + CPPUNIT_ASSERT(preferences["bar"] == "foo"); + // Remove conversations removes its preferences. + CPPUNIT_ASSERT(DRing::removeConversation(aliceId, convId)); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return conversationRemoved; })); + CPPUNIT_ASSERT(DRing::getConversationPreferences(aliceId, convId).size() == 0); +} +void +ConversationTest::testConversationPreferencesMultiDevices() +{ + auto aliceAccount = Manager::instance().getAccount(aliceId); + auto bobAccount = Manager::instance().getAccount(bobId); + auto bobUri = bobAccount->getUsername(); + auto aliceUri = aliceAccount->getUsername(); + std::mutex mtx; + std::unique_lock lk {mtx}; + std::condition_variable cv; + std::map> confHandlers; + auto requestReceived = false, requestReceivedBob2 = false; + confHandlers.insert( + DRing::exportable_callback( + [&](const std::string& accountId, + const std::string& conversationId, + std::map /*metadatas*/) { + if (accountId == bobId) + requestReceived = true; + else if (accountId == bob2Id) + requestReceivedBob2 = true; + cv.notify_one(); + })); + std::string convId = ""; + auto conversationReadyBob = false, conversationReadyBob2 = false; + confHandlers.insert(DRing::exportable_callback( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == aliceId) { + convId = conversationId; + } else if (accountId == bobId) { + conversationReadyBob = true; + } else if (accountId == bob2Id) { + conversationReadyBob2 = true; + } + cv.notify_one(); + })); + auto bob2Started = false; + confHandlers.insert( + DRing::exportable_callback( + [&](const std::string& accountId, const std::map& details) { + if (accountId == bob2Id) { + auto daemonStatus = details.at( + DRing::Account::VolatileProperties::DEVICE_ANNOUNCED); + if (daemonStatus == "true") + bob2Started = true; + } + cv.notify_one(); + })); + std::map preferencesBob, preferencesBob2; + confHandlers.insert( + DRing::exportable_callback( + [&](const std::string& accountId, + const std::string& conversationId, + std::map preferences) { + if (accountId == bobId) + preferencesBob = preferences; + else if (accountId == bob2Id) + preferencesBob2 = preferences; + cv.notify_one(); + })); + DRing::registerSignalHandlers(confHandlers); + // Bob creates a second device + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + bobAccount->exportArchive(bobArchive); + std::map details = DRing::getAccountTemplate("RING"); + details[ConfProperties::TYPE] = "RING"; + details[ConfProperties::DISPLAYNAME] = "BOB2"; + details[ConfProperties::ALIAS] = "BOB2"; + details[ConfProperties::UPNP_ENABLED] = "true"; + details[ConfProperties::ARCHIVE_PASSWORD] = ""; + details[ConfProperties::ARCHIVE_PIN] = ""; + details[ConfProperties::ARCHIVE_PATH] = bobArchive; + bob2Id = Manager::instance().addAccount(details); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bob2Started; })); + // Alice adds bob + requestReceived = false; + aliceAccount->addContact(bobUri); + aliceAccount->sendTrustRequest(bobUri, {}); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived && requestReceivedBob2; })); + DRing::acceptConversationRequest(bobId, convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&]() { return conversationReadyBob && conversationReadyBob2; })); + DRing::setConversationPreferences(bobId, convId, {{"foo", "bar"}, {"bar", "foo"}}); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return preferencesBob.size() == 2 && preferencesBob2.size() == 2; + })); + CPPUNIT_ASSERT(preferencesBob["foo"] == "bar" && preferencesBob["bar"] == "foo"); + CPPUNIT_ASSERT(preferencesBob2["foo"] == "bar" && preferencesBob2["bar"] == "foo"); +} + } // namespace test } // namespace jami diff --git a/tools/jamictrl/controller.py b/tools/jamictrl/controller.py index 0f778f819..beb61bba7 100644 --- a/tools/jamictrl/controller.py +++ b/tools/jamictrl/controller.py @@ -129,6 +129,7 @@ class DRingCtrl(Thread): proxy_confmgr.connect_to_signal('dataTransferEvent', self.onDataTransferEvent) proxy_confmgr.connect_to_signal('conversationReady', self.onConversationReady) proxy_confmgr.connect_to_signal('conversationRequestReceived', self.onConversationRequestReceived) + proxy_confmgr.connect_to_signal('conversationPreferencesUpdated', self.onConversationPreferencesUpdated) proxy_confmgr.connect_to_signal('messageReceived', self.onMessageReceived) except dbus.DBusException as e: @@ -314,6 +315,9 @@ class DRingCtrl(Thread): def onConversationRequestReceived(self, account, conversationId, metadatas): print(f'New conversation request for {account} with id {conversationId}') + def onConversationPreferencesUpdated(self, account, conversationId, metadatas): + print(f'New conversation preferences for {account} with id {conversationId}') + def onMessageReceived(self, account, conversationId, message): print(f'New message for {account} in conversation {conversationId} with id {message["id"]}') for key in message: