diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 29c26eb662b0a9d0077709bfe04cdb046dd23df7..71b4bcd8e40a10483e39502f3ec092ca3f5f9c00 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -597,6 +597,26 @@ </arg> </signal> + <signal name="needsHost" tp:name-for-bindings="needsHost"> + <tp:added version="13.7.0"/> + <tp:docstring> + Notify client that a conversation needs a host for calls + </tp:docstring> + <arg type="s" name="accountId"/> + <arg type="s" name="conversationId"/> + </signal> + + <signal name="activeCallsChanged" tp:name-for-bindings="activeCallsChanged"> + <tp:added version="13.7.0"/> + <tp:docstring> + Notify client that a conversation got new active calls + </tp:docstring> + <arg type="s" name="accountId"/> + <arg type="s" name="conversationId"/> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out2" value="VectorMapStringString"/> + <arg type="aa{ss}" name="activeCalls" direction="out"/> + </signal> + <signal name="profileReceived" tp:name-for-bindings="profileReceived"> <tp:added version="9.2.0"/> <tp:docstring> @@ -1691,6 +1711,17 @@ <arg type="s" name="accountId" direction="in"/> </method> + <method name="getActiveCalls" tp:name-for-bindings="getActiveCalls"> + <tp:added version="13.7.0"/> + <tp:docstring> + Get the active call list per conversation + </tp:docstring> + <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="VectorMapStringString"/> + <arg type="aa{ss}" name="activeCalls" direction="out"/> + <arg type="s" name="accountId" direction="in"/> + <arg type="s" name="conversationId" direction="in"/> + </method> + <method name="getConversationRequests" tp:name-for-bindings="getConversationRequests"> <tp:added version="10.0.0"/> <tp:docstring> diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index 22179f059f5b7cff82ea9c6d2e84cf9945f4c490..b087a0e3eacecf31923aa55b551d7e7747179f4f 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -227,6 +227,8 @@ DBusClient::initLibrary(int flags) bind(&DBusConfigurationManager::accountMessageStatusChanged, confM, _1, _2, _3, _4, _5)), exportable_callback<ConfigurationSignal::ProfileReceived>( bind(&DBusConfigurationManager::profileReceived, confM, _1, _2, _3)), + exportable_callback<ConfigurationSignal::ActiveCallsChanged>( + bind(&DBusConfigurationManager::activeCallsChanged, confM, _1, _2, _3)), exportable_callback<ConfigurationSignal::ComposingStatusChanged>( bind(&DBusConfigurationManager::composingStatusChanged, confM, _1, _2, _3, _4)), exportable_callback<ConfigurationSignal::IncomingTrustRequest>( diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index 7e72fb4cd1c0274cdebd576c66a2347580be2e1e..7896840266b059c1c56967982fa64179275dfb06 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -303,7 +303,8 @@ DBusConfigurationManager::setAudioPlugin(const std::string& audioPlugin) } auto -DBusConfigurationManager::getAudioOutputDeviceList() -> decltype(libjami::getAudioOutputDeviceList()) +DBusConfigurationManager::getAudioOutputDeviceList() + -> decltype(libjami::getAudioOutputDeviceList()) { return libjami::getAudioOutputDeviceList(); } @@ -547,7 +548,11 @@ DBusConfigurationManager::validateCertificatePath(const std::string& accountId, -> decltype(libjami::validateCertificatePath( accountId, certificate, privateKey, privateKeyPass, caList)) { - return libjami::validateCertificatePath(accountId, certificate, privateKey, privateKeyPass, caList); + return libjami::validateCertificatePath(accountId, + certificate, + privateKey, + privateKeyPass, + caList); } auto @@ -721,7 +726,8 @@ DBusConfigurationManager::setVolume(const std::string& device, const double& val } auto -DBusConfigurationManager::getVolume(const std::string& device) -> decltype(libjami::getVolume(device)) +DBusConfigurationManager::getVolume(const std::string& device) + -> decltype(libjami::getVolume(device)) { return libjami::getVolume(device); } @@ -858,6 +864,13 @@ DBusConfigurationManager::getConversations(const std::string& accountId) return libjami::getConversations(accountId); } +std::vector<std::map<std::string, std::string>> +DBusConfigurationManager::getActiveCalls(const std::string& accountId, + const std::string& conversationId) +{ + return libjami::getActiveCalls(accountId, conversationId); +} + std::vector<std::map<std::string, std::string>> DBusConfigurationManager::getConversationRequests(const std::string& accountId) { @@ -967,14 +980,14 @@ DBusConfigurationManager::searchConversation(const std::string& accountId, const uint32_t& maxResult) { return libjami::searchConversation(accountId, - conversationId, - author, - lastId, - regexSearch, - type, - after, - before, - maxResult); + conversationId, + author, + lastId, + regexSearch, + type, + after, + before, + maxResult); } bool diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index 413a3dac8c7d138527a676fa43dab9b7e2fa9b67..c176849c8ad7410d7592570eb3441eee6dba0fab 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -53,8 +53,8 @@ using RingDBusMessage = DBus::Struct<std::string, std::map<std::string, std::string>, uint64_t>; class LIBJAMI_PUBLIC DBusConfigurationManager : public cx::ring::Ring::ConfigurationManager_adaptor, - public DBus::IntrospectableAdaptor, - public DBus::ObjectAdaptor + public DBus::IntrospectableAdaptor, + public DBus::ObjectAdaptor { public: using RingDBusDataTransferInfo = DBus::Struct<std::string, @@ -258,6 +258,8 @@ public: void declineConversationRequest(const std::string& accountId, const std::string& conversationId); bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); + std::vector<std::map<std::string, std::string>> getActiveCalls( + const std::string& accountId, const std::string& conversationId); std::vector<std::map<std::string, std::string>> getConversationRequests( const std::string& accountId); void updateConversationInfos(const std::string& accountId, diff --git a/bin/jni/configurationmanager.i b/bin/jni/configurationmanager.i index 3714a1ca2b98deb5c09b5a0623db60103aa1de7e..be861049ff0cfed5ee1ee9cac29a2bc8e8bceabc 100644 --- a/bin/jni/configurationmanager.i +++ b/bin/jni/configurationmanager.i @@ -35,6 +35,8 @@ public: virtual void volatileAccountDetailsChanged(const std::string& account_id, const std::map<std::string, std::string>& details){} virtual void incomingAccountMessage(const std::string& /*account_id*/, const std::string& /*from*/, const std::string& /*message_id*/, const std::map<std::string, std::string>& /*payload*/){} virtual void accountMessageStatusChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::string& /*peer*/, const std::string& /*message_id*/, int /*state*/){} + virtual void needsHost(const std::string& /*account_id*/, const std::string& /*conversationId*/){} + virtual void activeCallsChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::vector<std::map<std::string, std::string>>& /*activeCalls*/ ){} virtual void profileReceived(const std::string& /*account_id*/, const std::string& /*from*/, const std::string& /*path*/){} virtual void composingStatusChanged(const std::string& /*account_id*/, const std::string& /*convId*/, const std::string& /*from*/, int /*state*/){} virtual void knownDevicesChanged(const std::string& /*account_id*/, const std::map<std::string, std::string>& /*devices*/){} @@ -120,6 +122,7 @@ std::map<std::string, std::string> getKnownRingDevices(const std::string& accoun bool revokeDevice(const std::string& accountID, const std::string& password, const std::string& deviceID); void setActiveCodecList(const std::string& accountID, const std::vector<unsigned>& list); +std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& accountId, const std::string& convId); std::vector<std::string> getAudioPluginList(); void setAudioPlugin(const std::string& audioPlugin); @@ -248,6 +251,8 @@ public: virtual void volatileAccountDetailsChanged(const std::string& account_id, const std::map<std::string, std::string>& details){} virtual void incomingAccountMessage(const std::string& /*account_id*/, const std::string& /*from*/, const std::string& /*message_id*/, const std::map<std::string, std::string>& /*payload*/){} virtual void accountMessageStatusChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::string& /*peer*/, const std::string& /*message_id*/, int /*state*/){} + virtual void needsHost(const std::string& /*account_id*/, const std::string& /*conversationId*/){} + virtual void activeCallsChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::vector<std::map<std::string, std::string>>& /*activeCalls*/ ){} virtual void composingStatusChanged(const std::string& /*account_id*/, const std::string& /*convId*/, const std::string& /*from*/, int /*state*/){} virtual void knownDevicesChanged(const std::string& /*account_id*/, const std::map<std::string, std::string>& /*devices*/){} virtual void exportOnRingEnded(const std::string& /*account_id*/, int /*state*/, const std::string& /*pin*/){} diff --git a/bin/jni/conversation.i b/bin/jni/conversation.i index 5d2c5384141c7e96fdb60308518be8fc0777f6d5..ae7b5501dcb7ca22ad889384c960039c7e3217dc 100644 --- a/bin/jni/conversation.i +++ b/bin/jni/conversation.i @@ -49,6 +49,7 @@ namespace libjami { void declineConversationRequest(const std::string& accountId, const std::string& conversationId); bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); + std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& accountId, const std::string& conversationId); std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId); void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map<std::string, std::string>& infos); std::map<std::string, std::string> conversationInfos(const std::string& accountId, const std::string& conversationId); diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i index e9f4d399dfa3b0cdfd73e8e13bf60884d5798aec..8f2159d888d49d1d9ab1faa54d07ae81db37d910 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -273,6 +273,8 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM exportable_callback<ConfigurationSignal::Error>(bind(&ConfigurationCallback::errorAlert, confM, _1)), exportable_callback<ConfigurationSignal::IncomingAccountMessage>(bind(&ConfigurationCallback::incomingAccountMessage, confM, _1, _2, _3, _4 )), exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>(bind(&ConfigurationCallback::accountMessageStatusChanged, confM, _1, _2, _3, _4, _5 )), + exportable_callback<ConfigurationSignal::NeedsHost>(bind(&ConfigurationCallback::needsHost, confM, _1, _2 )), + exportable_callback<ConfigurationSignal::ActiveCallsChanged>(bind(&ConfigurationCallback::activeCallsChanged, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::ProfileReceived>(bind(&ConfigurationCallback::profileReceived, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::ComposingStatusChanged>(bind(&ConfigurationCallback::composingStatusChanged, confM, _1, _2, _3, _4 )), exportable_callback<ConfigurationSignal::IncomingTrustRequest>(bind(&ConfigurationCallback::incomingTrustRequest, confM, _1, _2, _3, _4, _5 )), diff --git a/bin/nodejs/callback.h b/bin/nodejs/callback.h index 749d887c70f7764a9960c3bc8c55c34f9d352efc..7b2a4d69b2a75e1c79522a45a1d5428ed012f041 100644 --- a/bin/nodejs/callback.h +++ b/bin/nodejs/callback.h @@ -16,6 +16,8 @@ Persistent<Function> composingStatusChangedCb; Persistent<Function> volatileDetailsChangedCb; Persistent<Function> incomingAccountMessageCb; Persistent<Function> accountMessageStatusChangedCb; +Persistent<Function> needsHostCb; +Persistent<Function> activeCallsChangedCb; Persistent<Function> incomingTrustRequestCb; Persistent<Function> contactAddedCb; Persistent<Function> contactRemovedCb; @@ -67,6 +69,10 @@ getPresistentCb(std::string_view signal) return &incomingAccountMessageCb; else if (signal == "AccountMessageStatusChanged") return &accountMessageStatusChangedCb; + else if (signal == "NeedsHost") + return &needsHostCb; + else if (signal == "ActiveCallsChanged") + return &activeCallsChangedCb; else if (signal == "IncomingTrustRequest") return &incomingTrustRequestCb; else if (signal == "ContactAdded") @@ -417,6 +423,41 @@ accountMessageStatusChanged(const std::string& account_id, uv_async_send(&signalAsync); } +void +needsHost(const std::string& account_id, const std::string& conversationId) +{ + std::lock_guard<std::mutex> lock(pendingSignalsLock); + pendingSignals.emplace([account_id, conversationId]() { + Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), needsHostCb); + if (!func.IsEmpty()) { + Local<Value> callback_args[] = {V8_STRING_NEW_LOCAL(account_id), + V8_STRING_NEW_LOCAL(conversationId)}; + func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 2, callback_args); + } + }); + + uv_async_send(&signalAsync); +} + +void +activeCallsChanged(const std::string& account_id, + const std::string& conversationId, + const std::vector<std::map<std::string, std::string>>& activeCalls) +{ + std::lock_guard<std::mutex> lock(pendingSignalsLock); + pendingSignals.emplace([account_id, conversationId, activeCalls]() { + Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), activeCallsChangedCb); + if (!func.IsEmpty()) { + Local<Value> callback_args[] = {V8_STRING_NEW_LOCAL(account_id), + V8_STRING_NEW_LOCAL(conversationId), + stringMapVecToJsMapArray(activeCalls)}; + func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 3, callback_args); + } + }); + + uv_async_send(&signalAsync); +} + void incomingAccountMessage(const std::string& accountId, const std::string& messageId, @@ -852,8 +893,7 @@ logMessage(const std::string& message) { std::lock_guard<std::mutex> lock(pendingSignalsLock); pendingSignals.emplace([message]() { - Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), - messageSendCb); + Local<Function> func = Local<Function>::New(Isolate::GetCurrent(), messageSendCb); if (!func.IsEmpty()) { SWIGV8_VALUE callback_args[] = {V8_STRING_NEW_LOCAL(message)}; func->Call(SWIGV8_CURRENT_CONTEXT(), SWIGV8_NULL(), 1, callback_args); diff --git a/bin/nodejs/configurationmanager.i b/bin/nodejs/configurationmanager.i index dfecde2b9c1008b75cd4646a4b19149e87805be3..6cdc41f4cc120c6c2ae426512ee30b7b7e7fa992 100644 --- a/bin/nodejs/configurationmanager.i +++ b/bin/nodejs/configurationmanager.i @@ -34,6 +34,8 @@ public: virtual void volatileAccountDetailsChanged(const std::string& account_id, const std::map<std::string, std::string>& details){} virtual void incomingAccountMessage(const std::string& /*account_id*/, const std::string& /*from*/, const std::map<std::string, std::string>& /*payload*/){} virtual void accountMessageStatusChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::string& /*peer*/, const std::string& /*message_id*/, int /*state*/){} + virtual void needsHost(const std::string& /*account_id*/, const std::string& /*conversationId*/){} + virtual void activeCallsChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::vector<std::map<std::string, std::string>>& /*activeCalls*/ ){} virtual void profileReceived(const std::string& /*account_id*/, const std::string& /*from*/, const std::string& /*path*/){} virtual void composingStatusChanged(const std::string& /*account_id*/, const std::string& /*convId*/, const std::string& /*from*/, int /*state*/){} virtual void knownDevicesChanged(const std::string& /*account_id*/, const std::map<std::string, std::string>& /*devices*/){} @@ -103,6 +105,7 @@ bool searchUser(const std::string& account, const std::string& query); std::vector<unsigned> getCodecList(); std::vector<std::string> getSupportedTlsMethod(); std::vector<std::string> getSupportedCiphers(const std::string& accountID); +std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& accountId, const std::string& convId); std::map<std::string, std::string> getCodecDetails(const std::string& accountID, const unsigned& codecId); bool setCodecDetails(const std::string& accountID, const unsigned& codecId, const std::map<std::string, std::string>& details); std::vector<unsigned> getActiveCodecList(const std::string& accountID); @@ -233,6 +236,8 @@ public: virtual void volatileAccountDetailsChanged(const std::string& account_id, const std::map<std::string, std::string>& details){} virtual void incomingAccountMessage(const std::string& /*account_id*/, const std::string& /*from*/, const std::map<std::string, std::string>& /*payload*/){} virtual void accountMessageStatusChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::string& /*peer*/, const std::string& /*message_id*/, int /*state*/){} + virtual void needsHost(const std::string& /*account_id*/, const std::string& /*conversationId*/){} + virtual void activeCallsChanged(const std::string& /*account_id*/, const std::string& /*conversationId*/, const std::vector<std::map<std::string, std::string>>& /*activeCalls*/ ){} virtual void profileReceived(const std::string& /*account_id*/, const std::string& /*from*/, const std::string& /*path*/){} virtual void composingStatusChanged(const std::string& /*account_id*/, const std::string& /*convId*/, const std::string& /*from*/, int /*state*/){} virtual void knownDevicesChanged(const std::string& /*account_id*/, const std::map<std::string, std::string>& /*devices*/){} diff --git a/bin/nodejs/conversation.i b/bin/nodejs/conversation.i index fc99c55a17439f511eecc92442b5ac8600fb9d78..0de5d6168f179ff82df07741efec0cd609c05946 100644 --- a/bin/nodejs/conversation.i +++ b/bin/nodejs/conversation.i @@ -49,6 +49,7 @@ namespace libjami { void declineConversationRequest(const std::string& accountId, const std::string& conversationId); bool removeConversation(const std::string& accountId, const std::string& conversationId); std::vector<std::string> getConversations(const std::string& accountId); + std::vector<std::map<std::string, std::string>> getActiveCalls(const std::string& accountId, const std::string& conversationId); std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId); void updateConversationInfos(const std::string& accountId, const std::string& conversationId, const std::map<std::string, std::string>& infos); std::map<std::string, std::string> conversationInfos(const std::string& accountId, const std::string& conversationId); diff --git a/bin/nodejs/nodejs_interface.i b/bin/nodejs/nodejs_interface.i index 1acb741cd1d08d7bbadfe3ca739ac09043cc9ea9..8adf698695e445d0092b818e9d4494f1142eaa32 100644 --- a/bin/nodejs/nodejs_interface.i +++ b/bin/nodejs/nodejs_interface.i @@ -142,6 +142,8 @@ void init(const SWIGV8_VALUE& funcMap){ exportable_callback<ConfigurationSignal::IncomingAccountMessage>(bind(&incomingAccountMessage, _1, _2, _3, _4 )), exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>(bind(&accountMessageStatusChanged, _1, _2, _3, _4, _5 )), exportable_callback<ConfigurationSignal::MessageSend>(bind(&logMessage, _1 )), + exportable_callback<ConfigurationSignal::NeedsHost>(bind(&ConfigurationCallback::needsHost, _1, _2 )), + exportable_callback<ConfigurationSignal::ActiveCallsChanged>(bind(&ConfigurationCallback::activeCallsChanged, _1, _2, _3 )), //exportable_callback<ConfigurationSignal::ProfileReceived>(bind(&profileReceived, _1, _2, _3, _4 )), //exportable_callback<ConfigurationSignal::IncomingTrustRequest>(bind(&incomingTrustRequest, _1, _2, _3, _4, _5 )), }; diff --git a/configure.ac b/configure.ac index 8a38e43429ad7ccfac7338a22c16df7ea0e44176..19b263e97a409aab46c30d7325f69f5f8c23798a 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.6.0],[jami@gnu.org],[jami]) +AC_INIT([Jami Daemon],[13.7.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 211acff63081ca08b3cdafedb536babfbc12de36..a93b4052556b28823fb7b5e14b2cc0d20790c213 100644 --- a/meson.build +++ b/meson.build @@ -1,5 +1,5 @@ project('jami-daemon', ['c', 'cpp'], - version: '13.6.0', + version: '13.7.0', license: 'GPL3+', default_options: ['cpp_std=gnu++17', 'buildtype=debugoptimized'], meson_version:'>= 0.56' diff --git a/src/call.cpp b/src/call.cpp index 328c2af1f464e3ce057d481e67cc7ae810f0d6d7..e07f3f27046c202a6ffabafd02ff3ec143899a2d 100644 --- a/src/call.cpp +++ b/src/call.cpp @@ -117,13 +117,17 @@ Call::Call(const std::shared_ptr<Account>& account, std::chrono::seconds(timeout)); } - if (!isSubcall() && getCallType() == CallType::OUTGOING) { + if (!isSubcall()) { if (cnx_state == ConnectionState::CONNECTED && duration_start_ == time_point::min()) duration_start_ = clock::now(); else if (cnx_state == ConnectionState::DISCONNECTED && call_state == CallState::OVER) { if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>(getAccount().lock())) { - jamiAccount->convModule()->addCallHistoryMessage(getPeerNumber(), - getCallDuration().count()); + // TODO: This will be removed when 1:1 swarm will have a conference. + // For now, only commit for 1:1 calls + if (toUsername().find('/') == std::string::npos) { + jamiAccount->convModule()->addCallHistoryMessage(getPeerNumber(), + getCallDuration().count()); + } monitor(); } } @@ -275,7 +279,10 @@ Call::setState(CallState call_state, ConnectionState cnx_state, signed code) new_client_state.c_str(), code); lock.unlock(); - emitSignal<libjami::CallSignal::StateChange>(getAccountId(), id_, new_client_state, code); + emitSignal<libjami::CallSignal::StateChange>(getAccountId(), + id_, + new_client_state, + code); } } diff --git a/src/call.h b/src/call.h index ac423d60b69aea9367267fcb4a54473df3cd4ea7..72c715394af92f910477b6cbe853873a310ce2e7 100644 --- a/src/call.h +++ b/src/call.h @@ -170,6 +170,18 @@ public: */ void setPeerDisplayName(const std::string& name) { peerDisplayName_ = name; } + /** + * Get "To" from the invite + * @note Used to make the difference between incoming calls for accounts and for conversations + * @return the "To" that was present in the invite + */ + const std::string& toUsername() const { return toUsername_; } + /** + * Updated by sipvoiplink, corresponds to the "To" in the invite + * @param username "To" + */ + void toUsername(const std::string& username) { toUsername_ = username; } + /** * Get the peer display name (caller in ingoing) * not protected by mutex (when created) @@ -537,6 +549,7 @@ protected: /// Supported conference protocol version int peerConfProtocol_ {0}; + std::string toUsername_ {}; }; // Helpers diff --git a/src/client/conversation_interface.cpp b/src/client/conversation_interface.cpp index d46952c8b153b1aadb2d8513815013305bc63803..f67f7f7b00d9caf088dfb7aadb4fd22d574ab79b 100644 --- a/src/client/conversation_interface.cpp +++ b/src/client/conversation_interface.cpp @@ -78,6 +78,15 @@ getConversations(const std::string& accountId) return {}; } +std::vector<std::map<std::string, std::string>> +getActiveCalls(const std::string& accountId, const std::string& conversationId) +{ + if (auto acc = jami::Manager::instance().getAccount<jami::JamiAccount>(accountId)) + if (auto convModule = acc->convModule()) + return convModule->getActiveCalls(conversationId); + return {}; +} + std::vector<std::map<std::string, std::string>> getConversationRequests(const std::string& accountId) { diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index 6906887ebeeff45e042695f4adc0dede7eaa8df5..277e25405838ae7fb03213e25a0f05deca3ad7ba 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -66,6 +66,8 @@ getSignalHandlers() exported_callback<libjami::ConfigurationSignal::IncomingAccountMessage>(), exported_callback<libjami::ConfigurationSignal::ComposingStatusChanged>(), exported_callback<libjami::ConfigurationSignal::AccountMessageStatusChanged>(), + exported_callback<libjami::ConfigurationSignal::NeedsHost>(), + exported_callback<libjami::ConfigurationSignal::ActiveCallsChanged>(), exported_callback<libjami::ConfigurationSignal::ProfileReceived>(), exported_callback<libjami::ConfigurationSignal::IncomingTrustRequest>(), exported_callback<libjami::ConfigurationSignal::ContactAdded>(), diff --git a/src/conference.cpp b/src/conference.cpp index ffab65089f6084ceed3677309e6cf19eeaa2103c..d7b1db1a1809a677544538a021b7d0607b58e7ad 100644 --- a/src/conference.cpp +++ b/src/conference.cpp @@ -54,8 +54,10 @@ using namespace std::literals; namespace jami { -Conference::Conference(const std::shared_ptr<Account>& account, bool attachHost) - : id_(Manager::instance().callFactory.getNewCallID()) +Conference::Conference(const std::shared_ptr<Account>& account, + const std::string& confId, + bool attachHost) + : id_(confId.empty() ? Manager::instance().callFactory.getNewCallID() : confId) , account_(account) #ifdef ENABLE_VIDEO , videoEnabled_(account->isVideoEnabled()) @@ -87,6 +89,7 @@ Conference::Conference(const std::shared_ptr<Account>& account, bool attachHost) JAMI_INFO("Create new conference %s", id_.c_str()); setLocalHostDefaultMediaSource(); + duration_start_ = clock::now(); #ifdef ENABLE_VIDEO auto itVideo = std::find_if(hostSources_.begin(), hostSources_.end(), [&](auto attr) { @@ -281,11 +284,11 @@ Conference::~Conference() // Continue the recording for the call if the conference was recorded if (isRecording()) { - JAMI_DBG("Stop recording for conf %s", getConfId().c_str()); + JAMI_DEBUG("Stop recording for conf {:s}", getConfId()); toggleRecording(); if (not call->isRecording()) { - JAMI_DBG("Conference was recorded, start recording for conf %s", - call->getCallId().c_str()); + JAMI_DEBUG("Conference was recorded, start recording for conf {:s}", + call->getCallId()); call->toggleRecording(); } } @@ -314,6 +317,8 @@ Conference::~Conference() confAVStreams.clear(); } #endif // ENABLE_PLUGIN + if (shutdownCb_) + shutdownCb_(getDuration().count()); jami_tracepoint(conference_end, id_.c_str()); } @@ -326,10 +331,10 @@ Conference::getState() const void Conference::setState(State state) { - JAMI_DBG("[conf %s] Set state to [%s] (was [%s])", - id_.c_str(), - getStateStr(state), - getStateStr()); + JAMI_DEBUG("[conf {:s}] Set state to [{:s}] (was [{:s}])", + id_, + getStateStr(state), + getStateStr()); confState_ = state; } @@ -345,9 +350,7 @@ Conference::setLocalHostDefaultMediaSource() = {MediaType::MEDIA_AUDIO, false, false, true, {}, sip_utils::DEFAULT_AUDIO_STREAMID}; } - JAMI_DBG("[conf %s] Setting local host audio source to [%s]", - id_.c_str(), - audioAttr.toString().c_str()); + JAMI_DEBUG("[conf {:s}] Setting local host audio source to [{:s}]", id_, audioAttr.toString()); hostSources_.emplace_back(audioAttr); #ifdef ENABLE_VIDEO @@ -363,9 +366,9 @@ Conference::setLocalHostDefaultMediaSource() Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice(), sip_utils::DEFAULT_VIDEO_STREAMID}; } - JAMI_DBG("[conf %s] Setting local host video source to [%s]", - id_.c_str(), - videoAttr.toString().c_str()); + JAMI_DEBUG("[conf {:s}] Setting local host video source to [{:s}]", + id_, + videoAttr.toString()); hostSources_.emplace_back(videoAttr); } #endif @@ -516,9 +519,9 @@ Conference::takeOverMediaSourceControl(const std::string& callId) if (iter == mediaList.end()) { // Nothing to do if the call does not have a stream with // the requested media. - JAMI_DBG("[Call: %s] Does not have an active [%s] media source", - callId.c_str(), - MediaAttribute::mediaTypeToString(mediaType)); + JAMI_DEBUG("[Call: {:s}] Does not have an active [{:s}] media source", + callId, + MediaAttribute::mediaTypeToString(mediaType)); continue; } @@ -568,14 +571,12 @@ Conference::requestMediaChange(const std::vector<libjami::MediaMap>& mediaList) return false; } - JAMI_DBG("[conf %s] Request media change", getConfId().c_str()); + JAMI_DEBUG("[conf {:s}] Request media change", getConfId()); auto mediaAttrList = MediaAttribute::buildMediaAttributesList(mediaList, false); for (auto const& mediaAttr : mediaAttrList) { - JAMI_DBG("[conf %s] New requested media: %s", - getConfId().c_str(), - mediaAttr.toString(true).c_str()); + JAMI_DEBUG("[conf {:s}] New requested media: {:s}", getConfId(), mediaAttr.toString(true)); } std::vector<std::string> newVideoInputs; @@ -616,7 +617,7 @@ void Conference::handleMediaChangeRequest(const std::shared_ptr<Call>& call, const std::vector<libjami::MediaMap>& remoteMediaList) { - JAMI_DBG("Conf [%s] Answer to media change request", getConfId().c_str()); + JAMI_DEBUG("Conf [{:s}] Answer to media change request", getConfId()); auto currentMediaList = hostSources_; #ifdef ENABLE_VIDEO @@ -664,7 +665,7 @@ Conference::handleMediaChangeRequest(const std::shared_ptr<Call>& call, void Conference::addParticipant(const std::string& participant_id) { - JAMI_DBG("Adding call %s to conference %s", participant_id.c_str(), id_.c_str()); + JAMI_DEBUG("Adding call {:s} to conference {:s}", participant_id, id_); jami_tracepoint(conference_add_participant, id_.c_str(), participant_id.c_str()); @@ -674,19 +675,15 @@ Conference::addParticipant(const std::string& participant_id) return; } - // Check if participant was muted before conference if (auto call = getCall(participant_id)) { - if (call->isPeerMuted()) { + // Check if participant was muted before conference + if (call->isPeerMuted()) participantsMuted_.emplace(call->getCallId()); - } // NOTE: // When a call joins a conference, the media source of the call // will be set to the output of the conference mixer. takeOverMediaSourceControl(participant_id); - } - - if (auto call = getCall(participant_id)) { auto w = call->getAccount(); auto account = w.lock(); if (account) { @@ -708,9 +705,7 @@ Conference::addParticipant(const std::string& participant_id) if (account->isAllModerators()) moderators_.emplace(getRemoteId(call)); } - } #ifdef ENABLE_VIDEO - if (auto call = getCall(participant_id)) { // In conference, if a participant joins with an audio only // call, it must be listed in the audioonlylist. auto mediaList = call->getMediaAttributeList(); @@ -722,17 +717,17 @@ Conference::addParticipant(const std::string& participant_id) call->enterConference(shared_from_this()); // Continue the recording for the conference if one participant was recording if (call->isRecording()) { - JAMI_DBG("Stop recording for call %s", call->getCallId().c_str()); + JAMI_DEBUG("Stop recording for call {:s}", call->getCallId()); call->toggleRecording(); if (not this->isRecording()) { - JAMI_DBG("One participant was recording, start recording for conference %s", - getConfId().c_str()); + JAMI_DEBUG("One participant was recording, start recording for conference {:s}", + getConfId()); this->toggleRecording(); } } +#endif // ENABLE_VIDEO } else JAMI_ERR("no call associate to participant %s", participant_id.c_str()); -#endif // ENABLE_VIDEO #ifdef ENABLE_PLUGIN createConfAVStreams(); #endif @@ -847,7 +842,8 @@ Conference::sendConferenceInfos() // Inform client that layout has changed jami::emitSignal<libjami::CallSignal::OnConferenceInfosUpdated>(id_, - confInfo.toVectorMapStringString()); + confInfo + .toVectorMapStringString()); } #ifdef ENABLE_VIDEO @@ -869,6 +865,7 @@ Conference::createSinks(const ConfInfo& infos) void Conference::removeParticipant(const std::string& participant_id) { + JAMI_DEBUG("Remove call {:s} in conference {:s}", participant_id, id_); { std::lock_guard<std::mutex> lk(participantsMtx_); if (!participants_.erase(participant_id)) @@ -955,9 +952,11 @@ Conference::detachLocalParticipant() "Invalid conference state in detach participant: current \"%s\" - expected \"%s\"", getStateStr(), "ACTIVE_ATTACHED"); + return; } setLocalHostDefaultMediaSource(); + setState(State::ACTIVE_DETACHED); } void @@ -1058,7 +1057,7 @@ void Conference::switchInput(const std::string& input) { #ifdef ENABLE_VIDEO - JAMI_DBG("[Conf:%s] Setting video input to %s", id_.c_str(), input.c_str()); + JAMI_DEBUG("[Conf:{:s}] Setting video input to {:s}", id_, input); std::vector<MediaAttribute> newSources; auto firstVideo = true; // Rewrite hostSources (remove all except one video input) @@ -1232,11 +1231,11 @@ Conference::setHandRaised(const std::string& deviceId, const bool& state) callDeviceId = transport->deviceId(); if (deviceId == callDeviceId) { if (state and not isPeerRequiringAttention) { - JAMI_DBG("Raise %s hand", deviceId.c_str()); + JAMI_DEBUG("Raise {:s} hand", deviceId); handsRaised_.emplace(deviceId); updateHandsRaised(); } else if (not state and isPeerRequiringAttention) { - JAMI_DBG("Remove %s raised hand", deviceId.c_str()); + JAMI_DEBUG("Remove {:s} raised hand", deviceId); handsRaised_.erase(deviceId); updateHandsRaised(); } @@ -1301,11 +1300,11 @@ Conference::setModerator(const std::string& participant_id, const bool& state) auto isPeerModerator = isModerator(participant_id); if (participant_id == getRemoteId(call)) { if (state and not isPeerModerator) { - JAMI_DBG("Add %s as moderator", participant_id.c_str()); + JAMI_DEBUG("Add {:s} as moderator", participant_id); moderators_.emplace(participant_id); updateModerators(); } else if (not state and isPeerModerator) { - JAMI_DBG("Remove %s as moderator", participant_id.c_str()); + JAMI_DEBUG("Remove {:s} as moderator", participant_id); moderators_.erase(participant_id); updateModerators(); } @@ -1417,12 +1416,12 @@ Conference::muteCall(const std::string& callId, bool state) { auto isPartMuted = isMuted(callId); if (state and not isPartMuted) { - JAMI_DBG("Mute participant %.*s", (int) callId.size(), callId.data()); + JAMI_DEBUG("Mute participant {:s}", callId); participantsMuted_.emplace(callId); unbindParticipant(callId); updateMuted(); } else if (not state and isPartMuted) { - JAMI_DBG("Unmute participant %.*s", (int) callId.size(), callId.data()); + JAMI_DEBUG("Unmute participant {:s}", callId); participantsMuted_.erase(callId); bindParticipant(callId); updateMuted(); @@ -1596,7 +1595,8 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType) { if (mediaType.compare(libjami::Media::Details::MEDIA_TYPE_AUDIO) == 0) { if (is_muted == isMediaSourceMuted(MediaType::MEDIA_AUDIO)) { - JAMI_DBG("Local audio source already in [%s] state", is_muted ? "muted" : "un-muted"); + JAMI_DEBUG("Local audio source already in [{:s}] state", + is_muted ? "muted" : "un-muted"); return; } @@ -1620,7 +1620,8 @@ Conference::muteLocalHost(bool is_muted, const std::string& mediaType) } if (is_muted == isMediaSourceMuted(MediaType::MEDIA_VIDEO)) { - JAMI_DBG("Local video source already in [%s] state", is_muted ? "muted" : "un-muted"); + JAMI_DEBUG("Local video source already in [{:s}] state", + is_muted ? "muted" : "un-muted"); return; } setLocalHostMuteState(MediaType::MEDIA_VIDEO, is_muted); diff --git a/src/conference.h b/src/conference.h index defb1f540affde3c3e80a85f9e90a9beda8c73f0..7212daedbccdfca9ec0c2ff1cd590c45664ffef3 100644 --- a/src/conference.h +++ b/src/conference.h @@ -24,6 +24,7 @@ #include "config.h" #endif +#include <chrono> #include <set> #include <string> #include <memory> @@ -187,6 +188,7 @@ struct ConfInfo : public std::vector<ParticipantInfo> }; using ParticipantSet = std::set<std::string>; +using clock = std::chrono::steady_clock; class Conference : public Recordable, public std::enable_shared_from_this<Conference> { @@ -196,7 +198,9 @@ public: /** * Constructor for this class, increment static counter */ - explicit Conference(const std::shared_ptr<Account>&, bool attachHost = true); + explicit Conference(const std::shared_ptr<Account>&, + const std::string& confId = "", + bool attachHost = true); /** * Destructor for this class, decrement static counter @@ -222,6 +226,11 @@ public: */ void setState(State state); + /** + * Set a callback that will be called when the conference will be destroyed + */ + void onShutdown(std::function<void(int)> cb) { shutdownCb_ = std::move(cb); } + /** * Return a string description of the conference state */ @@ -399,6 +408,17 @@ public: void stopRecording() override; bool startRecording(const std::string& path) override; + /** + * @return Conference duration in milliseconds + */ + std::chrono::milliseconds getDuration() const + { + return duration_start_ == clock::time_point::min() + ? std::chrono::milliseconds::zero() + : std::chrono::duration_cast<std::chrono::milliseconds>(clock::now() + - duration_start_); + } + private: std::weak_ptr<Conference> weak() { @@ -513,6 +533,9 @@ private: ConfProtocolParser parser_; std::string getRemoteId(const std::shared_ptr<jami::Call>& call) const; + + std::function<void(int)> shutdownCb_; + clock::time_point duration_start_; }; } // namespace jami diff --git a/src/jami/configurationmanager_interface.h b/src/jami/configurationmanager_interface.h index d8a3ef0d16922d09159c28b5975e7fd2bf66573d..4ad507ba7bbc8e405d9b2eaafbb7a4a7d64ce860 100644 --- a/src/jami/configurationmanager_interface.h +++ b/src/jami/configurationmanager_interface.h @@ -57,36 +57,36 @@ LIBJAMI_PUBLIC std::map<std::string, std::string> getAccountDetails(const std::s LIBJAMI_PUBLIC std::map<std::string, std::string> getVolatileAccountDetails( const std::string& accountID); LIBJAMI_PUBLIC void setAccountDetails(const std::string& accountID, - const std::map<std::string, std::string>& details); + const std::map<std::string, std::string>& details); LIBJAMI_PUBLIC void setAccountActive(const std::string& accountID, - bool active, - bool shutdownConnections = false); + bool active, + bool shutdownConnections = false); LIBJAMI_PUBLIC std::map<std::string, std::string> getAccountTemplate(const std::string& accountType); LIBJAMI_PUBLIC std::string addAccount(const std::map<std::string, std::string>& details, - const std::string& accountID = {}); + const std::string& accountID = {}); LIBJAMI_PUBLIC void monitor(bool continuous); LIBJAMI_PUBLIC bool exportOnRing(const std::string& accountID, const std::string& password); LIBJAMI_PUBLIC bool exportToFile(const std::string& accountID, - const std::string& destinationPath, - const std::string& password = {}); + const std::string& destinationPath, + const std::string& password = {}); LIBJAMI_PUBLIC bool revokeDevice(const std::string& accountID, - const std::string& password, - const std::string& deviceID); + const std::string& password, + const std::string& deviceID); LIBJAMI_PUBLIC std::map<std::string, std::string> getKnownRingDevices(const std::string& accountID); LIBJAMI_PUBLIC bool changeAccountPassword(const std::string& accountID, - const std::string& password_old, - const std::string& password_new); + const std::string& password_old, + const std::string& password_new); LIBJAMI_PUBLIC bool isPasswordValid(const std::string& accountID, const std::string& password); LIBJAMI_PUBLIC bool lookupName(const std::string& account, - const std::string& nameserver, - const std::string& name); + const std::string& nameserver, + const std::string& name); LIBJAMI_PUBLIC bool lookupAddress(const std::string& account, - const std::string& nameserver, - const std::string& address); + const std::string& nameserver, + const std::string& address); LIBJAMI_PUBLIC bool registerName(const std::string& account, - const std::string& password, - const std::string& name); + const std::string& password, + const std::string& name); LIBJAMI_PUBLIC bool searchUser(const std::string& account, const std::string& query); LIBJAMI_PUBLIC void removeAccount(const std::string& accountID); @@ -94,34 +94,34 @@ LIBJAMI_PUBLIC std::vector<std::string> getAccountList(); LIBJAMI_PUBLIC void sendRegister(const std::string& accountID, bool enable); LIBJAMI_PUBLIC void registerAllAccounts(void); LIBJAMI_PUBLIC uint64_t sendAccountTextMessage(const std::string& accountID, - const std::string& to, - const std::map<std::string, std::string>& payloads); + const std::string& to, + const std::map<std::string, std::string>& payloads); LIBJAMI_PUBLIC bool cancelMessage(const std::string& accountID, uint64_t message); LIBJAMI_PUBLIC std::vector<Message> getLastMessages(const std::string& accountID, - const uint64_t& base_timestamp); + const uint64_t& base_timestamp); LIBJAMI_PUBLIC std::map<std::string, std::string> getNearbyPeers(const std::string& accountID); LIBJAMI_PUBLIC int getMessageStatus(uint64_t id); LIBJAMI_PUBLIC int getMessageStatus(const std::string& accountID, uint64_t id); LIBJAMI_PUBLIC void setIsComposing(const std::string& accountID, - const std::string& conversationUri, - bool isWriting); + const std::string& conversationUri, + bool isWriting); LIBJAMI_PUBLIC bool setMessageDisplayed(const std::string& accountID, - const std::string& conversationUri, - const std::string& messageId, - int status); + const std::string& conversationUri, + const std::string& messageId, + int status); LIBJAMI_PUBLIC std::vector<unsigned> getCodecList(); LIBJAMI_PUBLIC std::vector<std::string> getSupportedTlsMethod(); LIBJAMI_PUBLIC std::vector<std::string> getSupportedCiphers(const std::string& accountID); LIBJAMI_PUBLIC std::map<std::string, std::string> getCodecDetails(const std::string& accountID, - const unsigned& codecId); + const unsigned& codecId); LIBJAMI_PUBLIC bool setCodecDetails(const std::string& accountID, - const unsigned& codecId, - const std::map<std::string, std::string>& details); + const unsigned& codecId, + const std::map<std::string, std::string>& details); LIBJAMI_PUBLIC std::vector<unsigned> getActiveCodecList(const std::string& accountID); LIBJAMI_PUBLIC void setActiveCodecList(const std::string& accountID, - const std::vector<unsigned>& list); + const std::vector<unsigned>& list); LIBJAMI_PUBLIC std::vector<std::string> getAudioPluginList(); LIBJAMI_PUBLIC void setAudioPlugin(const std::string& audioPlugin); @@ -175,7 +175,7 @@ LIBJAMI_PUBLIC void setAccountsOrder(const std::string& order); LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getCredentials( const std::string& accountID); LIBJAMI_PUBLIC void setCredentials(const std::string& accountID, - const std::vector<std::map<std::string, std::string>>& details); + const std::vector<std::map<std::string, std::string>>& details); LIBJAMI_PUBLIC std::string getAddrFromInterfaceName(const std::string& iface); @@ -188,8 +188,8 @@ LIBJAMI_PUBLIC double getVolume(const std::string& device); /* * Security */ -LIBJAMI_PUBLIC std::map<std::string, std::string> validateCertificate(const std::string& accountId, - const std::string& certificate); +LIBJAMI_PUBLIC std::map<std::string, std::string> validateCertificate( + const std::string& accountId, const std::string& certificate); LIBJAMI_PUBLIC std::map<std::string, std::string> validateCertificatePath( const std::string& accountId, const std::string& certificatePath, @@ -197,7 +197,8 @@ LIBJAMI_PUBLIC std::map<std::string, std::string> validateCertificatePath( const std::string& privateKeyPassword, const std::string& caList); -LIBJAMI_PUBLIC std::map<std::string, std::string> getCertificateDetails(const std::string& certificate); +LIBJAMI_PUBLIC std::map<std::string, std::string> getCertificateDetails( + const std::string& certificate); LIBJAMI_PUBLIC std::map<std::string, std::string> getCertificateDetailsPath( const std::string& certificatePath, const std::string& privateKey, @@ -206,7 +207,7 @@ LIBJAMI_PUBLIC std::map<std::string, std::string> getCertificateDetailsPath( LIBJAMI_PUBLIC std::vector<std::string> getPinnedCertificates(); LIBJAMI_PUBLIC std::vector<std::string> pinCertificate(const std::vector<uint8_t>& certificate, - bool local); + bool local); LIBJAMI_PUBLIC bool unpinCertificate(const std::string& certId); LIBJAMI_PUBLIC void pinCertificatePath(const std::string& path); @@ -214,10 +215,10 @@ LIBJAMI_PUBLIC unsigned unpinCertificatePath(const std::string& path); LIBJAMI_PUBLIC bool pinRemoteCertificate(const std::string& accountId, const std::string& certId); LIBJAMI_PUBLIC bool setCertificateStatus(const std::string& account, - const std::string& certId, - const std::string& status); + const std::string& certId, + const std::string& status); LIBJAMI_PUBLIC std::vector<std::string> getCertificatesByStatus(const std::string& account, - const std::string& status); + const std::string& status); /* contact requests */ LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getTrustRequests( @@ -225,15 +226,15 @@ LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getTrustRequests( LIBJAMI_PUBLIC bool acceptTrustRequest(const std::string& accountId, const std::string& from); LIBJAMI_PUBLIC bool discardTrustRequest(const std::string& accountId, const std::string& from); LIBJAMI_PUBLIC void sendTrustRequest(const std::string& accountId, - const std::string& to, - const std::vector<uint8_t>& payload = {}); + const std::string& to, + const std::vector<uint8_t>& payload = {}); /* Contacts */ LIBJAMI_PUBLIC void addContact(const std::string& accountId, const std::string& uri); LIBJAMI_PUBLIC void removeContact(const std::string& accountId, const std::string& uri, bool ban); LIBJAMI_PUBLIC std::map<std::string, std::string> getContactDetails(const std::string& accountId, - const std::string& uri); + const std::string& uri); LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getContacts( const std::string& accountId); @@ -261,7 +262,7 @@ LIBJAMI_PUBLIC void setPushNotificationTopic(const std::string& topic); * To be called by clients with relevant data when a push notification is received. */ LIBJAMI_PUBLIC void pushNotificationReceived(const std::string& from, - const std::map<std::string, std::string>& data); + const std::map<std::string, std::string>& data); /** * Returns whether or not the audio meter is enabled for ring buffer @id. @@ -281,8 +282,8 @@ LIBJAMI_PUBLIC void setAudioMeterState(const std::string& id, bool state); * Add/remove default moderator for conferences */ LIBJAMI_PUBLIC void setDefaultModerator(const std::string& accountID, - const std::string& peerURI, - bool state); + const std::string& peerURI, + bool state); /** * Get default moderators for an account @@ -386,6 +387,19 @@ struct LIBJAMI_PUBLIC ConfigurationSignal const std::string& /*message_id*/, int /*state*/); }; + struct LIBJAMI_PUBLIC NeedsHost + { + constexpr static const char* name = "NeedsHost"; + using cb_type = void(const std::string& /*account_id*/, + const std::string& /*conversation_id*/); + }; + struct LIBJAMI_PUBLIC ActiveCallsChanged + { + constexpr static const char* name = "ActiveCallsChanged"; + using cb_type = void(const std::string& /*account_id*/, + const std::string& /*conversation_id*/, + const std::vector<std::map<std::string, std::string>>& /*activeCalls*/); + }; struct LIBJAMI_PUBLIC ProfileReceived { constexpr static const char* name = "ProfileReceived"; diff --git a/src/jami/conversation_interface.h b/src/jami/conversation_interface.h index 011af5d64e16b15ba877a84eb66aad4a618bd657..0a9901f1a43887fc2b7de0857e5ae2572e246e73 100644 --- a/src/jami/conversation_interface.h +++ b/src/jami/conversation_interface.h @@ -34,65 +34,69 @@ namespace libjami { // Conversation management LIBJAMI_PUBLIC std::string startConversation(const std::string& accountId); LIBJAMI_PUBLIC void acceptConversationRequest(const std::string& accountId, - const std::string& conversationId); + const std::string& conversationId); LIBJAMI_PUBLIC void declineConversationRequest(const std::string& accountId, - const std::string& conversationId); + const std::string& conversationId); LIBJAMI_PUBLIC bool removeConversation(const std::string& accountId, - const std::string& conversationId); + const std::string& conversationId); LIBJAMI_PUBLIC std::vector<std::string> getConversations(const std::string& accountId); LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getConversationRequests( const std::string& accountId); +// Calls +LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getActiveCalls( + const std::string& accountId, const std::string& conversationId); + // Conversation's infos management LIBJAMI_PUBLIC void updateConversationInfos(const std::string& accountId, - const std::string& conversationId, - const std::map<std::string, std::string>& infos); -LIBJAMI_PUBLIC std::map<std::string, std::string> conversationInfos(const std::string& accountId, - const std::string& conversationId); + const std::string& conversationId, + const std::map<std::string, std::string>& infos); +LIBJAMI_PUBLIC std::map<std::string, std::string> conversationInfos( + const std::string& accountId, const std::string& conversationId); LIBJAMI_PUBLIC void setConversationPreferences(const std::string& accountId, - const std::string& conversationId, - const std::map<std::string, std::string>& prefs); + const std::string& conversationId, + const std::map<std::string, std::string>& prefs); LIBJAMI_PUBLIC std::map<std::string, std::string> getConversationPreferences( const std::string& accountId, const std::string& conversationId); // Member management LIBJAMI_PUBLIC void addConversationMember(const std::string& accountId, - const std::string& conversationId, - const std::string& contactUri); + const std::string& conversationId, + const std::string& contactUri); LIBJAMI_PUBLIC void removeConversationMember(const std::string& accountId, - const std::string& conversationId, - const std::string& contactUri); + const std::string& conversationId, + const std::string& contactUri); LIBJAMI_PUBLIC std::vector<std::map<std::string, std::string>> getConversationMembers( const std::string& accountId, const std::string& conversationId); // Message send/load LIBJAMI_PUBLIC void sendMessage(const std::string& accountId, - const std::string& conversationId, - const std::string& message, - const std::string& replyTo, - const int32_t& flag = 0); + const std::string& conversationId, + const std::string& message, + const std::string& replyTo, + const int32_t& flag = 0); LIBJAMI_PUBLIC uint32_t loadConversationMessages(const std::string& accountId, - const std::string& conversationId, - const std::string& fromMessage, - size_t n); + const std::string& conversationId, + const std::string& fromMessage, + size_t n); LIBJAMI_PUBLIC uint32_t loadConversationUntil(const std::string& accountId, - const std::string& conversationId, - const std::string& fromMessage, - const std::string& toMessage); + const std::string& conversationId, + const std::string& fromMessage, + const std::string& toMessage); LIBJAMI_PUBLIC uint32_t countInteractions(const std::string& accountId, - const std::string& conversationId, - const std::string& toId, - const std::string& fromId, - const std::string& authorUri); + const std::string& conversationId, + const std::string& toId, + const std::string& fromId, + const std::string& authorUri); LIBJAMI_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); + 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 LIBJAMI_PUBLIC ConversationSignal { diff --git a/src/jamidht/conversation.cpp b/src/jamidht/conversation.cpp index 2c67d01ce245ab397f753f0968717eda6c98e613..d6c43c91b89977295ca1a0deab0300186b4b8778 100644 --- a/src/jamidht/conversation.cpp +++ b/src/jamidht/conversation.cpp @@ -28,7 +28,6 @@ #include <json/json.h> #include <string_view> #include <opendht/thread_pool.h> -#include <tuple> #ifdef ENABLE_PLUGIN #include "manager.h" @@ -154,6 +153,12 @@ public: throw std::logic_error("Couldn't clone repository"); } init(); + // To get current active calls from previous commit, we need to read the history + auto convCommits = loadMessages({}); + std::reverse(std::begin(convCommits), std::end(convCommits)); + for (const auto& c : convCommits) { + updateActiveCalls(c); + } } void init() @@ -171,17 +176,31 @@ public: + ConversationMapKeys::LAST_DISPLAYED; preferencesPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + ConversationMapKeys::PREFERENCES; + activeCallsPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + + ConversationMapKeys::ACTIVE_CALLS; + hostedCallsPath_ = conversationDataPath_ + DIR_SEPARATOR_STR + + ConversationMapKeys::HOSTED_CALLS; loadFetched(); loadSending(); loadLastDisplayed(); } } + /** + * If, for whatever reason, the daemon is stopped while hosting a conference, + * we need to announce the end of this call when restarting. + * To avoid to keep active calls forever. + */ + std::vector<std::string> announceEndedCalls(); + ~Impl() = default; + std::vector<std::string> refreshActiveCalls(); bool isAdmin() const; std::string repoPath() const; + // Protect against parallel commits in the repo + // As the index can add files to the commit we want. std::mutex writeMtx_ {}; void announce(const std::string& commitId) const { @@ -204,6 +223,100 @@ public: announce(repository_->convCommitToMap(convcommits)); } + /** + * Update activeCalls_ via announced commits (in load or via new commits) + * @param commit Commit to check + * @param eraseOnly If we want to ignore added commits + * @note eraseOnly is used by loadMessages. This is a fail-safe, this SHOULD NOT happen + */ + void updateActiveCalls(const std::map<std::string, std::string>& commit, + bool eraseOnly = false) const + { + if (!repository_) + return; + if (commit.at("type") == "member") { + // In this case, we need to check if we are not removing a hosting member or device + std::lock_guard<std::mutex> lk(activeCallsMtx_); + auto it = activeCalls_.begin(); + auto updateActives = false; + while (it != activeCalls_.end()) { + if (it->at("uri") == commit.at("uri") || it->at("device") == commit.at("uri")) { + JAMI_DEBUG("Removing {:s} from the active calls, because {:s} left", + it->at("id"), + commit.at("uri")); + it = activeCalls_.erase(it); + updateActives = true; + } else { + ++it; + } + } + if (updateActives) { + saveActiveCalls(); + emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_, + repository_->id(), + activeCalls_); + } + return; + } + // Else, it's a call information + if (commit.find("confId") != commit.end() && commit.find("uri") != commit.end() + && commit.find("device") != commit.end()) { + auto convId = repository_->id(); + auto confId = commit.at("confId"); + auto uri = commit.at("uri"); + auto device = commit.at("device"); + if (commit.find("duration") == commit.end()) { + if (!eraseOnly) { + JAMI_DEBUG( + "swarm:{:s} new current call detected: {:s} on device {:s}, account {:s}", + convId, + confId, + device, + uri); + std::lock_guard<std::mutex> lk(activeCallsMtx_); + std::map<std::string, std::string> activeCall; + activeCall["id"] = confId; + activeCall["uri"] = uri; + activeCall["device"] = device; + activeCalls_.emplace_back(activeCall); + saveActiveCalls(); + emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_, + repository_->id(), + activeCalls_); + } + } else { + std::lock_guard<std::mutex> lk(activeCallsMtx_); + auto itActive = std::find_if(activeCalls_.begin(), + activeCalls_.end(), + [&](auto value) { + return value["id"] == confId && value["uri"] == uri + && value["device"] == device; + }); + if (itActive != activeCalls_.end()) { + activeCalls_.erase(itActive); + if (eraseOnly) { + JAMI_WARNING("previous swarm:{:s} call finished detected: {:s} on device " + "{:s}, account {:s}", + convId, + confId, + device, + uri); + } else { + JAMI_DEBUG("swarm:{:s} call finished: {:s} on device {:s}, account {:s}", + convId, + confId, + device, + uri); + } + } + saveActiveCalls(); + emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_, + repository_->id(), + activeCalls_); + } + } + } + void announce(const std::vector<std::map<std::string, std::string>>& commits) const { auto shared = account_.lock(); @@ -231,14 +344,18 @@ public: action = 3; else if (actionStr == "unban") action = 4; + if (actionStr == "ban" || actionStr == "remove") { + // In this case, a potential host was removed during a call. + updateActiveCalls(c); + } if (action != -1) { announceMember = true; - emitSignal<libjami::ConversationSignal::ConversationMemberEvent>(accountId_, - convId, - uri, - action); + emitSignal<libjami::ConversationSignal::ConversationMemberEvent>( + accountId_, convId, uri, action); } } + } else if (c.at("type") == "application/call-history+json") { + updateActiveCalls(c); } #ifdef ENABLE_PLUGIN auto& pluginChatManager @@ -348,6 +465,46 @@ public: msgpack::pack(file, lastDisplayed_); } + void loadActiveCalls() const + { + try { + // read file + auto file = fileutils::loadFile(activeCallsPath_); + // load values + msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); + std::lock_guard<std::mutex> lk {activeCallsMtx_}; + oh.get().convert(activeCalls_); + } catch (const std::exception& e) { + return; + } + } + + void saveActiveCalls() const + { + std::ofstream file(activeCallsPath_, std::ios::trunc | std::ios::binary); + msgpack::pack(file, activeCalls_); + } + + void loadHostedCalls() const + { + try { + // read file + auto file = fileutils::loadFile(hostedCallsPath_); + // load values + msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size()); + std::lock_guard<std::mutex> lk {hostedCallsMtx_}; + oh.get().convert(hostedCalls_); + } catch (const std::exception& e) { + return; + } + } + + void saveHostedCalls() const + { + std::ofstream file(hostedCallsPath_, std::ios::trunc | std::ios::binary); + msgpack::pack(file, hostedCalls_); + } + void voteUnban(const std::string& contactUri, const std::string& type, const OnDoneCb& cb); std::string bannedType(const std::string& uri) const @@ -374,6 +531,7 @@ public: void pull(); std::vector<std::map<std::string, std::string>> mergeHistory(const std::string& uri); + // Avoid multiple fetch/merges at the same time. std::mutex pullcbsMtx_ {}; std::set<std::string> fetchingRemotes_ {}; // store current remote in fetch std::deque<std::tuple<std::string, std::string, OnPullCb>> pullcbs_ {}; @@ -392,6 +550,15 @@ public: mutable std::mutex lastDisplayedMtx_ {}; // for lastDisplayed_ mutable std::map<std::string, std::string> lastDisplayed_ {}; std::function<void(const std::string&, const std::string&)> lastDisplayedUpdatedCb_ {}; + + // Manage hosted calls on this device + std::string hostedCallsPath_ {}; + mutable std::mutex hostedCallsMtx_ {}; + mutable std::map<std::string, uint64_t /* start time */> hostedCalls_ {}; + // Manage active calls for this conversation (can be hosted by other devices) + std::string activeCallsPath_ {}; + mutable std::mutex activeCallsMtx_ {}; + mutable std::vector<std::map<std::string, std::string>> activeCalls_ {}; }; bool @@ -409,6 +576,65 @@ Conversation::Impl::isAdmin() const return fileutils::isFile(fileutils::getFullPath(adminsPath, uri + ".crt")); } +std::vector<std::string> +Conversation::Impl::refreshActiveCalls() +{ + loadActiveCalls(); + loadHostedCalls(); + return announceEndedCalls(); +} + +std::vector<std::string> +Conversation::Impl::announceEndedCalls() +{ + auto shared = account_.lock(); + // Handle current calls + std::vector<std::string> commits {}; + std::unique_lock<std::mutex> lk(writeMtx_); + std::unique_lock<std::mutex> lkA(activeCallsMtx_); + for (const auto& hostedCall : hostedCalls_) { + // In this case, this means that we left + // the conference while still hosting it, so activeCalls + // will not be correctly updated + // We don't need to send notifications there, as peers will sync with presence + Json::Value value; + auto uri = shared->getUsername(); + auto device = std::string(shared->currentDeviceId()); + value["uri"] = uri; + value["device"] = device; + value["confId"] = hostedCall.first; + value["type"] = "application/call-history+json"; + auto now = std::chrono::system_clock::now(); + auto nowConverted = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()) + .count(); + value["duration"] = std::to_string((nowConverted - hostedCall.second) * 1000); + Json::StreamWriterBuilder wbuilder; + auto itActive = std::find_if(activeCalls_.begin(), + activeCalls_.end(), + [confId = hostedCall.first, uri, device](auto value) { + return value.at("id") == confId && value.at("uri") == uri + && value.at("device") == device; + }); + if (itActive != activeCalls_.end()) + activeCalls_.erase(itActive); + wbuilder["commentStyle"] = "None"; + wbuilder["indentation"] = ""; + auto commit = repository_->commitMessage(Json::writeString(wbuilder, value)); + commits.emplace_back(commit); + + JAMI_DEBUG("Removing hosted conference... {:s}", hostedCall.first); + } + hostedCalls_.clear(); + saveActiveCalls(); + saveHostedCalls(); + lkA.unlock(); + lk.unlock(); + if (!commits.empty()) { + announce(commits); + } + return commits; +} + std::string Conversation::Impl::repoPath() const { @@ -842,7 +1068,7 @@ Conversation::Impl::mergeHistory(const std::string& uri) newCommits.emplace_back(*commit); } - JAMI_DBG("Successfully merge history with %s", uri.c_str()); + JAMI_DEBUG("Successfully merge history with {:s}", uri); auto result = repository_->convCommitToMap(newCommits); for (const auto& commit : result) { auto it = commit.find("type"); @@ -1092,8 +1318,8 @@ Conversation::updatePreferences(const std::map<std::string, std::string>& map) std::ofstream file(filePath, std::ios::trunc | std::ios::binary); msgpack::pack(file, prefs); emitSignal<libjami::ConversationSignal::ConversationPreferencesUpdated>(pimpl_->accountId_, - id(), - std::move(prefs)); + id(), + std::move(prefs)); } std::map<std::string, std::string> @@ -1155,20 +1381,21 @@ Conversation::onFileChannelRequest(const std::string& member, if (fileutils::isSymLink(path)) { fileutils::remove(path, true); } - JAMI_DBG("[Account %s] %s asked for non existing file %s in %s", - pimpl_->accountId_.c_str(), - member.c_str(), - fileId.c_str(), - id().c_str()); + JAMI_DEBUG("[Account {:s}] {:s} asked for non existing file {:s} in {:s}", + pimpl_->accountId_, + member, + fileId, + id()); return false; } // 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", - pimpl_->accountId_.c_str(), - member.c_str(), - fileId.c_str(), - id().c_str()); + JAMI_DEBUG( + "[Account {:s}] {:s} asked for file {:s} in {:s}, but our version is not complete", + pimpl_->accountId_, + member, + fileId, + id()); return false; } return true; @@ -1329,6 +1556,12 @@ Conversation::updateLastDisplayed(const std::string& lastDisplayed) updateLastDisplayed(); } +std::vector<std::string> +Conversation::refreshActiveCalls() +{ + return pimpl_->refreshActiveCalls(); +} + void Conversation::onLastDisplayedUpdated( std::function<void(const std::string&, const std::string&)>&& lastDisplayedUpdatedCb) @@ -1366,9 +1599,9 @@ Conversation::search(uint32_t req, auto commits = sthis->pimpl_->repository_->search(filter); if (commits.size() > 0) emitSignal<libjami::ConversationSignal::MessagesFound>(req, - acc->getAccountID(), - sthis->id(), - std::move(commits)); + 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<libjami::ConversationSignal::MessagesFound>( @@ -1381,4 +1614,63 @@ Conversation::search(uint32_t req, }); } +void +Conversation::hostConference(Json::Value&& message, OnDoneCb&& cb) +{ + if (!message.isMember("confId")) { + JAMI_ERR() << "Malformed commit"; + return; + } + + auto now = std::chrono::system_clock::now(); + auto nowSecs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count(); + { + std::lock_guard<std::mutex> lk(pimpl_->hostedCallsMtx_); + pimpl_->hostedCalls_[message["confId"].asString()] = nowSecs; + pimpl_->saveHostedCalls(); + } + + sendMessage(std::move(message), "", {}, std::move(cb)); +} + +bool +Conversation::isHosting(const std::string& confId) const +{ + auto shared = pimpl_->account_.lock(); + if (!shared) + return false; + auto info = infos(); + if (info["rdvDevice"] == shared->currentDeviceId() && info["rdvHost"] == shared->getUsername()) + return true; // We are the current device Host + std::lock_guard<std::mutex> lk(pimpl_->hostedCallsMtx_); + return pimpl_->hostedCalls_.find(confId) != pimpl_->hostedCalls_.end(); +} + +void +Conversation::removeActiveConference(Json::Value&& message, OnDoneCb&& cb) +{ + if (!message.isMember("confId")) { + JAMI_ERR() << "Malformed commit"; + return; + } + + auto erased = false; + { + std::lock_guard<std::mutex> lk(pimpl_->hostedCallsMtx_); + erased = pimpl_->hostedCalls_.erase(message["confId"].asString()); + } + if (erased) { + pimpl_->saveHostedCalls(); + sendMessage(std::move(message), "", {}, std::move(cb)); + } else + cb(false, ""); +} + +std::vector<std::map<std::string, std::string>> +Conversation::currentCalls() const +{ + std::lock_guard<std::mutex> lk(pimpl_->activeCallsMtx_); + return pimpl_->activeCalls_; +} + } // namespace jami diff --git a/src/jamidht/conversation.h b/src/jamidht/conversation.h index 29b0ee0e93fd9067249d80112d4cff6b988b2fa0..d5114d232a295554815f8a6935cc93d7ab1af0ae 100644 --- a/src/jamidht/conversation.h +++ b/src/jamidht/conversation.h @@ -26,6 +26,7 @@ #include <memory> #include <json/json.h> #include <msgpack.hpp> +#include <set> #include "jamidht/conversationrepository.h" #include "jami/datatransfer_interface.h" @@ -41,6 +42,8 @@ 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* ACTIVE_CALLS = "activeCalls"; +static constexpr const char* HOSTED_CALLS = "hostedCalls"; static constexpr const char* CACHED = "cached"; static constexpr const char* RECEIVED = "received"; static constexpr const char* DECLINED = "declined"; @@ -122,6 +125,14 @@ public: const std::string& conversationId); ~Conversation(); + /** + * Refresh active calls. + * @note: If the host crash during a call, when initializing, we need to update + * and commit all the crashed calls + * @return Commits added + */ + std::vector<std::string> refreshActiveCalls(); + /** * Add a callback to update upper layers * @note to call after the construction (and before ConversationReady) @@ -385,6 +396,33 @@ public: void search(uint32_t req, const Filter& filter, const std::shared_ptr<std::atomic_int>& flag) const; + /** + * Host a conference in the conversation + * @note the message must have "confId" + * @note Update hostedCalls_ and commit in the conversation + * @param message message to commit + * @param cb callback triggered when committed + */ + void hostConference(Json::Value&& message, OnDoneCb&& cb = {}); + /** + * Announce the end of a call + * @note the message must have "confId" + * @note called when conference is finished + * @param message message to commit + * @param cb callback triggered when committed + */ + void removeActiveConference(Json::Value&& message, OnDoneCb&& cb = {}); + /** + * Check if we're currently hosting this conference + * @param confId + * @return true if hosting + */ + bool isHosting(const std::string& confId) const; + /** + * Return current detected calls + * @return a vector of map with the following keys: "id", "uri", "device" + */ + std::vector<std::map<std::string, std::string>> currentCalls() const; private: std::shared_ptr<Conversation> shared() diff --git a/src/jamidht/conversation_module.cpp b/src/jamidht/conversation_module.cpp index 610014e210d7acc36d8532d2ce8b457830b4fe90..14611bbca971fd88540df5e082000a0a019eb9e0 100644 --- a/src/jamidht/conversation_module.cpp +++ b/src/jamidht/conversation_module.cpp @@ -25,11 +25,13 @@ #include <opendht/thread_pool.h> #include "account_const.h" +#include "call.h" #include "client/ring_signal.h" #include "fileutils.h" #include "jamidht/account_manager.h" #include "jamidht/jamiaccount.h" #include "manager.h" +#include "sip/sipcall.h" #include "vcard.h" namespace jami { @@ -987,6 +989,13 @@ ConversationModule::loadConversations() info.lastDisplayed = conv->infos()[ConversationMapKeys::LAST_DISPLAYED]; addConvInfo(info); } + auto commits = conv->refreshActiveCalls(); + if (!commits.empty()) { + // Note: here, this means that some calls were actives while the + // daemon finished (can be a crash). + // Notify other in the conversation that the call is finished + pimpl_->sendMessageNotification(*conv, *commits.rbegin(), true); + } pimpl_->conversations_.emplace(repository, std::move(conv)); } catch (const std::logic_error& e) { JAMI_WARN("[Account %s] Conversations not loaded : %s", @@ -1017,7 +1026,7 @@ ConversationModule::loadConversations() pimpl_->accountId_, info.id); emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, - info.id); + info.id); itInfo = pimpl_->convInfos_.erase(itInfo); continue; } @@ -1120,10 +1129,10 @@ ConversationModule::onTrustRequest(const std::string& uri, return; } emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(pimpl_->accountId_, - conversationId, - uri, - payload, - received); + conversationId, + uri, + payload, + received); ConversationRequest req; req.from = uri; req.conversationId = conversationId; @@ -1133,8 +1142,8 @@ ConversationModule::onTrustRequest(const std::string& uri, auto reqMap = req.toMap(); pimpl_->addConversationRequest(conversationId, std::move(req)); emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, - conversationId, - reqMap); + conversationId, + reqMap); } void @@ -1161,8 +1170,8 @@ ConversationModule::onConversationRequest(const std::string& from, const Json::V // the same conversation request. Will sync when the conversation will be added emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, - convId, - reqMap); + convId, + reqMap); } void @@ -1215,7 +1224,7 @@ ConversationModule::declineConversationRequest(const std::string& conversationId pimpl_->saveConvRequests(); } emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, - conversationId); + conversationId); pimpl_->needsSyncingCb_({}); } @@ -1402,9 +1411,9 @@ ConversationModule::loadConversationMessages(const std::string& conversationId, conversation->second->loadMessages( [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) { emitSignal<libjami::ConversationSignal::ConversationLoaded>(id, - accountId, - conversationId, - messages); + accountId, + conversationId, + messages); }, options); return id; @@ -1429,9 +1438,9 @@ ConversationModule::loadConversationUntil(const std::string& conversationId, conversation->second->loadMessages( [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) { emitSignal<libjami::ConversationSignal::ConversationLoaded>(id, - accountId, - conversationId, - messages); + accountId, + conversationId, + messages); }, options); return id; @@ -1538,7 +1547,7 @@ ConversationModule::onSyncData(const SyncMsg& msg, auto itConv = pimpl_->conversations_.find(convId); if (itConv != pimpl_->conversations_.end() && !itConv->second->isRemoving()) { emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, - convId); + convId); itConv->second->setRemovingFlag(); } } @@ -1575,7 +1584,7 @@ ConversationModule::onSyncData(const SyncMsg& msg, convId.c_str(), deviceId.c_str()); emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, - convId); + convId); continue; } @@ -1585,8 +1594,8 @@ ConversationModule::onSyncData(const SyncMsg& msg, deviceId.c_str()); emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, - convId, - req.toMap()); + convId, + req.toMap()); } // Updates preferences for conversations @@ -1897,8 +1906,8 @@ ConversationModule::removeContact(const std::string& uri, bool banned) auto it = pimpl_->conversationsRequests_.begin(); while (it != pimpl_->conversationsRequests_.end()) { if (it->second.from == uri) { - emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, - it->first); + emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>( + pimpl_->accountId_, it->first); update = true; it = pimpl_->conversationsRequests_.erase(it); } else { @@ -1989,6 +1998,245 @@ ConversationModule::initReplay(const std::string& oldConvId, const std::string& } } +bool +ConversationModule::isHosting(const std::string& conversationId, const std::string& confId) const +{ + std::lock_guard<std::mutex> lk(pimpl_->conversationsMtx_); + if (conversationId.empty()) { + return std::find_if(pimpl_->conversations_.cbegin(), + pimpl_->conversations_.cend(), + [&](const auto& conv) { return conv.second->isHosting(confId); }) + != pimpl_->conversations_.cend(); + } else { + auto conversation = pimpl_->conversations_.find(conversationId); + if (conversation != pimpl_->conversations_.end() && conversation->second) { + return conversation->second->isHosting(confId); + } + } + return false; +} + +std::vector<std::map<std::string, std::string>> +ConversationModule::getActiveCalls(const std::string& conversationId) const +{ + std::unique_lock<std::mutex> lk(pimpl_->conversationsMtx_); + auto conversation = pimpl_->conversations_.find(conversationId); + if (conversation == pimpl_->conversations_.end() || !conversation->second) { + JAMI_ERR("Conversation %s not found", conversationId.c_str()); + return {}; + } + return conversation->second->currentCalls(); +} + +void +ConversationModule::call(const std::string& url, + const std::shared_ptr<SIPCall>& call, + std::function<void(const std::string&, const DeviceId&)>&& cb) +{ + std::string conversationId = "", confId = "", uri = "", deviceId = ""; + if (url.find('/') == std::string::npos) { + conversationId = url; + } else { + auto parameters = jami::split_string(url, '/'); + if (parameters.size() != 4) { + JAMI_ERR("Incorrect url %s", url.c_str()); + return; + } + conversationId = parameters[0]; + JAMI_ERR("@@@ %s", conversationId.c_str()); + uri = parameters[1]; + JAMI_ERR("@@@ %s", uri.c_str()); + deviceId = parameters[2]; + JAMI_ERR("@@@ %s", deviceId.c_str()); + confId = parameters[3]; + JAMI_ERR("@@@ %s", confId.c_str()); + } + + std::string callUri; + auto sendCall = [&]() { + call->setState(Call::ConnectionState::TRYING); + call->setPeerNumber(callUri); + call->setPeerUri("rdv:" + callUri); + call->addStateListener([w = pimpl_->weak(), conversationId](Call::CallState call_state, + Call::ConnectionState cnx_state, + int) { + if (cnx_state == Call::ConnectionState::DISCONNECTED + && call_state == Call::CallState::MERROR) { + auto shared = w.lock(); + if (!shared) + return false; + if (auto acc = shared->account_.lock()) + emitSignal<libjami::ConfigurationSignal::NeedsHost>(acc->getAccountID(), + conversationId); + return true; + } + return true; + }); + cb(callUri, DeviceId(deviceId)); + }; + + std::unique_lock<std::mutex> lk(pimpl_->conversationsMtx_); + auto conversation = pimpl_->conversations_.find(conversationId); + if (conversation == pimpl_->conversations_.end() || !conversation->second) { + JAMI_ERR("Conversation %s not found", conversationId.c_str()); + return; + } + + // Check if we want to join a specific conference + // So, if confId is specified or if there is some activeCalls + // or if we are the default host. + auto& conv = conversation->second; + auto activeCalls = conv->currentCalls(); + auto infos = conv->infos(); + auto itRdvAccount = infos.find("rdvAccount"); + auto itRdvDevice = infos.find("rdvDevice"); + auto sendCallRequest = false; + if (confId != "") { + sendCallRequest = true; + confId = confId == "0" ? Manager::instance().callFactory.getNewCallID() : confId; + JAMI_DBG("Calling self, join conference"); + } else if (!activeCalls.empty()) { + // Else, we try to join active calls + sendCallRequest = true; + auto& ac = *activeCalls.rbegin(); + confId = ac.at("id"); + uri = ac.at("uri"); + deviceId = ac.at("device"); + JAMI_DBG("Calling last active call: %s", callUri.c_str()); + } else if (itRdvAccount != infos.end() && itRdvDevice != infos.end()) { + // Else, creates "to" (accountId/deviceId/conversationId/confId) and ask remote host + sendCallRequest = true; + uri = itRdvAccount->second; + deviceId = itRdvDevice->second; + confId = call->getCallId(); + JAMI_DBG("Remote host detected. Calling %s on device %s", uri.c_str(), deviceId.c_str()); + } + + if (sendCallRequest) { + callUri = fmt::format("{}/{}/{}/{}", conversationId, uri, deviceId, confId); + if (uri == pimpl_->username_ && deviceId == pimpl_->deviceId_) { + // In this case, we're probably hosting the conference. + call->setState(Call::ConnectionState::CONNECTED); + // In this case, the call is the only one in the conference + // and there is no peer, so media succeeded and are shown to + // the client. + call->reportMediaNegotiationStatus(); + lk.unlock(); + hostConference(conversationId, confId, call->getCallId()); + return; + } + JAMI_DBG("Calling: %s", callUri.c_str()); + sendCall(); + return; + } + + // Else, we are the host. + confId = Manager::instance().callFactory.getNewCallID(); + call->setState(Call::ConnectionState::CONNECTED); + // In this case, the call is the only one in the conference + // and there is no peer, so media succeeded and are shown to + // the client. + call->reportMediaNegotiationStatus(); + lk.unlock(); + hostConference(conversationId, confId, call->getCallId()); +} + +void +ConversationModule::hostConference(const std::string& conversationId, + const std::string& confId, + const std::string& callId) +{ + auto acc = pimpl_->account_.lock(); + if (!acc) + return; + std::shared_ptr<Call> call; + call = acc->getCall(callId); + if (!call) { + JAMI_WARN("No call with id %s found", callId.c_str()); + return; + } + auto conf = acc->getConference(confId); + auto createConf = !conf; + if (createConf) { + conf = std::make_shared<Conference>(acc, confId); + acc->attach(conf); + } + conf->addParticipant(callId); + + if (createConf) { + emitSignal<libjami::CallSignal::ConferenceCreated>(acc->getAccountID(), confId); + } else { + conf->attachLocalParticipant(); + conf->reportMediaNegotiationStatus(); + emitSignal<libjami::CallSignal::ConferenceChanged>(acc->getAccountID(), + conf->getConfId(), + conf->getStateStr()); + return; + } + + std::unique_lock<std::mutex> lk(pimpl_->conversationsMtx_); + auto conversation = pimpl_->conversations_.find(conversationId); + if (conversation == pimpl_->conversations_.end() || !conversation->second) { + JAMI_ERR("Conversation %s not found", conversationId.c_str()); + return; + } + auto& conv = conversation->second; + // Add commit to conversation + Json::Value value; + value["uri"] = pimpl_->username_; + value["device"] = pimpl_->deviceId_; + value["confId"] = confId; + value["type"] = "application/call-history+json"; + conv->hostConference(std::move(value), + std::move([w = pimpl_->weak(), + conversationId](bool ok, const std::string& commitId) { + if (ok) { + if (auto shared = w.lock()) + shared->sendMessageNotification(conversationId, commitId, true); + } else { + JAMI_ERR("Failed to send message to conversation %s", + conversationId.c_str()); + } + })); + + // When conf finished = remove host & commit + // Master call, so when it's stopped, the conference will be stopped (as we use the hold state + // for detaching the call) + conf->onShutdown( + [w = pimpl_->weak(), accountUri = pimpl_->username_, confId, conversationId, call]( + int duration) { + auto shared = w.lock(); + if (shared) { + Json::Value value; + value["uri"] = accountUri; + value["device"] = shared->deviceId_; + value["confId"] = confId; + value["type"] = "application/call-history+json"; + value["duration"] = std::to_string(duration); + + std::unique_lock<std::mutex> lk(shared->conversationsMtx_); + auto conversation = shared->conversations_.find(conversationId); + if (conversation == shared->conversations_.end() || !conversation->second) { + JAMI_ERR("Conversation %s not found", conversationId.c_str()); + return true; + } + auto& conv = conversation->second; + conv->removeActiveConference( + std::move(value), [w, conversationId](bool ok, const std::string& commitId) { + if (ok) { + if (auto shared = w.lock()) { + shared->sendMessageNotification(conversationId, commitId, true); + } + } else { + JAMI_ERR("Failed to send message to conversation %s", + conversationId.c_str()); + } + }); + } + return true; + }); +} + std::map<std::string, ConvInfo> ConversationModule::convInfos(const std::string& accountId) { diff --git a/src/jamidht/conversation_module.h b/src/jamidht/conversation_module.h index 7dee281aaf2b7785abb69cb6434e9070da5b6d97..c45f8a57c0e132fc25cca710da496fd570f434dd 100644 --- a/src/jamidht/conversation_module.h +++ b/src/jamidht/conversation_module.h @@ -21,6 +21,7 @@ #pragma once #include "scheduled_executor.h" +#include "jamidht/account_manager.h" #include "jamidht/conversation.h" #include "jamidht/conversationrepository.h" #include "jamidht/jami_contact.h" @@ -30,6 +31,8 @@ namespace jami { +class SIPCall; + struct SyncMsg { jami::DeviceSync ds; @@ -42,7 +45,8 @@ struct SyncMsg }; using ChannelCb = std::function<bool(const std::shared_ptr<ChannelSocket>&)>; -using NeedSocketCb = std::function<void(const std::string&, const std::string&, ChannelCb&&, const std::string&)>; +using NeedSocketCb + = std::function<void(const std::string&, const std::string&, ChannelCb&&, const std::string&)>; using SengMsgCb = std::function<uint64_t(const std::string&, std::map<std::string, std::string>, uint64_t)>; using NeedsSyncingCb = std::function<void(std::shared_ptr<SyncMsg>&&)>; @@ -365,6 +369,32 @@ public: */ bool removeConversation(const std::string& conversationId); void initReplay(const std::string& oldConvId, const std::string& newConvId); + /** + * Check if we're hosting a specific conference + * @param conversationId (empty to search all conv) + * @param confId + * @return true if hosting this conference + */ + bool isHosting(const std::string& conversationId, const std::string& confId) const; + /** + * Return active calls + * @param convId Which conversation to choose + * @return {{"id":id}, {"uri":uri}, {"device":device}} + */ + std::vector<std::map<std::string, std::string>> getActiveCalls( + const std::string& conversationId) const; + /** + * Call the conversation + * @param url Url to call (swarm:conversation or swarm:conv/account/device/conf to join) + * @param call Call to use + * @param cb Callback to pass which device to call (called in the same thread) + */ + void call(const std::string& url, + const std::shared_ptr<SIPCall>& call, + std::function<void(const std::string&, const DeviceId&)>&& cb); + void hostConference(const std::string& conversationId, + const std::string& confId, + const std::string& callId); // The following methods modify what is stored on the disk static void saveConvInfos(const std::string& accountId, diff --git a/src/jamidht/conversationrepository.cpp b/src/jamidht/conversationrepository.cpp index b36f46f0d7d5b4fc71bf8705116b445576d80664..da0579f68517f09f514c1ee016f7826175cd0fbf 100644 --- a/src/jamidht/conversationrepository.cpp +++ b/src/jamidht/conversationrepository.cpp @@ -1749,9 +1749,9 @@ ConversationRepository::Impl::mode() const if (lastMsg.size() == 0) { if (auto shared = account_.lock()) { emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EINVALIDMODE, - "No initial commit"); + id_, + EINVALIDMODE, + "No initial commit"); } throw std::logic_error("Can't retrieve first commit"); } @@ -1764,18 +1764,18 @@ ConversationRepository::Impl::mode() const if (!reader->parse(commitMsg.data(), commitMsg.data() + commitMsg.size(), &root, &err)) { if (auto shared = account_.lock()) { emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EINVALIDMODE, - "No initial commit"); + id_, + EINVALIDMODE, + "No initial commit"); } throw std::logic_error("Can't retrieve first commit"); } if (!root.isMember("mode")) { if (auto shared = account_.lock()) { emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EINVALIDMODE, - "No mode detected"); + id_, + EINVALIDMODE, + "No mode detected"); } throw std::logic_error("No mode detected for initial commit"); } @@ -1797,9 +1797,9 @@ ConversationRepository::Impl::mode() const default: if (auto shared = account_.lock()) { emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EINVALIDMODE, - "Incorrect mode detected"); + id_, + EINVALIDMODE, + "Incorrect mode detected"); } throw std::logic_error("Incorrect mode detected"); } @@ -2715,10 +2715,8 @@ ConversationRepository::Impl::validCommits( validUserAtCommit.c_str(), commit.commit_msg.c_str()); if (auto shared = account_.lock()) { - emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EVALIDFETCH, - "Malformed commit"); + emitSignal<libjami::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed commit"); } return false; } @@ -2730,10 +2728,8 @@ ConversationRepository::Impl::validCommits( "that your contact is not doing unwanted stuff.", validUserAtCommit.c_str()); if (auto shared = account_.lock()) { - emitSignal<libjami::ConversationSignal::OnConversationError>(shared->getAccountID(), - id_, - EVALIDFETCH, - "Malformed commit"); + emitSignal<libjami::ConversationSignal::OnConversationError>( + shared->getAccountID(), id_, EVALIDFETCH, "Malformed commit"); } return false; } @@ -3248,8 +3244,8 @@ ConversationRepository::leave() auto uri = details[libjami::Account::ConfProperties::USERNAME]; auto name = details[libjami::Account::ConfProperties::DISPLAYNAME]; if (name.empty()) - name = account - ->getVolatileAccountDetails()[libjami::Account::VolatileProperties::REGISTERED_NAME]; + name = account->getVolatileAccountDetails() + [libjami::Account::VolatileProperties::REGISTERED_NAME]; if (name.empty()) name = deviceId; @@ -3706,35 +3702,38 @@ ConversationRepository::updateInfos(const std::map<std::string, std::string>& pr JAMI_ERR("Could not write data to %s", profilePath.c_str()); return {}; } + + auto addKey = [&](auto property, auto key) { + auto it = infosMap.find(key); + if (it != infosMap.end()) { + file << property; + file << ":"; + file << it->second; + file << vCard::Delimiter::END_LINE_TOKEN; + } + }; + file << vCard::Delimiter::BEGIN_TOKEN; file << vCard::Delimiter::END_LINE_TOKEN; file << vCard::Property::VCARD_VERSION; file << ":2.1"; file << vCard::Delimiter::END_LINE_TOKEN; - auto titleIt = infosMap.find("title"); - if (titleIt != infosMap.end()) { - file << vCard::Property::FORMATTED_NAME; - file << ":"; - file << titleIt->second; - file << vCard::Delimiter::END_LINE_TOKEN; - } - auto descriptionIt = infosMap.find("description"); - if (descriptionIt != infosMap.end()) { - file << vCard::Property::DESCRIPTION; - file << ":"; - file << descriptionIt->second; - file << vCard::Delimiter::END_LINE_TOKEN; - } + addKey(vCard::Property::FORMATTED_NAME, vCard::Value::TITLE); + addKey(vCard::Property::DESCRIPTION, vCard::Value::DESCRIPTION); file << vCard::Property::PHOTO; file << vCard::Delimiter::SEPARATOR_TOKEN; file << vCard::Property::BASE64; - auto avatarIt = infosMap.find("avatar"); + auto avatarIt = infosMap.find(vCard::Value::AVATAR); if (avatarIt != infosMap.end()) { // TODO type=png? store another way? file << ":"; file << avatarIt->second; } file << vCard::Delimiter::END_LINE_TOKEN; + addKey(vCard::Property::RDV_ACCOUNT, vCard::Value::RDV_ACCOUNT); + file << vCard::Delimiter::END_LINE_TOKEN; + addKey(vCard::Property::RDV_DEVICE, vCard::Value::RDV_DEVICE); + file << vCard::Delimiter::END_LINE_TOKEN; file << vCard::Delimiter::END_TOKEN; file.close(); @@ -3780,6 +3779,10 @@ ConversationRepository::infosFromVCard(std::map<std::string, std::string>&& deta result["description"] = std::move(v); } else if (k.find(vCard::Property::PHOTO) == 0) { result["avatar"] = std::move(v); + } else if (k.find(vCard::Property::RDV_ACCOUNT) == 0) { + result["rdvAccount"] = std::move(v); + } else if (k.find(vCard::Property::RDV_DEVICE) == 0) { + result["rdvDevice"] = std::move(v); } } return result; diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index f617bd91266cf7e1fd8252d4ac62fefc4b3505e4..8ef6922fdbf08a01ac7095005cdfbcce643ea666 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -365,7 +365,7 @@ JamiAccount::newIncomingCall(const std::string& from, auto call = Manager::instance().callFactory.newSipCall(shared(), Call::CallType::INCOMING, mediaList); - call->setPeerUri(RING_URI_PREFIX + from); + call->setPeerUri(JAMI_URI_PREFIX + from); call->setPeerNumber(from); call->setSipTransport(sipTransp, getContactHeader(sipTransp)); @@ -383,9 +383,6 @@ JamiAccount::newIncomingCall(const std::string& from, std::shared_ptr<Call> JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::MediaMap>& mediaList) { - auto suffix = stripPrefix(toUrl); - JAMI_DBG() << *this << "Calling peer " << suffix; - auto& manager = Manager::instance(); std::shared_ptr<SIPCall> call; @@ -403,7 +400,8 @@ JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami:: if (not call) return {}; - getIceOptions([call, w = weak(), uri = std::string(toUrl)](auto&& opts) { + auto uri = Uri(toUrl); + getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) { if (call->isIceEnabled()) { if (not call->createIceMediaTransport(false) or not call->initIceMediaTransport(true, std::forward<IceTransportOptions>(opts))) { @@ -413,23 +411,28 @@ JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami:: auto shared = w.lock(); if (!shared) return; - shared->newOutgoingCallHelper(call, uri); + JAMI_DBG() << "New outgoing call with " << uri.toString(); + call->setPeerNumber(uri.authority()); + call->setPeerUri(uri.toString()); + + if (uri.scheme() == Uri::Scheme::SWARM || uri.scheme() == Uri::Scheme::RENDEZVOUS) + shared->newSwarmOutgoingCallHelper(call, uri); + else + shared->newOutgoingCallHelper(call, uri); }); return call; } void -JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::string_view toUri) +JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri) { - auto suffix = stripPrefix(toUri); - JAMI_DBG() << *this << "Calling DHT peer " << suffix; - + JAMI_DBG() << this << "Calling peer " << uri.authority(); try { - const std::string uri {parseJamiUri(suffix)}; - startOutgoingCall(call, uri); + startOutgoingCall(call, uri.authority()); } catch (...) { #if HAVE_RINGNS + auto suffix = stripPrefix(uri.toString()); NameDirectory::lookupUri(suffix, config().nameServer, [wthis_ = weak(), call](const std::string& result, @@ -443,8 +446,7 @@ JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::st } if (auto sthis = wthis_.lock()) { try { - const std::string toUri {parseJamiUri(result)}; - sthis->startOutgoingCall(call, toUri); + sthis->startOutgoingCall(call, result); } catch (...) { call->onFailure(ENOENT); } @@ -459,6 +461,103 @@ JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::st } } +void +JamiAccount::newSwarmOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri) +{ + JAMI_DBG("[Account %s] Calling conversation %s", + getAccountID().c_str(), + uri.authority().c_str()); + convModule()->call( + uri.authority(), + call, + std::move([this, uri, call](const std::string& accountUri, const DeviceId& deviceId) { + std::unique_lock<std::mutex> lkSipConn(sipConnsMtx_); + for (auto& [key, value] : sipConns_) { + if (key.first != accountUri || key.second != deviceId) + continue; + if (value.empty()) + continue; + auto& sipConn = value.back(); + + if (!sipConn.channel) { + JAMI_WARN( + "A SIP transport exists without Channel, this is a bug. Please report"); + continue; + } + + auto transport = sipConn.transport; + if (!transport or !sipConn.channel) + continue; + call->setState(Call::ConnectionState::PROGRESSING); + + auto remoted_address = sipConn.channel->getRemoteAddress(); + try { + onConnectedOutgoingCall(call, uri.authority(), remoted_address); + return; + } catch (const VoipLinkException&) { + // In this case, the main scenario is that SIPStartCall failed because + // the ICE is dead and the TLS session didn't send any packet on that dead + // link (connectivity change, killed by the os, etc) + // Here, we don't need to do anything, the TLS will fail and will delete + // the cached transport + continue; + } + } + lkSipConn.unlock(); + { + std::lock_guard<std::mutex> lkP(pendingCallsMutex_); + pendingCalls_[deviceId].emplace_back(call); + } + + // Else, ask for a channel (for future calls/text messages) + auto type = call->hasVideo() ? "videoCall" : "audioCall"; + JAMI_WARN("[call %s] No channeled socket with this peer. Send request", + call->getCallId().c_str()); + requestSIPConnection(accountUri, deviceId, type, true, call); + })); +} + +void +JamiAccount::handleIncomingConversationCall(const std::string& callId, + const std::string& destination) +{ + auto split = jami::split_string(destination, '/'); + if (split.size() != 4) + return; + auto conversationId = std::string(split[0]); + auto accountUri = std::string(split[1]); + auto deviceId = std::string(split[2]); + auto confId = std::string(split[3]); + + if (getUsername() != accountUri || currentDeviceId() != deviceId) + return; + + auto call = getCall(callId); + if (!call) { + JAMI_ERR("Call %s not found", callId.c_str()); + return; + } + Manager::instance().answerCall(*call); + + if (!convModule()->isHosting(conversationId, confId)) { + // Create conference and host it. + convModule()->hostConference(conversationId, confId, callId); + if (auto conf = getConference(confId)) + conf->detachLocalParticipant(); + } else { + auto conf = getConference(confId); + if (!conf) { + JAMI_ERR("Conference %s not found", confId.c_str()); + return; + } + + conf->addParticipant(callId); + emitSignal<libjami::CallSignal::ConferenceChanged>(getAccountID(), + conf->getConfId(), + conf->getStateStr()); + } +} + std::shared_ptr<SIPCall> JamiAccount::createSubCall(const std::shared_ptr<SIPCall>& mainCall) { @@ -473,11 +572,10 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: call->onFailure(ENETDOWN); return; } + // TODO: for now, we automatically trust all explicitly called peers setCertificateStatus(toUri, tls::TrustStore::PermissionStatus::ALLOWED); - call->setPeerNumber(toUri + "@ring.dht"); - call->setPeerUri(JAMI_URI_PREFIX + toUri); call->setState(Call::ConnectionState::TRYING); std::weak_ptr<SIPCall> wCall = call; @@ -488,7 +586,6 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: if (response == NameDirectory::Response::found) if (auto call = wCall.lock()) { call->setPeerRegisteredName(result); - call->setPeerUri(JAMI_URI_PREFIX + result); } }); #endif @@ -522,7 +619,7 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: return; auto dev_call = createSubCall(call); - dev_call->setIPToIP(true); + dev_call->setPeerNumber(call->getPeerNumber()); dev_call->setState(Call::ConnectionState::TRYING); call->addStateListener( [w = weak(), deviceId](Call::CallState, Call::ConnectionState state, int) { @@ -572,6 +669,7 @@ JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std:: call->getCallId().c_str()); auto dev_call = createSubCall(call); + dev_call->setPeerNumber(call->getPeerNumber()); dev_call->setSipTransport(transport, getContactHeader(transport)); call->addSubCall(*dev_call); dev_call->setIceMedia(call->getIceMedia()); @@ -685,9 +783,6 @@ JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call, return; } - call->setIPToIP(true); - call->setPeerNumber(to_id); - // Note: pj_ice_strans_create can call onComplete in the same thread // This means that iceMutex_ in IceTransport can be locked when onInitDone is called // So, we need to run the call creation in the main thread diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index b6be21108592faf3a24d3db2b07406753aa9bb0a..f24e17e78f9574557223f8a893eb8cbcc91ec601 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -126,7 +126,8 @@ public: const std::string& getPath() const { return idPath_; } - const JamiAccountConfig& config() const { + const JamiAccountConfig& config() const + { return *static_cast<const JamiAccountConfig*>(&Account::config()); } @@ -146,7 +147,8 @@ public: */ virtual std::map<std::string, std::string> getVolatileAccountDetails() const override; - std::unique_ptr<AccountConfig> buildConfig() const override { + std::unique_ptr<AccountConfig> buildConfig() const override + { return std::make_unique<JamiAccountConfig>(getAccountID(), idPath_); } @@ -234,6 +236,10 @@ public: /** * Create outgoing SIPCall. + * @note Accepts several urls: + * + jami:uri for calling someone + * + swarm:id for calling a group (will host or join if an active call is detected) + * + rdv:id/uri/device/confId to join a specific conference hosted on (uri, device) * @param[in] toUrl The address to call * @param[in] mediaList list of medias * @return A shared pointer on the created call. @@ -420,10 +426,10 @@ public: void saveConfig() const override; - inline void editConfig(std::function<void(JamiAccountConfig& conf)>&& edit) { - Account::editConfig([&](AccountConfig& conf) { - edit(*static_cast<JamiAccountConfig*>(&conf)); - }); + inline void editConfig(std::function<void(JamiAccountConfig& conf)>&& edit) + { + Account::editConfig( + [&](AccountConfig& conf) { edit(*static_cast<JamiAccountConfig*>(&conf)); }); } /** @@ -521,8 +527,8 @@ public: // non-swarm version libjami::DataTransferId sendFile(const std::string& peer, - const std::string& path, - const InternalCompletionCb& icb = {}); + const std::string& path, + const InternalCompletionCb& icb = {}); void transferFile(const std::string& conversationId, const std::string& path, @@ -605,6 +611,13 @@ public: bool isValidAccountDevice(const dht::crypto::Certificate& cert) const; + /** + * Join incoming call to hosted conference + * @param callId The call to join + * @param destination conversation/uri/device/confId to join + */ + void handleIncomingConversationCall(const std::string& callId, const std::string& destination); + private: NON_COPYABLE(JamiAccount); @@ -706,11 +719,10 @@ private: template<class... Args> std::shared_ptr<IceTransport> createIceTransport(const Args&... args); - void newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, std::string_view toUri); + void newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri); + void newSwarmOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri); std::shared_ptr<SIPCall> createSubCall(const std::shared_ptr<SIPCall>& mainCall); - void updateContactHeader(); - #if HAVE_RINGNS std::string registeredName_; #endif diff --git a/src/manager.cpp b/src/manager.cpp index b82e9f51326f620239b46ef6d924fec065fcd75c..eaad12af7818ed9436bdb15ea8240b9fbd41aa16 100644 --- a/src/manager.cpp +++ b/src/manager.cpp @@ -561,9 +561,16 @@ Manager::ManagerPimpl::processRemainingParticipants(Conference& conf) JAMI_ERR("No account detected"); return; } + + // Stay in a conference if 1 participants for swarm and rendezvous if (account->isRendezVous()) return; + if (auto acc = std::dynamic_pointer_cast<JamiAccount>(account)) + if (acc->convModule()->isHosting("", conf.getConfId())) + return; + + // Else go in 1:1 if (current_callId != conf.getConfId()) base_.onHoldCall(account->getAccountID(), call->getCallId()); else @@ -1083,13 +1090,17 @@ Manager::answerCall(Call& call, const std::vector<libjami::MediaMap>& mediaList) // THREAD=Main bool -Manager::hangupCall(const std::string&, const std::string& callId) +Manager::hangupCall(const std::string& accountId, const std::string& callId) { + auto account = getAccount(accountId); + if (not account) + return false; + // store the current call id stopTone(); pimpl_->removeWaitingCall(callId); /* We often get here when the call was hungup before being created */ - auto call = getCallFromCallID(callId); + auto call = account->getCall(callId); if (not call) { JAMI_WARN("Could not hang up non-existant call %s", callId.c_str()); return false; @@ -1261,8 +1272,8 @@ Manager::holdConference(const std::string& accountId, const std::string& confId) if (auto conf = account->getConference(confId)) { conf->detachLocalParticipant(); emitSignal<libjami::CallSignal::ConferenceChanged>(accountId, - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); return true; } } @@ -1285,8 +1296,8 @@ Manager::unHoldConference(const std::string& accountId, const std::string& confI pimpl_->switchCall(confId); conf->setState(Conference::State::ACTIVE_ATTACHED); emitSignal<libjami::CallSignal::ConferenceChanged>(accountId, - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); return true; } else if (conf->getState() == Conference::State::ACTIVE_DETACHED) { pimpl_->addMainParticipant(*conf); @@ -1352,8 +1363,8 @@ Manager::ManagerPimpl::addMainParticipant(Conference& conf) { conf.attachLocalParticipant(); emitSignal<libjami::CallSignal::ConferenceChanged>(conf.getAccountId(), - conf.getConfId(), - conf.getStateStr()); + conf.getConfId(), + conf.getStateStr()); switchCall(conf.getConfId()); } @@ -1446,8 +1457,8 @@ Manager::joinParticipant(const std::string& accountId, conf->detachLocalParticipant(); } emitSignal<libjami::CallSignal::ConferenceChanged>(account->getAccountID(), - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); return true; } @@ -1503,8 +1514,8 @@ Manager::detachLocalParticipant(const std::shared_ptr<Conference>& conf) JAMI_INFO("Detach local participant from conference %s", conf->getConfId().c_str()); conf->detachLocalParticipant(); emitSignal<libjami::CallSignal::ConferenceChanged>(conf->getAccountId(), - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); pimpl_->unsetCurrentCall(); return true; } @@ -1544,8 +1555,8 @@ Manager::removeParticipant(Call& call) removeAudio(call); emitSignal<libjami::CallSignal::ConferenceChanged>(conf->getAccountId(), - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); pimpl_->processRemainingParticipants(*conf); } @@ -1822,24 +1833,6 @@ Manager::incomingCall(const std::string& accountId, Call& call) return; } - // Report incoming call using "CallSignal::IncomingCallWithMedia" signal. - auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps(call.getMediaAttributeList()); - - if (mediaList.empty()) { - JAMI_WARN("Incoming call %s has an empty media list", call.getCallId().c_str()); - } - - JAMI_DEBUG("Incoming call {:s} on account {:s} with {:d} media", - call.getCallId(), - accountId, - mediaList.size()); - - // Report the call using new API. - emitSignal<libjami::CallSignal::IncomingCallWithMedia>(accountId, - call.getCallId(), - call.getPeerDisplayName() + " " + from, - mediaList); - // Process the call. pimpl_->processIncomingCall(accountId, call); } @@ -1872,9 +1865,9 @@ Manager::incomingMessage(const std::string& accountId, // in case of a conference we must notify client using conference id emitSignal<libjami::CallSignal::IncomingMessage>(accountId, - conf->getConfId(), - from, - messages); + conf->getConfId(), + from, + messages); } else { JAMI_ERR("no conference associated to ID %s", callId.c_str()); } @@ -2475,6 +2468,32 @@ Manager::ManagerPimpl::processIncomingCall(const std::string& accountId, Call& i return; } + auto username = incomCall.toUsername(); + if (username.find('/') != std::string::npos) { + // Avoid to do heavy stuff in SIPVoIPLink's transaction_request_cb + dht::ThreadPool::io().run([this, account, incomCallId, username]() { + if (auto jamiAccount = std::dynamic_pointer_cast<JamiAccount>(account)) + jamiAccount->handleIncomingConversationCall(incomCallId, username); + }); + return; + } + + auto const& mediaList = MediaAttribute::mediaAttributesToMediaMaps( + incomCall.getMediaAttributeList()); + + if (mediaList.empty()) + JAMI_WARN("Incoming call %s has an empty media list", incomCallId.c_str()); + + JAMI_INFO("Incoming call %s on account %s with %lu media", + incomCallId.c_str(), + accountId.c_str(), + mediaList.size()); + + emitSignal<libjami::CallSignal::IncomingCallWithMedia>(accountId, + incomCallId, + incomCall.getPeerNumber(), + mediaList); + if (not base_.hasCurrentCall()) { incomCall.setState(Call::ConnectionState::RINGING); #if !defined(RING_UWP) && !(defined(TARGET_OS_IOS) && TARGET_OS_IOS) @@ -2509,17 +2528,17 @@ Manager::ManagerPimpl::processIncomingCall(const std::string& accountId, Call& i } // First call - auto conf = std::make_shared<Conference>(account, false); + auto conf = std::make_shared<Conference>(account, "", false); account->attach(conf); emitSignal<libjami::CallSignal::ConferenceCreated>(account->getAccountID(), - conf->getConfId()); + conf->getConfId()); // Bind calls according to their state bindCallToConference(*incomCall, *conf); conf->detachLocalParticipant(); emitSignal<libjami::CallSignal::ConferenceChanged>(account->getAccountID(), - conf->getConfId(), - conf->getStateStr()); + conf->getConfId(), + conf->getStateStr()); }); } else if (autoAnswer_ || account->isAutoAnswerEnabled()) { dht::ThreadPool::io().run( @@ -2968,8 +2987,8 @@ Manager::setAccountActive(const std::string& accountID, bool active, bool shutdo } } } - emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID, - acc->getVolatileAccountDetails()); + emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>( + accountID, acc->getVolatileAccountDetails()); } std::shared_ptr<AudioLayer> @@ -3173,9 +3192,8 @@ void Manager::enableLocalModerators(const std::string& accountID, bool isModEnabled) { if (auto acc = getAccount(accountID)) - acc->editConfig([&](AccountConfig& config){ - config.localModeratorsEnabled = isModEnabled; - }); + acc->editConfig( + [&](AccountConfig& config) { config.localModeratorsEnabled = isModEnabled; }); } bool @@ -3193,9 +3211,7 @@ void Manager::setAllModerators(const std::string& accountID, bool allModerators) { if (auto acc = getAccount(accountID)) - acc->editConfig([&](AccountConfig& config){ - config.allModeratorsEnabled = allModerators; - }); + acc->editConfig([&](AccountConfig& config) { config.allModeratorsEnabled = allModerators; }); } bool diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 57e29c959de0f818f573cdf0fa5595636b719c59..32c9aa56769d997d31f5569d78ee2861ad9b664a 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -93,8 +93,11 @@ SIPAccountBase::CreateClientDialogAndInvite(const pj_str_t* from, JAMI_DBG("No target provided, using 'to' as target"); } - if (pjsip_dlg_create_uac(pjsip_ua_instance(), from, contact, to, target, dlg) != PJ_SUCCESS) { - JAMI_ERR("Unable to create SIP dialogs for user agent client when calling %s", to->ptr); + auto status = pjsip_dlg_create_uac(pjsip_ua_instance(), from, contact, to, target, dlg); + if (status != PJ_SUCCESS) { + JAMI_ERR("Unable to create SIP dialogs for user agent client when calling %s %d", + to->ptr, + status); return false; } @@ -236,8 +239,8 @@ SIPAccountBase::getIceOptions() const noexcept IceTransportOptions opts; opts.upnpEnable = getUPnPActive(); - //if (config().stunEnabled) - // opts.stunServers.emplace_back(StunServerInfo().setUri(stunServer_)); + // if (config().stunEnabled) + // opts.stunServers.emplace_back(StunServerInfo().setUri(stunServer_)); if (config().turnEnabled) { auto cached = false; std::lock_guard<std::mutex> lk(cachedTurnMutex_); diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp index 85fb222a8fb0861d4c32974aa041aa65cd4f3eed..04a2e4ac74dedc7ae850fe5e152306ece18b0d6a 100644 --- a/src/sip/sipcall.cpp +++ b/src/sip/sipcall.cpp @@ -2589,9 +2589,8 @@ SIPCall::getMediaAttributeList() const { std::vector<MediaAttribute> mediaList; mediaList.reserve(rtpStreams_.size()); - for (auto const& stream : rtpStreams_) { + for (auto const& stream : rtpStreams_) mediaList.emplace_back(*stream.mediaAttribute_); - } return mediaList; } diff --git a/src/sip/sipcall.h b/src/sip/sipcall.h index 0b9a6d8288aa52e5b8f404dcf2764dfb7ed60c9c..444212f8f22f1990c353e33e40fb1d63730fdd28 100644 --- a/src/sip/sipcall.h +++ b/src/sip/sipcall.h @@ -323,6 +323,10 @@ public: { return std::weak_ptr<SIPCall>(shared()); } + /** + * Announce to the client that medias are successfully negotiated + */ + void reportMediaNegotiationStatus(); private: void generateMediaPorts(); @@ -391,7 +395,6 @@ private: void setupNegotiatedMedia(); void startAllMedia(); void stopAllMedia(); - void reportMediaNegotiationStatus(); void updateRemoteMedia(); /** diff --git a/src/sip/sipvoiplink.cpp b/src/sip/sipvoiplink.cpp index 4afe4dc217a8383240983aa6fe836943518ee219..ccc3c7c8373499ad77c75fbaee5cda8ed5dbba3f 100644 --- a/src/sip/sipvoiplink.cpp +++ b/src/sip/sipvoiplink.cpp @@ -318,9 +318,9 @@ transaction_request_cb(pjsip_rx_data* rdata) // urgent messages are optional if (ret >= 2) emitSignal<libjami::CallSignal::VoiceMailNotify>(account->getAccountID(), - newCount, - oldCount, - urgentCount); + newCount, + oldCount, + urgentCount); } } } else if (request.find(sip_utils::SIP_METHODS::MESSAGE) != std::string_view::npos) { @@ -393,7 +393,6 @@ transaction_request_cb(pjsip_rx_data* rdata) unsigned options = 0; if (pjsip_inv_verify_request(rdata, &options, NULL, NULL, endpt_, NULL) != PJ_SUCCESS) { - JAMI_ERR("Couldn't verify INVITE request in secure dialog."); try_respond_stateless(endpt_, rdata, PJSIP_SC_METHOD_NOT_ALLOWED, NULL, NULL, NULL); return PJ_FALSE; @@ -418,6 +417,8 @@ transaction_request_cb(pjsip_rx_data* rdata) } call->setPeerUaVersion(sip_utils::getPeerUserAgent(rdata)); + // The username can be used to join specific calls in conversations + call->toUsername(std::string(toUsername)); // FIXME : for now, use the same address family as the SIP transport auto family = pjsip_transport_type_get_af( diff --git a/src/uri.cpp b/src/uri.cpp index c851788af7d4b4824174744545b7679d2ef431d9..398bb00221955d12ad6658c657c373588efdebf0 100644 --- a/src/uri.cpp +++ b/src/uri.cpp @@ -39,6 +39,8 @@ Uri::Uri(const std::string_view& uri) scheme_ = Uri::Scheme::DATA_TRANSFER; else if (scheme_str == "git") scheme_ = Uri::Scheme::GIT; + else if (scheme_str == "rdv") + scheme_ = Uri::Scheme::RENDEZVOUS; else if (scheme_str == "sync") scheme_ = Uri::Scheme::SYNC; else @@ -75,6 +77,8 @@ Uri::schemeToString() const return "sip"; case Uri::Scheme::SWARM: return "swarm"; + case Uri::Scheme::RENDEZVOUS: + return "rdv"; case Uri::Scheme::GIT: return "git"; case Uri::Scheme::SYNC: diff --git a/src/uri.h b/src/uri.h index 5cafcf21e29ddaacd759e696f85645c0a9378fbe..d4d42f00525fa4eb54700c35f8bd6c9da1161996 100644 --- a/src/uri.h +++ b/src/uri.h @@ -31,6 +31,7 @@ public: JAMI, // Start with "jami:" and 45 ASCII chars OR 40 ASCII chars SIP, // Start with "sip:" SWARM, // Start with "swarm:" and 40 ASCII chars + RENDEZVOUS, // Start wutg "rdv" and used for call in swarms GIT, // Start with "git:" DATA_TRANSFER, // Start with "data-transfer://" SYNC, // Start with "sync:" diff --git a/src/vcard.h b/src/vcard.h index 4a91a22111b79d8f408339d68a63dc45b47d6298..09d746c36db36de59438b1097e057831165aa5c0 100644 --- a/src/vcard.h +++ b/src/vcard.h @@ -64,14 +64,23 @@ struct Property constexpr static const char* TELEPHONE = "TEL"; constexpr static const char* TIME_ZONE = "TZ"; constexpr static const char* TITLE = "TITLE"; + constexpr static const char* RDV_ACCOUNT = "RDV_ACCOUNT"; + constexpr static const char* RDV_DEVICE = "RDV_DEVICE"; constexpr static const char* URL = "URL"; constexpr static const char* BASE64 = "ENCODING=BASE64"; constexpr static const char* TYPE_PNG = "TYPE=PNG"; constexpr static const char* TYPE_JPEG = "TYPE=JPEG"; constexpr static const char* PHOTO_PNG = "PHOTO;ENCODING=BASE64;TYPE=PNG"; constexpr static const char* PHOTO_JPEG = "PHOTO;ENCODING=BASE64;TYPE=JPEG"; +}; - constexpr static const char* X_RINGACCOUNT = "X-RINGACCOUNTID"; +struct Value +{ + constexpr static const char* TITLE = "title"; + constexpr static const char* DESCRIPTION = "description"; + constexpr static const char* AVATAR = "avatar"; + constexpr static const char* RDV_ACCOUNT = "rdvAccount"; + constexpr static const char* RDV_DEVICE = "rdvDevice"; }; namespace utils { diff --git a/test/unitTest/Makefile.am b/test/unitTest/Makefile.am index f2b0fd99482716f308cc8df77b4b1fb248132945..98ed8813568e073f42500f55408ef7d727d4e943 100644 --- a/test/unitTest/Makefile.am +++ b/test/unitTest/Makefile.am @@ -168,6 +168,12 @@ ut_conversationRepository_SOURCES = conversationRepository/conversationRepositor check_PROGRAMS += ut_conversation ut_conversation_SOURCES = conversation/conversationcommon.cpp conversation/conversation.cpp common.cpp +# +# conversation_call +# +check_PROGRAMS += ut_conversation_call +ut_conversation_call_SOURCES = conversation/conversationcommon.cpp conversation/call.cpp common.cpp + # # media_negotiation # diff --git a/test/unitTest/call/conference.cpp b/test/unitTest/call/conference.cpp index c28f03ad3203b3a8535b61798e1547f3540ef12b..41b500b80bd32c6f13b9c15a7629a230fef05481 100644 --- a/test/unitTest/call/conference.cpp +++ b/test/unitTest/call/conference.cpp @@ -74,7 +74,8 @@ public: ConferenceTest() { // Init daemon - libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); if (not Manager::instance().initialized) CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); } @@ -104,6 +105,7 @@ private: void testPropagateRecording(); void testBrokenParticipantAudioAndVideo(); void testBrokenParticipantAudioOnly(); + void testRemoveConferenceInOneOne(); CPPUNIT_TEST_SUITE(ConferenceTest); CPPUNIT_TEST(testGetConference); @@ -126,6 +128,7 @@ private: CPPUNIT_TEST(testPropagateRecording); CPPUNIT_TEST(testBrokenParticipantAudioAndVideo); CPPUNIT_TEST(testBrokenParticipantAudioOnly); + CPPUNIT_TEST(testRemoveConferenceInOneOne); CPPUNIT_TEST_SUITE_END(); // Common parts @@ -204,11 +207,11 @@ ConferenceTest::registerSignalHandlers() } cv.notify_one(); })); - confHandlers.insert( - libjami::exportable_callback<libjami::CallSignal::StateChange>([=](const std::string& accountId, - const std::string& callId, - const std::string& state, - signed) { + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::StateChange>( + [=](const std::string& accountId, + const std::string& callId, + const std::string& state, + signed) { if (accountId == aliceId) { auto details = libjami::getCallDetails(aliceId, callId); if (details["PEER_NUMBER"].find(bobUri) != std::string::npos) @@ -469,16 +472,21 @@ ConferenceTest::testCreateParticipantsSinks() auto expectedNumberOfParticipants = 3; // Check participants number - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return pInfos_.size() == expectedNumberOfParticipants; })); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&] { return pInfos_.size() == expectedNumberOfParticipants; })); if (not jami::getVideoDeviceMonitor().getDeviceList().empty()) { JAMI_INFO() << "Check sinks if video device available."; for (auto& info : pInfos_) { auto uri = string_remove_suffix(info["uri"], '@'); if (uri == bobUri) { - CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&]{ return Manager::instance().getSinkClient(info["sinkId"]) != nullptr; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { + return Manager::instance().getSinkClient(info["sinkId"]) != nullptr; + })); } else if (uri == carlaUri) { - CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&]{ return Manager::instance().getSinkClient(info["sinkId"]) != nullptr; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { + return Manager::instance().getSinkClient(info["sinkId"]) != nullptr; + })); } } } else { @@ -486,14 +494,17 @@ ConferenceTest::testCreateParticipantsSinks() for (auto& info : pInfos_) { auto uri = string_remove_suffix(info["uri"], '@'); if (uri == bobUri) { - CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&]{ return Manager::instance().getSinkClient(info["sinkId"]) == nullptr; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { + return Manager::instance().getSinkClient(info["sinkId"]) == nullptr; + })); } else if (uri == carlaUri) { - CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&]{ return Manager::instance().getSinkClient(info["sinkId"]) == nullptr; })); + CPPUNIT_ASSERT(cv.wait_for(lk, 5s, [&] { + return Manager::instance().getSinkClient(info["sinkId"]) == nullptr; + })); } } } - hangupConference(); libjami::unregisterSignalHandlers(); @@ -560,9 +571,9 @@ ConferenceTest::testActiveStatusAfterRemove() daviCall.reset(); auto call2 = libjami::placeCallWithMedia(aliceId, - daviUri, - MediaAttribute::mediaAttributesToMediaMaps( - {defaultAudio})); + daviUri, + MediaAttribute::mediaAttributesToMediaMaps( + {defaultAudio})); CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return !daviCall.callId.empty(); })); Manager::instance().answerCall(daviId, daviCall.callId); CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return daviCall.hostState == "CURRENT"; })); @@ -793,17 +804,20 @@ ConferenceTest::testHostAddRmSecondVideo() // Alice adds new media pInfos_.clear(); std::vector<std::map<std::string, std::string>> mediaList - = {{{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::AUDIO}, + = {{{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::AUDIO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, ""}, {libjami::Media::MediaAttributeKey::LABEL, "audio_0"}}, - {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO}, + {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::VIDEO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, "bar"}, {libjami::Media::MediaAttributeKey::LABEL, "video_0"}}, - {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO}, + {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::VIDEO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, "foo"}, @@ -855,17 +869,20 @@ ConferenceTest::testParticipantAddRmSecondVideo() // Bob adds new media pInfos_.clear(); std::vector<std::map<std::string, std::string>> mediaList - = {{{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::AUDIO}, + = {{{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::AUDIO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, ""}, {libjami::Media::MediaAttributeKey::LABEL, "audio_0"}}, - {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO}, + {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::VIDEO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, "bar"}, {libjami::Media::MediaAttributeKey::LABEL, "video_0"}}, - {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO}, + {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, + libjami::Media::MediaAttributeValue::VIDEO}, {libjami::Media::MediaAttributeKey::ENABLED, "true"}, {libjami::Media::MediaAttributeKey::MUTED, "false"}, {libjami::Media::MediaAttributeKey::SOURCE, "foo"}, @@ -927,10 +944,11 @@ ConferenceTest::testBrokenParticipantAudioAndVideo() // Start conference with four participants startConference(false, true); - auto expectedNumberOfParticipants = 4; + auto expectedNumberOfParticipants = 4u; // Check participants number - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return pInfos_.size() == expectedNumberOfParticipants; })); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&] { return pInfos_.size() == expectedNumberOfParticipants; })); // Crash participant auto daviAccount = Manager::instance().getAccount<JamiAccount>(daviId); @@ -939,7 +957,8 @@ ConferenceTest::testBrokenParticipantAudioAndVideo() // Check participants number // It should have one less participant than in the conference start - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return expectedNumberOfParticipants - 1 == pInfos_.size(); })); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&] { return expectedNumberOfParticipants - 1 == pInfos_.size(); })); hangupConference(); @@ -953,10 +972,11 @@ ConferenceTest::testBrokenParticipantAudioOnly() // Start conference with four participants startConference(true, true); - auto expectedNumberOfParticipants = 4; + auto expectedNumberOfParticipants = 4u; // Check participants number - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return pInfos_.size() == expectedNumberOfParticipants; })); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&] { return pInfos_.size() == expectedNumberOfParticipants; })); // Crash participant auto daviAccount = Manager::instance().getAccount<JamiAccount>(daviId); @@ -965,10 +985,24 @@ ConferenceTest::testBrokenParticipantAudioOnly() // Check participants number // It should have one less participant than in the conference start - CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]{ return expectedNumberOfParticipants - 1 == pInfos_.size(); })); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&] { return expectedNumberOfParticipants - 1 == pInfos_.size(); })); hangupConference(); + libjami::unregisterSignalHandlers(); +} +void +ConferenceTest::testRemoveConferenceInOneOne() +{ + registerSignalHandlers(); + startConference(); + // Here it's 1:1 calls we merged, so we can close the conference + JAMI_INFO("Hangup Bob"); + Manager::instance().hangupCall(bobId, bobCall.callId); + CPPUNIT_ASSERT(cv.wait_for(lk, 20s, [&] { return confId.empty() && bobCall.state == "OVER"; })); + Manager::instance().hangupCall(carlaId, carlaCall.callId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return carlaCall.state == "OVER"; })); libjami::unregisterSignalHandlers(); } diff --git a/test/unitTest/conversation/call.cpp b/test/unitTest/conversation/call.cpp new file mode 100644 index 0000000000000000000000000000000000000000..6596946eafe614b37fa81520ec953d4a14700caf --- /dev/null +++ b/test/unitTest/conversation/call.cpp @@ -0,0 +1,832 @@ +/* + * Copyright (C) 2022 Savoir-faire Linux Inc. + * Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <cppunit/TestAssert.h> +#include <cppunit/TestFixture.h> +#include <cppunit/extensions/HelperMacros.h> + +#include <condition_variable> +#include <filesystem> +#include <string> + +#include "../../test_runner.h" +#include "account_const.h" +#include "common.h" +#include "conversation/conversationcommon.h" +#include "manager.h" + +using namespace std::literals::chrono_literals; + +namespace jami { +namespace test { + +struct ConvData +{ + std::string id {}; + bool requestReceived {false}; + bool conferenceChanged {false}; + bool conferenceRemoved {false}; + std::string hostState {}; + std::vector<std::map<std::string, std::string>> messages {}; +}; + +class ConversationCallTest : public CppUnit::TestFixture +{ +public: + ~ConversationCallTest() { libjami::fini(); } + static std::string name() { return "ConversationCallTest"; } + void setUp(); + void tearDown(); + + std::string aliceId; + std::string bobId; + std::string bob2Id; + std::string carlaId; + ConvData aliceData_; + ConvData bobData_; + ConvData bob2Data_; + ConvData carlaData_; + + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + +private: + void connectSignals(); + void enableCarla(); + + void testActiveCalls(); + void testActiveCalls3Peers(); + void testRejoinCall(); + void testParticipantHangupConfNotRemoved(); + void testJoinFinishedCall(); + void testJoinFinishedCallForbidden(); + void testUsePreference(); + void testJoinWhileActiveCall(); + + CPPUNIT_TEST_SUITE(ConversationCallTest); + CPPUNIT_TEST(testActiveCalls); + CPPUNIT_TEST(testActiveCalls3Peers); + CPPUNIT_TEST(testRejoinCall); + CPPUNIT_TEST(testParticipantHangupConfNotRemoved); + CPPUNIT_TEST(testJoinFinishedCall); + CPPUNIT_TEST(testJoinFinishedCallForbidden); + CPPUNIT_TEST(testUsePreference); + CPPUNIT_TEST(testJoinWhileActiveCall); + CPPUNIT_TEST_SUITE_END(); +}; + +CPPUNIT_TEST_SUITE_NAMED_REGISTRATION(ConversationCallTest, ConversationCallTest::name()); + +void +ConversationCallTest::setUp() +{ + // Init daemon + libjami::init( + libjami::InitFlag(libjami::libjami_FLAG_DEBUG | libjami::libjami_FLAG_CONSOLE_LOG)); + if (not Manager::instance().initialized) + CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); + + auto actors = load_actors("actors/alice-bob-carla.yml"); + aliceId = actors["alice"]; + bobId = actors["bob"]; + carlaId = actors["carla"]; + aliceData_ = {}; + bobData_ = {}; + bob2Data_ = {}; + carlaData_ = {}; + + Manager::instance().sendRegister(carlaId, false); + wait_for_announcement_of({aliceId, bobId}); +} + +void +ConversationCallTest::tearDown() +{ + auto bobArchive = std::filesystem::current_path().string() + "/bob.gz"; + std::remove(bobArchive.c_str()); + + if (bob2Id.empty()) { + wait_for_removal_of({aliceId, bobId, carlaId}); + } else { + wait_for_removal_of({aliceId, bobId, carlaId, bob2Id}); + } +} + +void +ConversationCallTest::connectSignals() +{ + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == aliceId) + aliceData_.id = conversationId; + else if (accountId == bobId) + bobData_.id = conversationId; + else if (accountId == bob2Id) + bob2Data_.id = conversationId; + else if (accountId == carlaId) + carlaData_.id = conversationId; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && aliceData_.id == conversationId) + aliceData_.messages.emplace_back(message); + if (accountId == bobId && bobData_.id == conversationId) + bobData_.messages.emplace_back(message); + if (accountId == bob2Id && bob2Data_.id == conversationId) + bob2Data_.messages.emplace_back(message); + if (accountId == carlaId && carlaData_.id == conversationId) + carlaData_.messages.emplace_back(message); + cv.notify_one(); + })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& accountId, + const std::string& /*conversationId*/, + std::map<std::string, std::string> /*metadatas*/) { + if (accountId == aliceId) + aliceData_.requestReceived = true; + if (accountId == bobId) + bobData_.requestReceived = true; + if (accountId == bob2Id) + bob2Data_.requestReceived = true; + if (accountId == carlaId) + carlaData_.requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::ConferenceChanged>( + [&](const std::string& accountId, const std::string&, const std::string&) { + if (accountId == aliceId) + aliceData_.conferenceChanged = true; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::ConferenceRemoved>( + [&](const std::string& accountId, const std::string&) { + if (accountId == aliceId) + aliceData_.conferenceRemoved = true; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::CallSignal::StateChange>( + [&](const std::string& accountId, + const std::string& callId, + const std::string& state, + signed) { + if (accountId == aliceId) { + auto details = libjami::getCallDetails(aliceId, callId); + if (details.find("PEER_NUMBER") != details.end()) { + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + + if (details["PEER_NUMBER"].find(bobUri) != std::string::npos) + bobData_.hostState = state; + else if (details["PEER_NUMBER"].find(carlaUri) != std::string::npos) + carlaData_.hostState = state; + } + } + cv.notify_one(); + })); + libjami::registerSignalHandlers(confHandlers); +} + +void +ConversationCallTest::enableCarla() +{ + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + // Enable carla + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + bool carlaConnected = false; + confHandlers.insert( + libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( + [&](const std::string&, const std::map<std::string, std::string>&) { + auto details = carlaAccount->getVolatileAccountDetails(); + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + if (deviceAnnounced == "true") { + carlaConnected = true; + cv.notify_one(); + } + })); + libjami::registerSignalHandlers(confHandlers); + + Manager::instance().sendRegister(carlaId, true); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&] { return carlaConnected; })); + confHandlers.clear(); + libjami::unregisterSignalHandlers(); +} + +void +ConversationCallTest::testActiveCalls() +{ + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + connectSignals(); + + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); + + // start call + aliceData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + // should get message + cv.wait_for(lk, 30s, [&]() { return !aliceData_.messages.empty(); }); + CPPUNIT_ASSERT(aliceData_.messages[0]["type"] == "application/call-history+json"); + + // get active calls = 1 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 1); + + // hangup + aliceData_.messages.clear(); + Manager::instance().hangupCall(aliceId, callId); + + // should get message + cv.wait_for(lk, 30s, [&]() { return !aliceData_.messages.empty(); }); + CPPUNIT_ASSERT(aliceData_.messages[0].find("duration") != aliceData_.messages[0].end()); + + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); +} + +void +ConversationCallTest::testActiveCalls3Peers() +{ + enableCarla(); + connectSignals(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + libjami::addConversationMember(aliceId, aliceData_.id, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bobData_.requestReceived && carlaData_.requestReceived; + })); + + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(carlaId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !carlaData_.id.empty() && !aliceData_.messages.empty(); + })); + + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_); + }); + + auto destination = fmt::format("rdv:{}/{}/{}/{}", + bobData_.id, + bobData_.messages.rbegin()->at("uri"), + bobData_.messages.rbegin()->at("device"), + bobData_.messages.rbegin()->at("confId")); + + aliceData_.conferenceChanged = false; + libjami::placeCallWithMedia(bobId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && bobData_.hostState == "CURRENT"; + }); + aliceData_.conferenceChanged = false; + libjami::placeCallWithMedia(carlaId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && carlaData_.hostState == "CURRENT"; + }); + + // get 3 participants + auto callList = libjami::getParticipantList(aliceId, bobData_.messages.rbegin()->at("confId")); + CPPUNIT_ASSERT(callList.size() == 3); + + // get active calls = 1 + CPPUNIT_ASSERT(libjami::getActiveCalls(bobId, bobData_.id).size() == 1); + + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupCall(aliceId, callId); + + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + CPPUNIT_ASSERT(aliceData_.messages[0].find("duration") != aliceData_.messages[0].end()); + CPPUNIT_ASSERT(bobData_.messages[0].find("duration") != bobData_.messages[0].end()); + CPPUNIT_ASSERT(carlaData_.messages[0].find("duration") != carlaData_.messages[0].end()); + + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); +} + +void +ConversationCallTest::testRejoinCall() +{ + enableCarla(); + connectSignals(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + libjami::addConversationMember(aliceId, aliceData_.id, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bobData_.requestReceived && carlaData_.requestReceived; + })); + + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(carlaId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !carlaData_.id.empty() && !aliceData_.messages.empty(); + })); + + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_); + }); + + auto confId = bobData_.messages.rbegin()->at("confId"); + auto destination = fmt::format("rdv:{}/{}/{}/{}", + bobData_.id, + bobData_.messages.rbegin()->at("uri"), + bobData_.messages.rbegin()->at("device"), + bobData_.messages.rbegin()->at("confId")); + + aliceData_.conferenceChanged = false; + auto bobCall = libjami::placeCallWithMedia(bobId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && bobData_.hostState == "CURRENT"; + }); + aliceData_.conferenceChanged = false; + libjami::placeCallWithMedia(carlaId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && carlaData_.hostState == "CURRENT"; + }); + + CPPUNIT_ASSERT(libjami::getParticipantList(aliceId, confId).size() == 3); + + // hangup 1 participant and rejoin + aliceData_.messages.clear(); + bobData_.messages.clear(); + aliceData_.conferenceChanged = false; + Manager::instance().hangupCall(bobId, bobCall); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && bobData_.hostState == "OVER"; + }); + + CPPUNIT_ASSERT(libjami::getParticipantList(aliceId, confId).size() == 2); + + aliceData_.conferenceChanged = false; + libjami::placeCallWithMedia(bobId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && bobData_.hostState == "CURRENT"; + }); + + CPPUNIT_ASSERT(libjami::getParticipantList(aliceId, confId).size() == 3); + CPPUNIT_ASSERT(aliceData_.messages.empty()); + CPPUNIT_ASSERT(bobData_.messages.empty()); + + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupCall(aliceId, callId); + + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + CPPUNIT_ASSERT(aliceData_.messages[0].find("duration") != aliceData_.messages[0].end()); + CPPUNIT_ASSERT(bobData_.messages[0].find("duration") != bobData_.messages[0].end()); + CPPUNIT_ASSERT(carlaData_.messages[0].find("duration") != carlaData_.messages[0].end()); + + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); +} + +void +ConversationCallTest::testParticipantHangupConfNotRemoved() +{ + connectSignals(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return bobData_.requestReceived; })); + + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_); + }); + + auto destination = fmt::format("rdv:{}/{}/{}/{}", + bobData_.id, + bobData_.messages.rbegin()->at("uri"), + bobData_.messages.rbegin()->at("device"), + bobData_.messages.rbegin()->at("confId")); + + aliceData_.conferenceChanged = false; + auto bobCallId = libjami::placeCallWithMedia(bobId, destination, {}); + cv.wait_for(lk, 30s, [&]() { + return aliceData_.conferenceChanged && bobData_.hostState == "CURRENT"; + }); + + // hangup bob MUST NOT stop the conference + aliceData_.messages.clear(); + bobData_.messages.clear(); + aliceData_.conferenceChanged = false; + Manager::instance().hangupCall(bobId, bobCallId); + + CPPUNIT_ASSERT(!cv.wait_for(lk, 10s, [&]() { return aliceData_.conferenceRemoved; })); +} + +void +ConversationCallTest::testJoinFinishedCall() +{ + enableCarla(); + connectSignals(); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + libjami::addConversationMember(aliceId, aliceData_.id, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bobData_.requestReceived && carlaData_.requestReceived; + })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(carlaId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !carlaData_.id.empty() && !aliceData_.messages.empty(); + })); + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_); + }); + auto confId = bobData_.messages.rbegin()->at("confId"); + auto destination = fmt::format("rdv:{}/{}/{}/{}", + bobData_.id, + bobData_.messages.rbegin()->at("uri"), + bobData_.messages.rbegin()->at("device"), + bobData_.messages.rbegin()->at("confId")); + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupCall(aliceId, callId); + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + // If bob try to join the call, it will re-host a new conference + // and commit a new active call. + auto bobCall = libjami::placeCallWithMedia(bobId, destination, {}); + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_) && bobData_.hostState == "CURRENT"; + }); + confId = bobData_.messages.rbegin()->at("confId"); + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 1); + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupConference(aliceId, confId); + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + CPPUNIT_ASSERT(aliceData_.messages[0].find("duration") != aliceData_.messages[0].end()); + CPPUNIT_ASSERT(bobData_.messages[0].find("duration") != bobData_.messages[0].end()); + CPPUNIT_ASSERT(carlaData_.messages[0].find("duration") != carlaData_.messages[0].end()); + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); +} + +void +ConversationCallTest::testJoinFinishedCallForbidden() +{ + enableCarla(); + connectSignals(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + auto carlaAccount = Manager::instance().getAccount<JamiAccount>(carlaId); + auto carlaUri = carlaAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + // Do not host conference for others + libjami::setConversationPreferences(aliceId, aliceData_.id, {{"hostConference", "false"}}); + + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + libjami::addConversationMember(aliceId, aliceData_.id, carlaUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { + return bobData_.requestReceived && carlaData_.requestReceived; + })); + + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(carlaId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !carlaData_.id.empty() && !aliceData_.messages.empty(); + })); + + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_); + }); + + auto confId = bobData_.messages.rbegin()->at("confId"); + auto destination = fmt::format("rdv:{}/{}/{}/{}", + bobData_.id, + bobData_.messages.rbegin()->at("uri"), + bobData_.messages.rbegin()->at("device"), + bobData_.messages.rbegin()->at("confId")); + + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupCall(aliceId, callId); + + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); + + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + // If bob try to join the call, it will re-host a new conference + // and commit a new active call. + auto bobCall = libjami::placeCallWithMedia(bobId, destination, {}); + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_) + && lastCommitIsCall(carlaData_) && bobData_.hostState == "CURRENT"; + }); + + confId = bobData_.messages.rbegin()->at("confId"); + + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 1); + + // hangup + aliceData_.messages.clear(); + bobData_.messages.clear(); + carlaData_.messages.clear(); + Manager::instance().hangupConference(aliceId, confId); + + // should get message + cv.wait_for(lk, 30s, [&]() { + return !aliceData_.messages.empty() && !bobData_.messages.empty() + && !carlaData_.messages.empty(); + }); + CPPUNIT_ASSERT(aliceData_.messages[0].find("duration") != aliceData_.messages[0].end()); + CPPUNIT_ASSERT(bobData_.messages[0].find("duration") != bobData_.messages[0].end()); + CPPUNIT_ASSERT(carlaData_.messages[0].find("duration") != carlaData_.messages[0].end()); + + // get active calls = 0 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, aliceData_.id).size() == 0); +} + +void +ConversationCallTest::testUsePreference() +{ + enableCarla(); + connectSignals(); + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto aliceDevice = std::string(aliceAccount->currentDeviceId()); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobData_.requestReceived; })); + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + + // Update preferences + aliceData_.messages.clear(); + bobData_.messages.clear(); + auto lastCommitIsProfile = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/update-profile"; + }; + libjami::updateConversationInfos(aliceId, + aliceData_.id, + std::map<std::string, std::string> { + {"rdvAccount", aliceUri}, + {"rdvDevice", aliceDevice}, + }); + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsProfile(aliceData_) && lastCommitIsProfile(bobData_); + }); + + // start call + aliceData_.messages.clear(); + bobData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(bobId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { + return lastCommitIsCall(aliceData_) && lastCommitIsCall(bobData_); + }); + auto confId = bobData_.messages.rbegin()->at("confId"); + + // Alice should be the host + CPPUNIT_ASSERT(aliceAccount->getConference(confId)); + Manager::instance().hangupCall(bobId, callId); +} + +void +ConversationCallTest::testJoinWhileActiveCall() +{ + connectSignals(); + + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto aliceUri = aliceAccount->getUsername(); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto bobUri = bobAccount->getUsername(); + // Start conversation + libjami::startConversation(aliceId); + cv.wait_for(lk, 30s, [&]() { return !aliceData_.id.empty(); }); + + // start call + aliceData_.messages.clear(); + auto callId = libjami::placeCallWithMedia(aliceId, "swarm:" + aliceData_.id, {}); + auto lastCommitIsCall = [&](const auto& data) { + return !data.messages.empty() + && data.messages.rbegin()->at("type") == "application/call-history+json"; + }; + // should get message + cv.wait_for(lk, 30s, [&]() { return lastCommitIsCall(aliceData_); }); + + libjami::addConversationMember(aliceId, aliceData_.id, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&]() { return bobData_.requestReceived; })); + + aliceData_.messages.clear(); + libjami::acceptConversationRequest(bobId, aliceData_.id); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { + return !bobData_.id.empty() && !aliceData_.messages.empty(); + })); + + CPPUNIT_ASSERT(libjami::getActiveCalls(bobId, bobData_.id).size() == 1); + + // hangup bob MUST NOT stop the conference + aliceData_.messages.clear(); + bobData_.messages.clear(); + aliceData_.conferenceChanged = false; + Manager::instance().hangupCall(bobId, bobCallId); + + CPPUNIT_ASSERT(!cv.wait_for(lk, 10s, [&]() { return aliceData_.conferenceRemoved; })); +} + +} // namespace test +} // namespace jami + +RING_TEST_RUNNER(jami::test::ConversationCallTest::name()) diff --git a/test/unitTest/conversation/conversationMembersEvent.cpp b/test/unitTest/conversation/conversationMembersEvent.cpp index c65acdc49791a29f10ffd427cbee58dcc5725595..42da0aa0baa2306232284145d2647a76af59c2de 100644 --- a/test/unitTest/conversation/conversationMembersEvent.cpp +++ b/test/unitTest/conversation/conversationMembersEvent.cpp @@ -86,6 +86,7 @@ public: void testRemoveRequestBannedMultiDevices(); void testBanUnbanMultiDevice(); void testBanUnbanGotFirstConv(); + void testBanHostWhileHosting(); std::string aliceId; std::string bobId; @@ -125,6 +126,7 @@ private: CPPUNIT_TEST(testRemoveRequestBannedMultiDevices); CPPUNIT_TEST(testBanUnbanMultiDevice); CPPUNIT_TEST(testBanUnbanGotFirstConv); + CPPUNIT_TEST(testBanHostWhileHosting); CPPUNIT_TEST_SUITE_END(); }; @@ -135,7 +137,8 @@ void ConversationMembersEventTest::setUp() { // Init daemon - libjami::init(libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); + libjami::init( + libjami::InitFlag(libjami::LIBJAMI_FLAG_DEBUG | libjami::LIBJAMI_FLAG_CONSOLE_LOG)); if (not Manager::instance().initialized) CPPUNIT_ASSERT(libjami::start("jami-sample.yml")); @@ -202,9 +205,9 @@ ConversationMembersEventTest::generateFakeInvite(std::shared_ptr<JamiAccount> ac cr.commitMessage(Json::writeString(wbuilder, json)); libjami::sendMessage(account->getAccountID(), - convId, - "trigger the fake history to be pulled"s, - ""); + convId, + "trigger the fake history to be pulled"s, + ""); } void @@ -412,15 +415,16 @@ ConversationMembersEventTest::testMemberAddedNoBadFile() cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& accountId, - const std::string& conversationId, - int code, - const std::string& /* what */) { - if (accountId == bobId && conversationId == convId && code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); addFile(aliceAccount, convId, "BADFILE"); generateFakeInvite(aliceAccount, convId, bobUri); @@ -705,7 +709,8 @@ ConversationMembersEventTest::testRemoveInvitedMember() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); @@ -796,7 +801,8 @@ ConversationMembersEventTest::testMemberBanNoBadFile() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); @@ -823,15 +829,16 @@ ConversationMembersEventTest::testMemberBanNoBadFile() } cv.notify_one(); })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& accountId, - const std::string& conversationId, - int code, - const std::string& /* what */) { - if (accountId == bobId && conversationId == convId && code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& accountId, + const std::string& conversationId, + int code, + const std::string& /* what */) { + if (accountId == bobId && conversationId == convId && code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); Manager::instance().sendRegister(carlaId, true); CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return carlaConnected; })); @@ -1078,7 +1085,8 @@ ConversationMembersEventTest::testMemberCannotBanOther() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); @@ -1105,15 +1113,16 @@ ConversationMembersEventTest::testMemberCannotBanOther() } cv.notify_one(); })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); Manager::instance().sendRegister(carlaId, true); CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return carlaConnected; })); @@ -1174,7 +1183,8 @@ ConversationMembersEventTest::testMemberCannotUnBanOther() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); @@ -1203,15 +1213,16 @@ ConversationMembersEventTest::testMemberCannotUnBanOther() } cv.notify_one(); })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); Manager::instance().sendRegister(carlaId, true); CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return carlaConnected; })); @@ -1302,21 +1313,23 @@ ConversationMembersEventTest::testCheckAdminFakeAVoteIsDetected() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); Manager::instance().sendRegister(carlaId, true); CPPUNIT_ASSERT(cv.wait_for(lk, 60s, [&] { return carlaConnected; })); @@ -1434,15 +1447,16 @@ ConversationMembersEventTest::testCommitUnauthorizedUser() cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); auto convId = libjami::startConversation(aliceId); @@ -1511,21 +1525,23 @@ ConversationMembersEventTest::testMemberJoinsNoBadFile() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); aliceAccount->convModule()->addConversationMember(convId, carlaUri, false); @@ -1595,21 +1611,23 @@ ConversationMembersEventTest::testMemberAddedNoCertificate() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); aliceAccount->convModule()->addConversationMember(convId, carlaUri, false); @@ -1689,21 +1707,23 @@ ConversationMembersEventTest::testMemberJoinsInviteRemoved() libjami::exportable_callback<libjami::ConfigurationSignal::VolatileDetailsChanged>( [&](const std::string&, const std::map<std::string, std::string>&) { auto details = carlaAccount->getVolatileAccountDetails(); - auto deviceAnnounced = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; + auto deviceAnnounced + = details[libjami::Account::VolatileProperties::DEVICE_ANNOUNCED]; if (deviceAnnounced == "true") { carlaConnected = true; cv.notify_one(); } })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); aliceAccount->convModule()->addConversationMember(convId, carlaUri, false); @@ -1768,16 +1788,17 @@ ConversationMembersEventTest::testFailAddMemberInOneToOne() std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false, memberMessageGenerated = false; std::string convId = ""; - confHandlers.insert(libjami::exportable_callback<libjami::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( + libjami::exportable_callback<libjami::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(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( [&](const std::string& accountId, const std::string& conversationId) { if (accountId == aliceId) { @@ -1821,16 +1842,17 @@ ConversationMembersEventTest::testOneToOneFetchWithNewMemberRefused() bool conversationReady = false, requestReceived = false, memberMessageGenerated = false, messageBob = false, errorDetected = false; std::string convId = ""; - confHandlers.insert(libjami::exportable_callback<libjami::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( + libjami::exportable_callback<libjami::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(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( [&](const std::string& accountId, const std::string& conversationId) { if (accountId == aliceId) { @@ -1852,15 +1874,16 @@ ConversationMembersEventTest::testOneToOneFetchWithNewMemberRefused() } cv.notify_one(); })); - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( - [&](const std::string& /* accountId */, - const std::string& /* conversationId */, - int code, - const std::string& /* what */) { - if (code == 3) - errorDetected = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::OnConversationError>( + [&](const std::string& /* accountId */, + const std::string& /* conversationId */, + int code, + const std::string& /* what */) { + if (code == 3) + errorDetected = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); aliceAccount->addContact(bobUri); aliceAccount->sendTrustRequest(bobUri, {}); @@ -1950,16 +1973,17 @@ ConversationMembersEventTest::testGetConversationsMembersWhileSyncing() std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; bool conversationReady = false, requestReceived = false; std::string convId = ""; - confHandlers.insert(libjami::exportable_callback<libjami::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( + libjami::exportable_callback<libjami::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(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( [&](const std::string& accountId, const std::string& conversationId) { if (accountId == aliceId) { @@ -2058,12 +2082,13 @@ ConversationMembersEventTest::testAvoidTwoOneToOne() cv.notify_one(); })); auto conversationRmBob = false; - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( - [&](const std::string& accountId, const std::string&) { - if (accountId == bobId) - conversationRmBob = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( + [&](const std::string& accountId, const std::string&) { + if (accountId == bobId) + conversationRmBob = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); // Alice adds bob @@ -2149,14 +2174,15 @@ ConversationMembersEventTest::testAvoidTwoOneToOneMultiDevices() cv.notify_one(); })); auto conversationRmBob = false, conversationRmBob2 = false; - confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( - [&](const std::string& accountId, const std::string&) { - if (accountId == bobId) - conversationRmBob = true; - else if (accountId == bob2Id) - conversationRmBob2 = true; - cv.notify_one(); - })); + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRemoved>( + [&](const std::string& accountId, const std::string&) { + if (accountId == bobId) + conversationRmBob = true; + else if (accountId == bob2Id) + conversationRmBob2 = true; + cv.notify_one(); + })); libjami::registerSignalHandlers(confHandlers); // Bob creates a second device @@ -2526,6 +2552,80 @@ ConversationMembersEventTest::testBanUnbanGotFirstConv() CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return bobMsgReceived && bob2MsgReceived; })); } +void +ConversationMembersEventTest::testBanHostWhileHosting() +{ + auto aliceAccount = Manager::instance().getAccount<JamiAccount>(aliceId); + auto bobAccount = Manager::instance().getAccount<JamiAccount>(bobId); + auto aliceUri = aliceAccount->getUsername(); + auto bobUri = bobAccount->getUsername(); + auto convId = libjami::startConversation(aliceId); + std::mutex mtx; + std::unique_lock<std::mutex> lk {mtx}; + std::condition_variable cv; + std::map<std::string, std::shared_ptr<libjami::CallbackWrapperBase>> confHandlers; + bool conversationReady = false, requestReceived = false; + confHandlers.insert( + libjami::exportable_callback<libjami::ConversationSignal::ConversationRequestReceived>( + [&](const std::string& /*accountId*/, + const std::string& /* conversationId */, + std::map<std::string, std::string> /*metadatas*/) { + requestReceived = true; + cv.notify_one(); + })); + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::ConversationReady>( + [&](const std::string& accountId, const std::string& conversationId) { + if (accountId == bobId && conversationId == convId) { + conversationReady = true; + cv.notify_one(); + } + })); + bool memberMessageGenerated = false, callMessageGenerated = false, voteMessageGenerated = false; + confHandlers.insert(libjami::exportable_callback<libjami::ConversationSignal::MessageReceived>( + [&](const std::string& accountId, + const std::string& conversationId, + std::map<std::string, std::string> message) { + if (accountId == aliceId && conversationId == convId && message["type"] == "vote") { + voteMessageGenerated = true; + cv.notify_one(); + } else if (accountId == aliceId && conversationId == convId) { + if (message["type"] == "application/call-history+json") { + callMessageGenerated = true; + } else if (message["type"] == "member") { + memberMessageGenerated = true; + } + cv.notify_one(); + } + })); + libjami::registerSignalHandlers(confHandlers); + libjami::addConversationMember(aliceId, convId, bobUri); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return requestReceived; })); + memberMessageGenerated = false; + libjami::acceptConversationRequest(bobId, convId); + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return memberMessageGenerated; })); + + // Now, Bob starts a call + auto callId = libjami::placeCallWithMedia(bobId, "swarm:" + convId, {}); + // should get message + CPPUNIT_ASSERT(cv.wait_for(lk, 30s, [&]() { return callMessageGenerated; })); + + // get active calls = 1 + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, convId).size() == 1); + + // Now check that alice, has the only admin, can remove bob + memberMessageGenerated = false; + voteMessageGenerated = false; + libjami::removeConversationMember(aliceId, convId, bobUri); + CPPUNIT_ASSERT( + cv.wait_for(lk, 30s, [&]() { return memberMessageGenerated && voteMessageGenerated; })); + auto members = libjami::getConversationMembers(aliceId, convId); + CPPUNIT_ASSERT(members.size() == 1); + CPPUNIT_ASSERT(members[0]["uri"] == aliceAccount->getUsername()); + CPPUNIT_ASSERT(members[0]["role"] == "admin"); + CPPUNIT_ASSERT(libjami::getActiveCalls(aliceId, convId).size() == 0); + + libjami::unregisterSignalHandlers(); +} } // namespace test } // namespace jami