From 89609362217aa31b2c859c609daeb6c1ff7e5cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Blin?= <sebastien.blin@savoirfairelinux.com> Date: Mon, 26 Jul 2021 14:16:53 -0400 Subject: [PATCH] swarm: add call support This patch introduces the ability to start calls and extends the usage of rendezvous points to swarm with multiple participants. When starting a call in a swarm with multiple participats, one device will work as the host of the conference, and the caller will immediately start the call alone. Other peers will receive a commit and a notification to be able to join the active call. To join a call, users needs to call rdv:uri/device/convId/confId to be added (if authorized) to the conf. There are some majors differences in the process. First, every conversation will be able to decide a default host for conferences. This still needs some design and will be introduced in another patch. For now, the caller is the host. Then, because all members of the call may not be interested to join a call, or they may want to get several calls at the same time, the system must be able to manage more than one active calls (e.g. a company with multiple projects can do several standups at the same time). Finally, in the conversation, two commits will be generated to be able to know what active calls are available. The first is announcing that a conference started, the second announces that the conference stopped (the host closed the call). However, this introduces a difficulty. The host may crash and not commit the end of the call in time. In this case, hostedCalls are stored in a file and the conversation is updated during the init of the daemon. Change-Id: I081a4920edb3773bbed884ae50f34e476ad42094 Documentation: https://docs.jami.net/technical/swarm.html#call-in-swarm GitLab: #312 --- .../cx.ring.Ring.ConfigurationManager.xml | 31 + bin/dbus/dbusclient.cpp | 2 + bin/dbus/dbusconfigurationmanager.cpp | 35 +- bin/dbus/dbusconfigurationmanager.h | 6 +- bin/jni/configurationmanager.i | 5 + bin/jni/conversation.i | 1 + bin/jni/jni_interface.i | 2 + bin/nodejs/callback.h | 44 +- bin/nodejs/configurationmanager.i | 5 + bin/nodejs/conversation.i | 1 + bin/nodejs/nodejs_interface.i | 2 + configure.ac | 2 +- meson.build | 2 +- src/call.cpp | 15 +- src/call.h | 13 + src/client/conversation_interface.cpp | 9 + src/client/ring_signal.cpp | 2 + src/conference.cpp | 93 +- src/conference.h | 25 +- src/jami/configurationmanager_interface.h | 98 ++- src/jami/conversation_interface.h | 74 +- src/jamidht/conversation.cpp | 334 ++++++- src/jamidht/conversation.h | 38 + src/jamidht/conversation_module.cpp | 292 +++++- src/jamidht/conversation_module.h | 32 +- src/jamidht/conversationrepository.cpp | 77 +- src/jamidht/jamiaccount.cpp | 137 ++- src/jamidht/jamiaccount.h | 34 +- src/manager.cpp | 110 ++- src/sip/sipaccountbase.cpp | 11 +- src/sip/sipcall.cpp | 3 +- src/sip/sipcall.h | 5 +- src/sip/sipvoiplink.cpp | 9 +- src/uri.cpp | 4 + src/uri.h | 1 + src/vcard.h | 11 +- test/unitTest/Makefile.am | 6 + test/unitTest/call/conference.cpp | 88 +- test/unitTest/conversation/call.cpp | 832 ++++++++++++++++++ .../conversation/conversationMembersEvent.cpp | 392 ++++++--- 40 files changed, 2393 insertions(+), 490 deletions(-) create mode 100644 test/unitTest/conversation/call.cpp diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 29c26eb662..71b4bcd8e4 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 22179f059f..b087a0e3ea 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 7e72fb4cd1..7896840266 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 413a3dac8c..c176849c8a 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 3714a1ca2b..be861049ff 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 5d2c538414..ae7b5501dc 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 e9f4d399df..8f2159d888 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 749d887c70..7b2a4d69b2 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 dfecde2b9c..6cdc41f4cc 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 fc99c55a17..0de5d6168f 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 1acb741cd1..8adf698695 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 8a38e43429..19b263e97a 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 211acff630..a93b405255 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 328c2af1f4..e07f3f2704 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 ac423d60b6..72c715394a 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 d46952c8b1..f67f7f7b00 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 6906887ebe..277e254058 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 ffab65089f..d7b1db1a18 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 defb1f540a..7212daedbc 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 d8a3ef0d16..4ad507ba7b 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 011af5d64e..0a9901f1a4 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 2c67d01ce2..d6c43c91b8 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 29b0ee0e93..d5114d232a 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 610014e210..14611bbca9 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 7dee281aaf..c45f8a57c0 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 b36f46f0d7..da0579f685 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 f617bd9126..8ef6922fdb 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 b6be211085..f24e17e78f 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 b82e9f5132..eaad12af78 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 57e29c959d..32c9aa5676 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 85fb222a8f..04a2e4ac74 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 0b9a6d8288..444212f8f2 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 4afe4dc217..ccc3c7c837 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 c851788af7..398bb00221 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 5cafcf21e2..d4d42f0052 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 4a91a22111..09d746c36d 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 f2b0fd9948..98ed881356 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 c28f03ad32..41b500b80b 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 0000000000..6596946eaf --- /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 c65acdc497..42da0aa0ba 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 -- GitLab