diff --git a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml index 88543d88d4c5337d23b1196c15a55a89f58a700a..a54d9b27bbe6656e05d4d6f4cc6a7dbb3bf661be 100644 --- a/bin/dbus/cx.ring.Ring.ConfigurationManager.xml +++ b/bin/dbus/cx.ring.Ring.ConfigurationManager.xml @@ -610,6 +610,40 @@ </arg> </signal> + <method name="setIsComposing" tp:name-for-bindings="setIsComposing"> + <tp:added version="7.9.0"/> + <tp:docstring> + <p>Sends and update composing indication for a given contact/conversation</p> + </tp:docstring> + <arg type="s" name="accountId" direction="in"> + <tp:docstring> + The account ID + </tp:docstring> + </arg> + <arg type="s" name="contactId" direction="in"> + <tp:docstring> + The contact ID + </tp:docstring> + </arg> + <arg type="b" name="isComposing" direction="in"> + <tp:docstring> + True is the user is composing a message, false otherwise + </tp:docstring> + </arg> + </method> + + <signal name="composingStatusChanged" tp:name-for-bindings="composingStatusChanged"> + <tp:added version="7.9.0"/> + <tp:docstring> + Notify clients that a message composition status changed + </tp:docstring> + <arg type="s" name="accountId"/> + <arg type="s" name="contactId"/> + <arg type="i" name="status"> + <tp:docstring>The new status of the message, 0 for Idle, 1 for Active</tp:docstring> + </arg> + </signal> + <method name="setVolume" tp:name-for-bindings="setVolume"> <tp:docstring> <p>Sets the volume using a linear scale [0,100].</p> diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp index f4970075e38d35705d8534d08d5eee73ad7aebbf..a56f974941c570c62343d5de680d84e301c797b8 100644 --- a/bin/dbus/dbusclient.cpp +++ b/bin/dbus/dbusclient.cpp @@ -177,6 +177,7 @@ DBusClient::initLibrary(int flags) exportable_callback<ConfigurationSignal::Error>(bind(&DBusConfigurationManager::errorAlert, confM, _1)), exportable_callback<ConfigurationSignal::IncomingAccountMessage>(bind(&DBusConfigurationManager::incomingAccountMessage, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>(bind(&DBusConfigurationManager::accountMessageStatusChanged, confM, _1, _2, _3, _4 )), + exportable_callback<ConfigurationSignal::ComposingStatusChanged>(bind(&DBusConfigurationManager::composingStatusChanged, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::IncomingTrustRequest>(bind(&DBusConfigurationManager::incomingTrustRequest, confM, _1, _2, _3, _4 )), exportable_callback<ConfigurationSignal::ContactAdded>(bind(&DBusConfigurationManager::contactAdded, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::ContactRemoved>(bind(&DBusConfigurationManager::contactRemoved, confM, _1, _2, _3 )), diff --git a/bin/dbus/dbusconfigurationmanager.cpp b/bin/dbus/dbusconfigurationmanager.cpp index b71da1af5a1b174b32368ec6d5ad81aa030bd608..068fcfa21cf3df7270e123ecfbfb3800401496b4 100644 --- a/bin/dbus/dbusconfigurationmanager.cpp +++ b/bin/dbus/dbusconfigurationmanager.cpp @@ -192,6 +192,12 @@ DBusConfigurationManager::cancelMessage(const std::string& accountID, const uint return DRing::cancelMessage(accountID, id); } +void +DBusConfigurationManager::setIsComposing(const std::string& accountID, const std::string& to, const bool& isWriting) +{ + DRing::setIsComposing(accountID, to, isWriting); +} + auto DBusConfigurationManager::getTlsDefaultSettings() -> decltype(DRing::getTlsDefaultSettings()) { diff --git a/bin/dbus/dbusconfigurationmanager.h b/bin/dbus/dbusconfigurationmanager.h index cad216eb05f979934b9af5bf0974d45c5f4b0a68..18b72829bd3160616f5959e3a2c6091a0703da18 100644 --- a/bin/dbus/dbusconfigurationmanager.h +++ b/bin/dbus/dbusconfigurationmanager.h @@ -88,6 +88,7 @@ class DRING_PUBLIC DBusConfigurationManager : int getMessageStatus(const uint64_t& id); int getMessageStatus(const std::string& accountID, const uint64_t& id); bool cancelMessage(const std::string& accountID, const uint64_t& messageId); + void setIsComposing(const std::string& accountID, const std::string& to, const bool& isWriting); std::map<std::string, std::string> getTlsDefaultSettings(); std::vector<std::string> getSupportedCiphers(const std::string& accountID); std::vector<unsigned> getCodecList(); diff --git a/bin/jni/configurationmanager.i b/bin/jni/configurationmanager.i index 03464636a9681ba512dd283b47ab2b7ea6d7d80e..2c61adb0224084ebfc9cb8ef5d01a0580cb6cb2f 100644 --- a/bin/jni/configurationmanager.i +++ b/bin/jni/configurationmanager.i @@ -35,6 +35,7 @@ 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*/, uint64_t /*message_id*/, const std::string& /*to*/, int /*state*/){} + virtual void composingStatusChanged(const std::string& /*account_id*/, 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*/){} @@ -92,6 +93,8 @@ std::vector<DRing::Message> getLastMessages(const std::string& accountID, uint64 int getMessageStatus(uint64_t id); int getMessageStatus(const std::string& accountID, uint64_t id); bool cancelMessage(const std::string& accountID, uint64_t id); +void setIsComposing(const std::string& accountID, const std::string& to, bool isWriting); + bool changeAccountPassword(const std::string& accountID, const std::string& password_old, const std::string& password_new); bool lookupName(const std::string& account, const std::string& nameserver, const std::string& name); @@ -241,6 +244,7 @@ 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*/, uint64_t /*message_id*/, const std::string& /*to*/, int /*state*/){} + virtual void composingStatusChanged(const std::string& /*account_id*/, 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/jni_interface.i b/bin/jni/jni_interface.i index 1a9201cdc47f9572ed346d283100bf175b0b58cf..304e208f132d65a3875411a80283667d3b6f3b03 100644 --- a/bin/jni/jni_interface.i +++ b/bin/jni/jni_interface.i @@ -263,6 +263,7 @@ 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 )), exportable_callback<ConfigurationSignal::AccountMessageStatusChanged>(bind(&ConfigurationCallback::accountMessageStatusChanged, confM, _1, _2, _3, _4 )), + exportable_callback<ConfigurationSignal::ComposingStatusChanged>(bind(&ConfigurationCallback::composingStatusChanged, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::IncomingTrustRequest>(bind(&ConfigurationCallback::incomingTrustRequest, confM, _1, _2, _3, _4 )), exportable_callback<ConfigurationSignal::ContactAdded>(bind(&ConfigurationCallback::contactAdded, confM, _1, _2, _3 )), exportable_callback<ConfigurationSignal::ContactRemoved>(bind(&ConfigurationCallback::contactRemoved, confM, _1, _2, _3 )), diff --git a/configure.ac b/configure.ac index 333267ea0092c485e1df34818fde66ad14a8d38e..69f68816281333e48a91b3d9f8212eac74b9b324 100644 --- a/configure.ac +++ b/configure.ac @@ -2,7 +2,7 @@ dnl Jami - configure.ac for automake 1.9 and autoconf 2.59 dnl Process this file with autoconf to produce a configure script. AC_PREREQ([2.65]) -AC_INIT([Jami Daemon],[7.8.0],[ring@gnu.org],[jami]) +AC_INIT([Jami Daemon],[7.9.0],[ring@gnu.org],[jami]) AC_COPYRIGHT([[Copyright (c) Savoir-faire Linux 2004-2019]]) AC_REVISION([$Revision$]) diff --git a/src/account.cpp b/src/account.cpp index 0c8565401dd92c401bf5b2556dd5ddf7ae2cf08f..86e92b4afee214ff2d18ecb9cbfbf9d72804fbc4 100644 --- a/src/account.cpp +++ b/src/account.cpp @@ -340,6 +340,12 @@ Account::getVolatileAccountDetails() const }; } +void +Account::onIsComposing(const std::string& peer, bool isComposing) +{ + emitSignal<DRing::ConfigurationSignal::ComposingStatusChanged>(accountID_, peer, isComposing ? 1 : 0); +} + bool Account::hasActiveCodec(MediaType mediaType) const { diff --git a/src/account.h b/src/account.h index e5271bab75670707f34541ef5e662090de32d9cd..37ea96e97631249d2f9d92ac1e44a6eb536e86f4 100644 --- a/src/account.h +++ b/src/account.h @@ -153,8 +153,12 @@ class Account : public Serializable, public std::enable_shared_from_this<Account * If supported, send a text message from this account. * @return a token to query the message status */ - virtual uint64_t sendTextMessage(const std::string& to UNUSED, - const std::map<std::string, std::string>& payloads UNUSED) { return 0; } + virtual uint64_t sendTextMessage(const std::string& /*to*/, + const std::map<std::string, std::string>& /*payloads*/) { return 0; } + + virtual void setIsComposing(const std::string& /*to*/, bool /*isWriting*/) {}; + + virtual void onIsComposing(const std::string& /*peer*/, bool /*isWriting*/); virtual std::vector<DRing::Message> getLastMessages(const uint64_t& /*base_timestamp*/) { return {}; diff --git a/src/client/configurationmanager.cpp b/src/client/configurationmanager.cpp index 14b6767eb7b2e03b317cd4c73b4d017ee9bcebaa..102756fb7e5a9345c821d96d2a86a5706cfd98ed 100644 --- a/src/client/configurationmanager.cpp +++ b/src/client/configurationmanager.cpp @@ -298,6 +298,13 @@ cancelMessage(const std::string& accountID, uint64_t messageId) return {}; } +void +setIsComposing(const std::string& accountID, const std::string& to, bool isWriting) +{ + if (const auto acc = jami::Manager::instance().getAccount(accountID)) + return acc->setIsComposing(to, isWriting); +} + bool exportOnRing(const std::string& accountID, const std::string& password) { diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp index 2e42aed62dfbc4613852d57fecbd35f246b17cb8..f61f1f44c28ac10a7e052cc0779a641a9620122e 100644 --- a/src/client/ring_signal.cpp +++ b/src/client/ring_signal.cpp @@ -61,6 +61,7 @@ getSignalHandlers() exported_callback<DRing::ConfigurationSignal::CertificateExpired>(), exported_callback<DRing::ConfigurationSignal::CertificateStateChanged>(), exported_callback<DRing::ConfigurationSignal::IncomingAccountMessage>(), + exported_callback<DRing::ConfigurationSignal::ComposingStatusChanged>(), exported_callback<DRing::ConfigurationSignal::AccountMessageStatusChanged>(), exported_callback<DRing::ConfigurationSignal::IncomingTrustRequest>(), exported_callback<DRing::ConfigurationSignal::ContactAdded>(), diff --git a/src/dring/configurationmanager_interface.h b/src/dring/configurationmanager_interface.h index 18d694cacb52159d63daaaf62da20d8f6ff69285..6169cd0f9a1b8dfdeaac6946b95ec51a714c243b 100644 --- a/src/dring/configurationmanager_interface.h +++ b/src/dring/configurationmanager_interface.h @@ -81,6 +81,7 @@ DRING_PUBLIC std::vector<Message> getLastMessages(const std::string& accountID, DRING_PUBLIC std::map<std::string, std::string> getNearbyPeers(const std::string& accountID); DRING_PUBLIC int getMessageStatus(uint64_t id); DRING_PUBLIC int getMessageStatus(const std::string& accountID, uint64_t id); +DRING_PUBLIC void setIsComposing(const std::string& accountID, const std::string& to, bool isWriting); DRING_PUBLIC std::map<std::string, std::string> getTlsDefaultSettings(); @@ -289,6 +290,10 @@ struct DRING_PUBLIC ConfigurationSignal { constexpr static const char* name = "AccountMessageStatusChanged"; using cb_type = void(const std::string& /*account_id*/, uint64_t /*message_id*/, const std::string& /*to*/, int /*state*/); }; + struct DRING_PUBLIC ComposingStatusChanged { + constexpr static const char* name = "ComposingStatusChanged"; + using cb_type = void(const std::string& /*account_id*/, const std::string& /*from*/, int /*status*/); + }; struct DRING_PUBLIC IncomingTrustRequest { constexpr static const char* name = "IncomingTrustRequest"; using cb_type = void(const std::string& /*account_id*/, const std::string& /*from*/, const std::vector<uint8_t>& payload, time_t received); diff --git a/src/im/instant_messaging.cpp b/src/im/instant_messaging.cpp index c6aebda124405206ab56deaffdf7bb78051d4cc9..12e271e7a83e932dfb7dba5511fabf2a2e217257 100644 --- a/src/im/instant_messaging.cpp +++ b/src/im/instant_messaging.cpp @@ -238,7 +238,7 @@ im::parseSipMessage(const pjsip_msg* msg) } // check if its a multipart message - pj_str_t typeMultipart {const_cast<char*>("multipart"), 9}; + constexpr pj_str_t typeMultipart {CONST_PJ_STR("multipart")}; if (pj_strcmp(&typeMultipart, &msg->body->content_type.type) != 0) { // treat as single content type message diff --git a/src/jamidht/jamiaccount.cpp b/src/jamidht/jamiaccount.cpp index 004b062021994d0433456d982cf2fe29331c0307..81fb11e130540906b288e172f1462b9a18b406b5 100644 --- a/src/jamidht/jamiaccount.cpp +++ b/src/jamidht/jamiaccount.cpp @@ -2908,6 +2908,16 @@ JamiAccount::sendTextMessage(const std::string& to, const std::map<std::string, }, std::chrono::steady_clock::now() + std::chrono::minutes(1)); } +void +JamiAccount::onIsComposing(const std::string& peer, bool isWriting) +{ + try { + Account::onIsComposing(parseJamiUri(peer), isWriting); + } catch (...) { + JAMI_ERR("[Account %s] Can't parse URI: %s", getAccountID().c_str(), peer.c_str()); + } +} + void JamiAccount::registerDhtAddress(IceTransport& ice) { diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h index 83287bfa329721537e867bda1dfe2b477d16e295..4e033f344c2636eec9aec1fb617e1d77451818c0 100644 --- a/src/jamidht/jamiaccount.h +++ b/src/jamidht/jamiaccount.h @@ -315,8 +315,9 @@ public: void sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload); void sendTrustRequestConfirm(const std::string& to); - virtual void sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads, uint64_t id, bool retryOnTimeout=true) override; - virtual uint64_t sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads) override; + void sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads, uint64_t id, bool retryOnTimeout=true) override; + uint64_t sendTextMessage(const std::string& to, const std::map<std::string, std::string>& payloads) override; + void onIsComposing(const std::string& peer, bool isWriting) override; /* Devices */ void addDevice(const std::string& password); diff --git a/src/sip/sipaccountbase.cpp b/src/sip/sipaccountbase.cpp index 9b9bf866780e1853884b55c324bba53485ebfe6a..e848211e13fec359dcfa58e0a16dda998d70ed33 100644 --- a/src/sip/sipaccountbase.cpp +++ b/src/sip/sipaccountbase.cpp @@ -32,22 +32,27 @@ #include "config/yamlparser.h" -#pragma GCC diagnostic push -#pragma GCC diagnostic ignored "-Wdeprecated-declarations" -#include <yaml-cpp/yaml.h> -#pragma GCC diagnostic pop - #include "client/ring_signal.h" #include "string_utils.h" #include "fileutils.h" #include "sip_utils.h" #include "utf8_utils.h" -#include <ctime> +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" +#include <yaml-cpp/yaml.h> +#pragma GCC diagnostic pop + #include <type_traits> +#include <regex> + +#include <ctime> namespace jami { +static constexpr const char MIME_TYPE_IM_COMPOSING[] {"application/im-iscomposing+xml"}; +static constexpr std::chrono::steady_clock::duration COMPOSING_TIMEOUT {std::chrono::seconds(12)}; + SIPAccountBase::SIPAccountBase(const std::string& accountID) : Account(accountID), messageEngine_(*this, fileutils::get_cache_dir()+DIR_SEPARATOR_STR+getAccountID()+DIR_SEPARATOR_STR "messages"), @@ -102,6 +107,53 @@ SIPAccountBase::flush() fileutils::remove(fileutils::get_cache_dir() + DIR_SEPARATOR_STR + getAccountID() + DIR_SEPARATOR_STR "messages"); } +std::string +getIsComposing(bool isWriting) +{ + // implementing https://tools.ietf.org/rfc/rfc3994.txt + std::ostringstream ss; + ss << "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" << std::endl + << "<isComposing><state>" << (isWriting ? "active" : "idle") << "</state></isComposing>"; + return ss.str(); +} + +void +SIPAccountBase::setIsComposing(const std::string& to, bool isWriting) +{ + if (not isWriting and to != composingUri_) + return; + + if (composingTimeout_) { + composingTimeout_->cancel(); + composingTimeout_.reset(); + } + if (isWriting) { + if (not composingUri_.empty() and composingUri_ != to) { + sendTextMessage(composingUri_, {{MIME_TYPE_IM_COMPOSING, getIsComposing(false)}}); + composingTime_ = std::chrono::steady_clock::time_point::min(); + } + composingUri_.clear(); + composingUri_.insert(composingUri_.end(), to.begin(), to.end()); + auto now = std::chrono::steady_clock::now(); + if (now >= composingTime_ + COMPOSING_TIMEOUT) { + sendTextMessage(composingUri_, {{MIME_TYPE_IM_COMPOSING, getIsComposing(true)}}); + composingTime_ = now; + } + std::weak_ptr<SIPAccountBase> weak = std::static_pointer_cast<SIPAccountBase>(shared_from_this()); + composingTimeout_ = Manager::instance().scheduleTask([weak, to](){ + if (auto sthis = weak.lock()) { + sthis->sendTextMessage(to, {{MIME_TYPE_IM_COMPOSING, getIsComposing(false)}}); + sthis->composingUri_.clear(); + sthis->composingTime_ = std::chrono::steady_clock::time_point::min(); + } + }, now + COMPOSING_TIMEOUT); + } else { + sendTextMessage(to, {{MIME_TYPE_IM_COMPOSING, getIsComposing(false)}}); + composingUri_.clear(); + composingTime_ = std::chrono::steady_clock::time_point::min(); + } +} + template <typename T> static void validate(std::string &member, const std::string ¶m, const T& valid) @@ -413,6 +465,22 @@ SIPAccountBase::onTextMessage(const std::string& from, JAMI_WARN("Dropping invalid message with MIME type %s", m.first.c_str()); return; } + if (m.first == MIME_TYPE_IM_COMPOSING) { + try { + static const std::regex COMPOSING_REGEX("<state>\\s*(\\w+)\\s*<\\/state>"); + std::smatch matched_pattern; + std::regex_search(m.second, matched_pattern, COMPOSING_REGEX); + bool isComposing {false}; + if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) { + isComposing = matched_pattern[1] == "active"; + } + onIsComposing(from, isComposing); + if (payloads.size() == 1) + return; + } catch (const std::exception& e) { + JAMI_WARN("Error parsing composing state: %s", e.what()); + } + } } emitSignal<DRing::ConfigurationSignal::IncomingAccountMessage>(accountID_, from, payloads); DRing::Message message; diff --git a/src/sip/sipaccountbase.h b/src/sip/sipaccountbase.h index 1ea26cca066dc89ab6818ee52dddbb167e7a8b91..79421c2eefea94806f087f4757f1807ee2ce8eec 100644 --- a/src/sip/sipaccountbase.h +++ b/src/sip/sipaccountbase.h @@ -31,8 +31,6 @@ #include "noncopyable.h" #include "im/message_engine.h" -#include <pjsip/sip_types.h> - #include <array> #include <deque> #include <map> @@ -40,6 +38,8 @@ #include <mutex> #include <vector> +extern "C" { +#include <pjsip/sip_types.h> #ifdef _WIN32 typedef uint16_t in_port_t; #else @@ -49,10 +49,12 @@ typedef uint16_t in_port_t; struct pjsip_dialog; struct pjsip_inv_session; struct pjmedia_sdp_session; +} namespace jami { class SipTransport; +class Task; namespace Conf { // SIP specific configuration keys @@ -267,6 +269,8 @@ public: return messageEngine_.sendMessage(to, payloads); } + void setIsComposing(const std::string& to, bool isWriting) override; + virtual im::MessageStatus getMessageStatus(uint64_t id) const override { return messageEngine_.getStatus(id); } @@ -462,6 +466,10 @@ protected: static constexpr size_t MAX_WAITING_MESSAGES_SIZE = 1000; std::deque<DRing::Message> lastMessages_; + std::string composingUri_; + std::chrono::steady_clock::time_point composingTime_ {std::chrono::steady_clock::time_point::min()}; + std::shared_ptr<Task> composingTimeout_; + mutable std::mutex cachedTurnMutex_ {}; std::unique_ptr<IpAddr> cacheTurnV4_ {}; std::unique_ptr<IpAddr> cacheTurnV6_ {};