diff --git a/src/account.cpp b/src/account.cpp index 6e9a1f3c510a7a7f26b04613b4497f46aa840af5..a6bfb6826b0ee87ea09b9e86fd116b7585db1e21 100644 --- a/src/account.cpp +++ b/src/account.cpp @@ -82,6 +82,7 @@ const char* const Account::PASSWORD_KEY = "password"; const char* const Account::HOSTNAME_KEY = "hostname"; const char* const Account::ACCOUNT_ENABLE_KEY = "enable"; const char* const Account::ACCOUNT_AUTOANSWER_KEY = "autoAnswer"; +const char* const Account::ACCOUNT_READRECEIPT_KEY = "sendReadReceipt"; const char* const Account::ACCOUNT_ISRENDEZVOUS_KEY = "rendezVous"; const char* const Account::ACCOUNT_ACTIVE_CALL_LIMIT_KEY = "activeCallLimit"; const char* const Account::MAILBOX_KEY = "mailbox"; @@ -96,9 +97,10 @@ const char* const Account::LOCAL_MODERATORS_ENABLED_KEY = "localModeratorsEnable const char* const Account::ALL_MODERATORS_ENABLED_KEY = "allModeratorsEnabled"; #ifdef __ANDROID__ -constexpr const char* const DEFAULT_RINGTONE_PATH = "/data/data/cx.ring/files/ringtones/default.opus"; +constexpr const char* const DEFAULT_RINGTONE_PATH + = "/data/data/cx.ring/files/ringtones/default.opus"; #else -constexpr const char* const DEFAULT_RINGTONE_PATH = PROGSHAREDIR "/ringtones/default.opus"; +constexpr const char* const DEFAULT_RINGTONE_PATH = PROGSHAREDIR "/ringtones/default.opus"; #endif Account::Account(const std::string& accountID) @@ -109,6 +111,7 @@ Account::Account(const std::string& accountID) , alias_() , enabled_(true) , autoAnswerEnabled_(false) + , sendReadReceipt_(true) , isRendezVous_(false) , registrationState_(RegistrationState::UNREGISTERED) , systemCodecContainer_(getSystemCodecContainer()) @@ -122,7 +125,8 @@ Account::Account(const std::string& accountID) , upnpEnabled_(true) , localModeratorsEnabled_(true) , allModeratorsEnabled_(true) -#if (defined(__linux__) and not defined(__ANDROID__)) || defined(WIN32) || (defined(__APPLE__) && TARGET_OS_MAC) +#if (defined(__linux__) and not defined(__ANDROID__)) || defined(WIN32) \ + || (defined(__APPLE__) && TARGET_OS_MAC) , multiStreamEnabled_(true) #else , multiStreamEnabled_(false) @@ -234,6 +238,7 @@ Account::serialize(YAML::Emitter& out) const out << YAML::Key << ACTIVE_CODEC_KEY << YAML::Value << activeCodecs; out << YAML::Key << MAILBOX_KEY << YAML::Value << mailBox_; out << YAML::Key << ACCOUNT_AUTOANSWER_KEY << YAML::Value << autoAnswerEnabled_; + out << YAML::Key << ACCOUNT_READRECEIPT_KEY << YAML::Value << sendReadReceipt_; out << YAML::Key << ACCOUNT_ISRENDEZVOUS_KEY << YAML::Value << isRendezVous_; out << YAML::Key << ACCOUNT_ACTIVE_CALL_LIMIT_KEY << YAML::Value << activeCallLimit_; out << YAML::Key << RINGTONE_ENABLED_KEY << YAML::Value << ringtoneEnabled_; @@ -257,6 +262,7 @@ Account::unserialize(const YAML::Node& node) parseValue(node, ALIAS_KEY, alias_); parseValue(node, ACCOUNT_ENABLE_KEY, enabled_); parseValue(node, ACCOUNT_AUTOANSWER_KEY, autoAnswerEnabled_); + parseValueOptional(node, ACCOUNT_READRECEIPT_KEY, sendReadReceipt_); parseValueOptional(node, ACCOUNT_ISRENDEZVOUS_KEY, isRendezVous_); parseValue(node, ACCOUNT_ACTIVE_CALL_LIMIT_KEY, activeCallLimit_); // parseValue(node, PASSWORD_KEY, password_); @@ -314,6 +320,7 @@ Account::setAccountDetails(const std::map<std::string, std::string>& details) parseString(details, Conf::CONFIG_ACCOUNT_HOSTNAME, hostname_); parseString(details, Conf::CONFIG_ACCOUNT_MAILBOX, mailBox_); parseBool(details, Conf::CONFIG_ACCOUNT_AUTOANSWER, autoAnswerEnabled_); + parseBool(details, Conf::CONFIG_ACCOUNT_SENDREADRECEIPT, sendReadReceipt_); parseBool(details, Conf::CONFIG_ACCOUNT_ISRENDEZVOUS, isRendezVous_); parseInt(details, DRing::Account::ConfProperties::ACTIVE_CALL_LIMIT, activeCallLimit_); parseBool(details, Conf::CONFIG_RINGTONE_ENABLED, ringtoneEnabled_); @@ -348,6 +355,7 @@ Account::getAccountDetails() const {Conf::CONFIG_ACCOUNT_USERAGENT, customUserAgent_}, {Conf::CONFIG_ACCOUNT_HAS_CUSTOM_USERAGENT, hasCustomUserAgent_ ? TRUE_STR : FALSE_STR}, {Conf::CONFIG_ACCOUNT_AUTOANSWER, autoAnswerEnabled_ ? TRUE_STR : FALSE_STR}, + {Conf::CONFIG_ACCOUNT_SENDREADRECEIPT, sendReadReceipt_ ? TRUE_STR : FALSE_STR}, {Conf::CONFIG_ACCOUNT_ISRENDEZVOUS, isRendezVous_ ? TRUE_STR : FALSE_STR}, {DRing::Account::ConfProperties::ACTIVE_CALL_LIMIT, std::to_string(activeCallLimit_)}, {Conf::CONFIG_RINGTONE_ENABLED, ringtoneEnabled_ ? TRUE_STR : FALSE_STR}, @@ -712,9 +720,8 @@ bool Account::meetMinimumRequiredVersion(const std::vector<unsigned>& version, const std::vector<unsigned>& minRequiredVersion) { - for (size_t i=0; i<minRequiredVersion.size(); i++) { - if (i == version.size() or - version[i] < minRequiredVersion[i]) + for (size_t i = 0; i < minRequiredVersion.size(); i++) { + if (i == version.size() or version[i] < minRequiredVersion[i]) return false; if (version[i] > minRequiredVersion[i]) return true; diff --git a/src/account.h b/src/account.h index 637dc275d913f80d4b0781cbc53cf9851d1685e5..51cc8bcb053d0305602053ad998332792d7b2c74 100644 --- a/src/account.h +++ b/src/account.h @@ -285,6 +285,8 @@ public: bool isAutoAnswerEnabled() const { return autoAnswerEnabled_; } + bool isReadReceiptEnabled() const { return sendReadReceipt_; } + void attachCall(const std::string& id); void detachCall(const std::string& id); @@ -458,6 +460,7 @@ protected: static const char* const HOSTNAME_KEY; static const char* const ACCOUNT_ENABLE_KEY; static const char* const ACCOUNT_AUTOANSWER_KEY; + static const char* const ACCOUNT_READRECEIPT_KEY; static const char* const ACCOUNT_ISRENDEZVOUS_KEY; static const char* const ACCOUNT_ACTIVE_CALL_LIMIT_KEY; static const char* const MAILBOX_KEY; @@ -521,6 +524,9 @@ protected: /* If true, automatically answer calls to this account */ bool autoAnswerEnabled_; + // If true, send Displayed status (and emit to the client) + bool sendReadReceipt_; + /* If true mix calls into a conference */ bool isRendezVous_; diff --git a/src/account_schema.h b/src/account_schema.h index 8485ecb7308c9b8f2073d52075dde2c62b1fd42d..2e20e9d3a570de1675d1df3e9fa68cb2cb3783cb 100644 --- a/src/account_schema.h +++ b/src/account_schema.h @@ -37,6 +37,7 @@ static const char* const CONFIG_ACCOUNT_DISPLAYNAME = "Account.displayName"; static const char* const CONFIG_ACCOUNT_MAILBOX = "Account.mailbox"; static const char* const CONFIG_ACCOUNT_ENABLE = "Account.enable"; static const char* const CONFIG_ACCOUNT_AUTOANSWER = "Account.autoAnswer"; +static const char* const CONFIG_ACCOUNT_SENDREADRECEIPT = "Account.sendReadReceipt"; static const char* const CONFIG_ACCOUNT_ISRENDEZVOUS = "Account.rendezVous"; static const char* const CONFIG_ACCOUNT_REGISTRATION_EXPIRE = "Account.registrationExpire"; static const char* const CONFIG_ACCOUNT_DTMF_TYPE = "Account.dtmfType"; diff --git a/src/jami/account_const.h b/src/jami/account_const.h index cdf7568e4cb8401f4365622c5f3bb24ac223d7b5..c3ef7425e7397cd0d2522433dd10557eb21ed4a6 100644 --- a/src/jami/account_const.h +++ b/src/jami/account_const.h @@ -111,6 +111,7 @@ constexpr static const char ENABLED[] = "Account.enable"; constexpr static const char MAILBOX[] = "Account.mailbox"; constexpr static const char DTMF_TYPE[] = "Account.dtmfType"; constexpr static const char AUTOANSWER[] = "Account.autoAnswer"; +constexpr static const char SENDREADRECEIPT[] = "Account.sendReadReceipt"; constexpr static const char ISRENDEZVOUS[] = "Account.rendezVous"; constexpr static const char ACTIVE_CALL_LIMIT[] = "Account.activeCallLimit"; constexpr static const char HOSTNAME[] = "Account.hostname"; diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 91d6465e79623ce2368b823641fa0753adc43d4d..73bd60203be71e8a83da62627ead1a3eb67dc1b0 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -611,6 +611,8 @@ SIPAccountBase::onTextMessage(const std::string& id, conversationId = matched_pattern[1]; } + if (!isReadReceiptEnabled()) + return; if (conversationId.empty()) // Old method messageEngine_.onMessageDisplayed(from, from_hex_string(messageId), isDisplayed); else if (isDisplayed) { @@ -710,7 +712,7 @@ SIPAccountBase::setMessageDisplayed(const std::string& conversationUri, conversationId = uri.authority(); if (!conversationId.empty()) onMessageDisplayed(getUsername(), conversationId, messageId); - if (status == (int) DRing::Account::MessageStates::DISPLAYED) + if (status == (int) DRing::Account::MessageStates::DISPLAYED && isReadReceiptEnabled()) sendInstantMessage(uri.authority(), {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}}); return true; diff --git a/test/unitTest/conversation/conversation.cpp b/test/unitTest/conversation/conversation.cpp index d99685b2dc8f30cc519389eeb926c53d50126df9..2afb3cd6d03854a5a25484d769a19ac46745d72f 100644 --- a/test/unitTest/conversation/conversation.cpp +++ b/test/unitTest/conversation/conversation.cpp @@ -110,6 +110,7 @@ private: void testPingPongMessages(); void testIsComposing(); void testSetMessageDisplayed(); + void testSetMessageDisplayedPreference(); void testRemoveMember(); void testMemberBanNoBadFile(); void testMemberTryToRemoveAdmin(); @@ -120,6 +121,7 @@ private: void testVoteNonEmpty(); void testAdminCannotKickTheirself(); void testCommitUnauthorizedUser(); + // LATER void testBanDevice(); // LATER void testBannedDeviceCannotSendMessageButMemberCan(); // LATER void testRevokedDeviceCannotSendMessage(); @@ -177,6 +179,7 @@ private: CPPUNIT_TEST(testPingPongMessages); CPPUNIT_TEST(testIsComposing); CPPUNIT_TEST(testSetMessageDisplayed); + CPPUNIT_TEST(testSetMessageDisplayedPreference); CPPUNIT_TEST(testRemoveMember); CPPUNIT_TEST(testMemberBanNoBadFile); CPPUNIT_TEST(testMemberTryToRemoveAdmin); @@ -1294,6 +1297,96 @@ ConversationTest::testSetMessageDisplayed() DRing::unregisterSignalHandlers(); } +void +ConversationTest::testSetMessageDisplayedPreference() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + auto convId = aliceAccount->startConversation(); + auto details = aliceAccount->getAccountDetails(); + CPPUNIT_ASSERT(details[ConfProperties::SENDREADRECEIPT] == "true"); + details[ConfProperties::SENDREADRECEIPT] = "false"; + DRing::setAccountDetails(aliceId, details); + 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, + msgDisplayed = 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 && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + confHandlers.insert(DRing::exportable_callback<DRing::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "member") { + memberMessageGenerated = true; + cv.notify_one(); + } + })); + confHandlers.insert( + DRing::exportable_callback<DRing::ConfigurationSignal::AccountMessageStatusChanged>( + [&](const std::string& accountId, + const std::string& conversationId, + const std::string& peer, + const std::string& msgId, + int status) { + if (accountId == bobId && conversationId == convId && msgId == conversationId + && peer == aliceUri && status == 3) { + msgDisplayed = true; + cv.notify_one(); + } + })); + DRing::registerSignalHandlers(confHandlers); + aliceAccount->addConversationMember(convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, std::chrono::seconds(30), [&]() { + return requestReceived && memberMessageGenerated; + })); + memberMessageGenerated = false; + bobAccount->acceptConversationRequest(convId); + CPPUNIT_ASSERT( + cv.wait_for(lk, std::chrono::seconds(30), [&]() { return memberMessageGenerated; })); + + // Last displayed messages should not be set yet + auto membersInfos = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri && infos["lastDisplayed"] == ""; + }) + != membersInfos.end()); + + aliceAccount->setMessageDisplayed("swarm:" + convId, convId, 3); + // Bob should not receive anything here, as sendMessageDisplayed is disabled for Alice + CPPUNIT_ASSERT(!cv.wait_for(lk, std::chrono::seconds(10), [&]() { return msgDisplayed; })); + + // Assert that message is set as displayed for self (for the read status) + membersInfos = aliceAccount->getConversationMembers(convId); + CPPUNIT_ASSERT(std::find_if(membersInfos.begin(), + membersInfos.end(), + [&](auto infos) { + return infos["uri"] == aliceUri + && infos["lastDisplayed"] == convId; + }) + != membersInfos.end()); + DRing::unregisterSignalHandlers(); +} + void ConversationTest::testRemoveMember() {