LCOV - code coverage report
Current view: top level - src/jamidht - jamiaccount.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 72.9 % 3057 2228
Test Date: 2026-08-23 08:52:56 Functions: 82.4 % 330 272

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
       3              :  *
       4              :  * This program is free software: you can redistribute it and/or modify
       5              :  * it under the terms of the GNU General Public License as published by
       6              :  * the Free Software Foundation, either version 3 of the License, or
       7              :  * (at your option) any later version.
       8              :  *
       9              :  * This program is distributed in the hope that it will be useful,
      10              :  * but WITHOUT ANY WARRANTY; without even the implied warranty of
      11              :  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
      12              :  * GNU General Public License for more details.
      13              :  *
      14              :  * You should have received a copy of the GNU General Public License
      15              :  * along with this program. If not, see <https://www.gnu.org/licenses/>.
      16              :  */
      17              : 
      18              : #ifdef HAVE_CONFIG_H
      19              : #include "config.h"
      20              : #endif
      21              : 
      22              : #include "jamiaccount.h"
      23              : #include "presence_manager.h"
      24              : 
      25              : #include "logger.h"
      26              : 
      27              : #include "accountarchive.h"
      28              : #include "jami_contact.h"
      29              : #include "configkeys.h"
      30              : #include "contact_list.h"
      31              : #include "archive_account_manager.h"
      32              : #include "server_account_manager.h"
      33              : #include "jamidht/commit_message.h"
      34              : #include "jamidht/channeled_transport.h"
      35              : #include "jamidht/collaborative_editing.h"
      36              : #include "conversation_channel_handler.h"
      37              : #include "sync_channel_handler.h"
      38              : #include "ydoc_channel_handler.h"
      39              : #include "message_channel_handler.h"
      40              : #include "auth_channel_handler.h"
      41              : #include "transfer_channel_handler.h"
      42              : #include "swarm/swarm_channel_handler.h"
      43              : #include "service_manager.h"
      44              : #include "svc_discovery_channel_handler.h"
      45              : #include "svc_tunnel_channel_handler.h"
      46              : #include "jami/media_const.h"
      47              : 
      48              : #include "sip/sdp.h"
      49              : #include "sip/sipvoiplink.h"
      50              : #include "sip/sipcall.h"
      51              : #include "sip/siptransport.h"
      52              : #include "connectivity/sip_utils.h"
      53              : 
      54              : #include "uri.h"
      55              : 
      56              : #include "client/jami_signal.h"
      57              : #include "jami/call_const.h"
      58              : #include "jami/account_const.h"
      59              : 
      60              : #include "system_codec_container.h"
      61              : 
      62              : #include "account_schema.h"
      63              : #include "manager.h"
      64              : #include "connectivity/utf8_utils.h"
      65              : #include "connectivity/ip_utils.h"
      66              : 
      67              : #ifdef ENABLE_PLUGIN
      68              : #include "plugin/jamipluginmanager.h"
      69              : #include "plugin/chatservicesmanager.h"
      70              : #endif
      71              : 
      72              : #ifdef ENABLE_VIDEO
      73              : #include "libav_utils.h"
      74              : #endif
      75              : #include "fileutils.h"
      76              : #include "string_utils.h"
      77              : #include "archiver.h"
      78              : #include "data_transfer.h"
      79              : #include "json_utils.h"
      80              : 
      81              : #include "libdevcrypto/Common.h"
      82              : #include "base64.h"
      83              : #include "vcard.h"
      84              : #include "im/instant_messaging.h"
      85              : 
      86              : #include <dhtnet/ice_transport.h>
      87              : #include <dhtnet/ice_transport_factory.h>
      88              : #include <dhtnet/upnp/upnp_control.h>
      89              : #include <dhtnet/multiplexed_socket.h>
      90              : #include <dhtnet/certstore.h>
      91              : 
      92              : #include <opendht/thread_pool.h>
      93              : #include <opendht/peer_discovery.h>
      94              : #include <opendht/http.h>
      95              : 
      96              : #include <yaml-cpp/yaml.h>
      97              : #include <fmt/format.h>
      98              : 
      99              : #include <unistd.h>
     100              : 
     101              : #include <algorithm>
     102              : #include <array>
     103              : #include <cctype>
     104              : #include <charconv>
     105              : #include <cinttypes>
     106              : #include <cstdarg>
     107              : #include <fstream>
     108              : #include <initializer_list>
     109              : #include <memory>
     110              : #include <regex>
     111              : #include <sstream>
     112              : #include <string>
     113              : #include <system_error>
     114              : #include <utility>
     115              : 
     116              : using namespace std::placeholders;
     117              : 
     118              : namespace jami {
     119              : 
     120              : constexpr pj_str_t STR_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("Message-ID");
     121              : static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
     122              : static constexpr const char MIME_TYPE_PIDF[] {"application/pidf+xml"};
     123              : static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"};
     124              : static constexpr const char DEVICE_ID_PATH[] {"ring_device"};
     125              : static constexpr auto TREATED_PATH = "treatedImMessages"sv;
     126              : 
     127              : struct VCardMessageCtx
     128              : {
     129              :     std::shared_ptr<std::atomic_int> success;
     130              :     int total;
     131              :     std::string path;
     132              : };
     133              : 
     134              : namespace Migration {
     135              : 
     136              : enum class State { // Contains all the Migration states
     137              :     SUCCESS,
     138              :     INVALID
     139              : };
     140              : 
     141              : std::string
     142            4 : mapStateNumberToString(const State migrationState)
     143              : {
     144              : #define CASE_STATE(X) \
     145              :     case Migration::State::X: \
     146              :         return #X
     147              : 
     148            4 :     switch (migrationState) {
     149            0 :         CASE_STATE(INVALID);
     150           12 :         CASE_STATE(SUCCESS);
     151              :     }
     152            0 :     return {};
     153              : }
     154              : 
     155              : void
     156            4 : setState(const std::string& accountID, const State migrationState)
     157              : {
     158            4 :     emitSignal<libjami::ConfigurationSignal::MigrationEnded>(accountID, mapStateNumberToString(migrationState));
     159            4 : }
     160              : 
     161              : } // namespace Migration
     162              : 
     163              : struct JamiAccount::PendingCall
     164              : {
     165              :     std::chrono::steady_clock::time_point start;
     166              :     std::shared_ptr<IceTransport> ice_sp;
     167              :     std::shared_ptr<IceTransport> ice_tcp_sp;
     168              :     std::weak_ptr<SIPCall> call;
     169              :     std::future<size_t> listen_key;
     170              :     dht::InfoHash call_key;
     171              :     dht::InfoHash from;
     172              :     dht::InfoHash from_account;
     173              :     std::shared_ptr<dht::crypto::Certificate> from_cert;
     174              : };
     175              : 
     176              : struct JamiAccount::PendingMessage
     177              : {
     178              :     std::set<DeviceId> to;
     179              : };
     180              : 
     181              : struct AccountPeerInfo
     182              : {
     183              :     dht::InfoHash accountId;
     184              :     std::string displayName;
     185            0 :     MSGPACK_DEFINE(accountId, displayName)
     186              : };
     187              : 
     188              : struct JamiAccount::DiscoveredPeer
     189              : {
     190              :     std::string displayName;
     191              :     std::unique_ptr<asio::steady_timer> cleanupTimer;
     192              : };
     193              : 
     194              : /**
     195              :  * Track sending state for a single message to one or more devices.
     196              :  */
     197              : class JamiAccount::SendMessageContext
     198              : {
     199              : public:
     200              :     using OnComplete = std::function<void(bool, bool)>;
     201        14314 :     SendMessageContext(OnComplete onComplete)
     202        14314 :         : onComplete(std::move(onComplete))
     203        14314 :     {}
     204              :     /** Track new pending message for device */
     205        13358 :     bool add(const DeviceId& device)
     206              :     {
     207        13358 :         std::lock_guard lk(mtx);
     208        26715 :         return devices.insert(device).second;
     209        13358 :     }
     210              :     /** Call after all messages are sent */
     211        14314 :     void start()
     212              :     {
     213        14314 :         std::unique_lock lk(mtx);
     214        14314 :         started = true;
     215        14314 :         checkComplete(lk);
     216        14314 :     }
     217              :     /** Complete pending message for device */
     218        12403 :     bool complete(const DeviceId& device, bool success)
     219              :     {
     220        12403 :         std::unique_lock lk(mtx);
     221        12404 :         if (devices.erase(device) == 0)
     222            0 :             return false;
     223        12403 :         ++completeCount;
     224        12403 :         if (success)
     225        12399 :             ++successCount;
     226        12403 :         checkComplete(lk);
     227        12402 :         return true;
     228        12402 :     }
     229              :     bool empty() const
     230              :     {
     231              :         std::lock_guard lk(mtx);
     232              :         return devices.empty();
     233              :     }
     234         2465 :     bool pending(const DeviceId& device) const
     235              :     {
     236         2465 :         std::lock_guard lk(mtx);
     237         4930 :         return devices.find(device) != devices.end();
     238         2465 :     }
     239              : 
     240              : private:
     241              :     mutable std::mutex mtx;
     242              :     OnComplete onComplete;
     243              :     std::set<DeviceId> devices;
     244              :     unsigned completeCount = 0;
     245              :     unsigned successCount = 0;
     246              :     bool started {false};
     247              : 
     248        26715 :     void checkComplete(std::unique_lock<std::mutex>& lk)
     249              :     {
     250        26715 :         if (started && (devices.empty() || successCount)) {
     251        14312 :             if (onComplete) {
     252        14311 :                 auto cb = std::move(onComplete);
     253        14312 :                 auto success = successCount != 0;
     254        14312 :                 auto complete = completeCount != 0;
     255        14312 :                 onComplete = {};
     256        14312 :                 lk.unlock();
     257        14313 :                 cb(success, complete);
     258        14314 :             }
     259              :         }
     260        26712 :     }
     261              : };
     262              : 
     263              : static const constexpr std::string_view RING_URI_PREFIX = "ring:";
     264              : static const constexpr std::string_view JAMI_URI_PREFIX = "jami:";
     265              : static const auto PROXY_REGEX = std::regex("(https?://)?([\\w\\.\\-_\\~]+)(:(\\d+)|:\\[(.+)-(.+)\\])?");
     266              : static const constexpr std::string_view PEER_DISCOVERY_JAMI_SERVICE = "jami";
     267              : const constexpr auto PEER_DISCOVERY_EXPIRATION = std::chrono::minutes(1);
     268              : 
     269              : using ValueIdDist = std::uniform_int_distribution<dht::Value::Id>;
     270              : 
     271              : std::string_view
     272        40055 : stripPrefix(std::string_view toUrl)
     273              : {
     274        40055 :     auto dhtf = toUrl.find(RING_URI_PREFIX);
     275        40048 :     if (dhtf != std::string_view::npos) {
     276            0 :         dhtf += RING_URI_PREFIX.size();
     277              :     } else {
     278        40048 :         dhtf = toUrl.find(JAMI_URI_PREFIX);
     279        40062 :         if (dhtf != std::string_view::npos) {
     280            0 :             dhtf += JAMI_URI_PREFIX.size();
     281              :         } else {
     282        40062 :             dhtf = toUrl.find("sips:");
     283        40045 :             dhtf = (dhtf == std::string_view::npos) ? 0 : dhtf + 5;
     284              :         }
     285              :     }
     286        40045 :     while (dhtf < toUrl.length() && toUrl[dhtf] == '/')
     287            0 :         dhtf++;
     288        40055 :     return toUrl.substr(dhtf);
     289              : }
     290              : 
     291              : std::string_view
     292        40030 : parseJamiUri(std::string_view toUrl)
     293              : {
     294        40030 :     auto sufix = stripPrefix(toUrl);
     295        40075 :     if (sufix.length() < 40)
     296            0 :         throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
     297              : 
     298        40072 :     const std::string_view toUri = sufix.substr(0, 40);
     299        40074 :     if (std::find_if_not(toUri.cbegin(), toUri.cend(), ::isxdigit) != toUri.cend())
     300            0 :         throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
     301        40133 :     return toUri;
     302              : }
     303              : 
     304              : static constexpr std::string_view
     305         4402 : dhtStatusStr(dht::NodeStatus status)
     306              : {
     307              :     return status == dht::NodeStatus::Connected
     308         4402 :                ? "connected"sv
     309         4402 :                : (status == dht::NodeStatus::Connecting ? "connecting"sv : "disconnected"sv);
     310              : }
     311              : 
     312          823 : JamiAccount::JamiAccount(const std::string& accountId)
     313              :     : SIPAccountBase(accountId)
     314          823 :     , cachePath_(fileutils::get_cache_dir() / accountId)
     315          823 :     , dataPath_(cachePath_ / "values")
     316         1646 :     , logger_(Logger::dhtLogger(fmt::format("Account {}", accountId)))
     317          823 :     , certStore_ {std::make_shared<dhtnet::tls::CertificateStore>(idPath_, logger_)}
     318          823 :     , dht_(std::make_shared<dht::DhtRunner>())
     319          823 :     , treatedMessages_(cachePath_ / TREATED_PATH)
     320          823 :     , presenceManager_(std::make_unique<PresenceManager>(dht_))
     321          823 :     , connectionManager_ {}
     322         6584 :     , nonSwarmTransferManager_()
     323              : {
     324          823 :     presenceListenerToken_ = presenceManager_->addListener([this](const std::string& uri, bool online) {
     325          656 :         runOnMainThread([w = weak(), uri, online] {
     326          656 :             if (auto sthis = w.lock()) {
     327          656 :                 if (online) {
     328          601 :                     sthis->onTrackedBuddyOnline(uri);
     329          601 :                     sthis->messageEngine_.onPeerOnline(uri);
     330              :                 } else {
     331           55 :                     sthis->onTrackedBuddyOffline(uri);
     332              :                 }
     333          656 :             }
     334          656 :         });
     335          656 :     });
     336              :     // When a device of a contact that advertises services changes presence,
     337              :     // re-publish that peer's service list with refreshed availability so an
     338              :     // already-open services menu greys out / re-enables entries live.
     339          823 :     svcPresenceListenerToken_ = presenceManager_->addDeviceListener([this](const std::string& uri,
     340              :                                                                            const dht::PkId&,
     341              :                                                                            bool) {
     342          733 :         runOnMainThread([w = weak(), uri] {
     343          733 :             auto sthis = w.lock();
     344          733 :             if (!sthis)
     345            0 :                 return;
     346          733 :             auto servicesJson = sthis->buildPeerServicesJson(uri);
     347          733 :             if (servicesJson.empty())
     348          733 :                 return;
     349            0 :             emitSignal<libjami::ServiceSignal::PeerServicesReceived>(0u,
     350            0 :                                                                      sthis->getAccountID(),
     351            0 :                                                                      uri,
     352              :                                                                      static_cast<int>(
     353              :                                                                          libjami::ServiceSignal::PeerServicesStatus::OK),
     354              :                                                                      servicesJson);
     355         1466 :         });
     356          733 :     });
     357          823 : }
     358              : 
     359         1646 : JamiAccount::~JamiAccount() noexcept
     360              : {
     361          823 :     if (dht_)
     362          823 :         dht_->join();
     363          823 : }
     364              : 
     365              : void
     366          840 : JamiAccount::shutdownConnections()
     367              : {
     368          840 :     JAMI_LOG("[Account {}] Shutdown connections", getAccountID());
     369              : 
     370          840 :     decltype(gitServers_) gservers;
     371              :     {
     372          840 :         std::lock_guard lk(gitServersMtx_);
     373          840 :         gservers = std::move(gitServers_);
     374          840 :     }
     375         1329 :     for (auto& [_id, gs] : gservers)
     376          974 :         dht::ThreadPool::io().run([gs = std::shared_ptr(std::move(gs))] { gs->stop(); });
     377          840 :     gservers.clear();
     378              : 
     379              :     {
     380          840 :         std::lock_guard lk(connManagerMtx_);
     381              :         // Just move destruction on another thread.
     382          840 :         dht::ThreadPool::io().run([conMgr = std::shared_ptr(std::move(connectionManager_))] {});
     383          840 :         connectionManager_.reset();
     384          840 :         channelHandlers_.clear();
     385          840 :     }
     386          840 :     if (convModule_) {
     387          741 :         convModule_->shutdownConnections();
     388              :     }
     389              : 
     390          840 :     std::lock_guard lk(sipConnsMtx_);
     391          840 :     sipConns_.clear();
     392          840 : }
     393              : 
     394              : void
     395          818 : JamiAccount::flush()
     396              : {
     397              :     // Class base method
     398          818 :     SIPAccountBase::flush();
     399              : 
     400          818 :     dhtnet::fileutils::removeAll(cachePath_);
     401          818 :     dhtnet::fileutils::removeAll(dataPath_);
     402          818 :     dhtnet::fileutils::removeAll(idPath_, true);
     403          818 : }
     404              : 
     405              : std::shared_ptr<SIPCall>
     406           97 : JamiAccount::newIncomingCall(const std::string& from,
     407              :                              const std::vector<libjami::MediaMap>& mediaList,
     408              :                              const std::shared_ptr<SipTransport>& sipTransp)
     409              : {
     410           97 :     JAMI_DEBUG("New incoming call from {:s} with {:d} media", from, mediaList.size());
     411              : 
     412           97 :     if (sipTransp) {
     413           97 :         auto call = Manager::instance().callFactory.newSipCall(shared(), Call::CallType::INCOMING, mediaList);
     414           97 :         call->setPeerUri(JAMI_URI_PREFIX + from);
     415           97 :         call->setPeerNumber(from);
     416              : 
     417           97 :         call->setSipTransport(sipTransp, getContactHeader(sipTransp));
     418              : 
     419           97 :         return call;
     420           97 :     }
     421              : 
     422            0 :     JAMI_ERROR("newIncomingCall: unable to find matching call for {}", from);
     423            0 :     return nullptr;
     424              : }
     425              : 
     426              : std::shared_ptr<Call>
     427          111 : JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::MediaMap>& mediaList)
     428              : {
     429          111 :     auto uri = Uri(toUrl);
     430          111 :     if (uri.scheme() == Uri::Scheme::SWARM || uri.scheme() == Uri::Scheme::RENDEZVOUS) {
     431              :         // NOTE: In this case newOutgoingCall can act as "resumeConference" and just attach the
     432              :         // host to the current hosted conference. So, no call will be returned in that case.
     433           22 :         return newSwarmOutgoingCallHelper(uri, mediaList);
     434              :     }
     435              : 
     436           89 :     auto& manager = Manager::instance();
     437           89 :     std::shared_ptr<SIPCall> call;
     438              : 
     439              :     // SIP allows sending empty invites, this use case is not used with Jami accounts.
     440           89 :     if (not mediaList.empty()) {
     441           33 :         call = manager.callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
     442              :     } else {
     443           56 :         JAMI_WARNING("Media list is empty, setting a default list");
     444          112 :         call = manager.callFactory.newSipCall(shared(),
     445              :                                               Call::CallType::OUTGOING,
     446          112 :                                               MediaAttribute::mediaAttributesToMediaMaps(
     447          168 :                                                   createDefaultMediaList(isVideoEnabled())));
     448              :     }
     449              : 
     450           89 :     if (not call)
     451            0 :         return {};
     452              : 
     453           89 :     std::shared_lock lkCM(connManagerMtx_);
     454           89 :     if (!connectionManager_)
     455            0 :         return {};
     456              : 
     457           89 :     connectionManager_->getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
     458           89 :         if (call->isIceEnabled()) {
     459           89 :             if (not call->createIceMediaTransport(false)
     460          178 :                 or not call->initIceMediaTransport(true, std::forward<dhtnet::IceTransportOptions>(opts))) {
     461            0 :                 return;
     462              :             }
     463              :         }
     464           89 :         auto shared = w.lock();
     465           89 :         if (!shared)
     466            0 :             return;
     467           89 :         JAMI_LOG("New outgoing call with {}", uri.toString());
     468           89 :         call->setPeerNumber(uri.authority());
     469           89 :         call->setPeerUri(uri.toString());
     470              : 
     471           89 :         shared->newOutgoingCallHelper(call, uri);
     472           89 :     });
     473              : 
     474           89 :     return call;
     475          111 : }
     476              : 
     477              : void
     478           89 : JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri)
     479              : {
     480           89 :     JAMI_LOG("[Account {}] Calling peer {}", getAccountID(), uri.authority());
     481              :     try {
     482           89 :         startOutgoingCall(call, uri.authority());
     483            0 :     } catch (const std::invalid_argument&) {
     484            0 :         auto suffix = stripPrefix(uri.toString());
     485            0 :         NameDirectory::lookupUri(suffix,
     486            0 :                                  config().nameServer,
     487            0 :                                  [wthis_ = weak(), call](const std::string& regName,
     488              :                                                          const std::string& address,
     489              :                                                          NameDirectory::Response response) {
     490              :                                      // we may run inside an unknown thread, but following code must
     491              :                                      // be called in main thread
     492            0 :                                      runOnMainThread([wthis_, regName, address, response, call]() {
     493            0 :                                          if (response != NameDirectory::Response::found) {
     494            0 :                                              call->onFailure(PJSIP_SC_NOT_FOUND);
     495            0 :                                              return;
     496              :                                          }
     497            0 :                                          if (auto sthis = wthis_.lock()) {
     498              :                                              try {
     499            0 :                                                  sthis->startOutgoingCall(call, address);
     500            0 :                                              } catch (const std::invalid_argument&) {
     501            0 :                                                  call->onFailure(PJSIP_SC_NOT_FOUND);
     502            0 :                                              }
     503              :                                          } else {
     504            0 :                                              call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
     505            0 :                                          }
     506              :                                      });
     507            0 :                                  });
     508            0 :     }
     509           89 : }
     510              : 
     511              : std::shared_ptr<SIPCall>
     512           22 : JamiAccount::newSwarmOutgoingCallHelper(const Uri& uri, const std::vector<libjami::MediaMap>& mediaList)
     513              : {
     514           22 :     JAMI_DEBUG("[Account {}] Calling conversation {}", getAccountID(), uri.authority());
     515              :     return convModule()
     516           22 :         ->call(uri.authority(), mediaList, [this, uri](const auto& accountUri, const auto& deviceId, const auto& call) {
     517           11 :             if (!call)
     518            0 :                 return;
     519              : 
     520           11 :             std::string peerId = accountUri;
     521           11 :             if (uri.scheme() == Uri::Scheme::RENDEZVOUS) {
     522            9 :                 auto parts = jami::split_string(accountUri, '/');
     523            9 :                 if (parts.size() == 4)
     524           18 :                     peerId = std::string(parts[1]);
     525            9 :             }
     526              : 
     527           11 :             std::unique_lock lkSipConn(sipConnsMtx_);
     528           12 :             for (auto& [key, value] : sipConns_) {
     529            1 :                 if (key.first != peerId || key.second != deviceId)
     530            0 :                     continue;
     531            1 :                 if (value.empty())
     532            0 :                     continue;
     533            1 :                 auto& sipConn = value.back();
     534              : 
     535            1 :                 if (!sipConn.channel) {
     536            0 :                     JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
     537            0 :                     continue;
     538              :                 }
     539              : 
     540            1 :                 auto transport = sipConn.transport;
     541            1 :                 if (!transport or !sipConn.channel)
     542            0 :                     continue;
     543            1 :                 call->setState(Call::ConnectionState::PROGRESSING);
     544            1 :                 call->setSipTransport(transport, getContactHeader(transport));
     545              : 
     546            1 :                 auto remoted_address = sipConn.channel->getRemoteAddress();
     547              :                 try {
     548            1 :                     onConnectedOutgoingCall(call, uri.authority(), remoted_address);
     549            1 :                     return;
     550            0 :                 } catch (const VoipLinkException&) {
     551              :                     // In this case, the main scenario is that SIPStartCall failed because
     552              :                     // the ICE is dead and the TLS session didn't send any packet on that dead
     553              :                     // link (connectivity change, killed by the operating system, etc)
     554              :                     // Here, we don't need to do anything, the TLS will fail and will delete
     555              :                     // the cached transport
     556            0 :                     continue;
     557              :                 }
     558              :             }
     559           10 :             lkSipConn.unlock();
     560              :             {
     561           10 :                 std::lock_guard lkP(pendingCallsMutex_);
     562           10 :                 pendingCalls_[deviceId].emplace_back(call);
     563           10 :             }
     564              : 
     565              :             // Else, ask for a channel (for future calls/text messages)
     566           10 :             auto type = call->hasVideo() ? "videoCall" : "audioCall";
     567           10 :             JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
     568           20 :             requestSIPConnection(peerId, deviceId, type, true, call);
     569           56 :         });
     570              : }
     571              : 
     572              : void
     573           10 : JamiAccount::handleIncomingConversationCall(const std::string& callId, const std::string& destination)
     574              : {
     575           10 :     auto split = jami::split_string(destination, '/');
     576           10 :     if (split.size() != 4)
     577            0 :         return;
     578           20 :     auto conversationId = std::string(split[0]);
     579           20 :     auto accountUri = std::string(split[1]);
     580           20 :     auto deviceId = std::string(split[2]);
     581           10 :     auto confId = std::string(split[3]);
     582              : 
     583           10 :     if (getUsername() != accountUri || currentDeviceId() != deviceId)
     584            0 :         return;
     585              : 
     586              :     // Avoid concurrent checks in this part
     587           10 :     std::lock_guard lk(rdvMtx_);
     588           10 :     auto isNotHosting = !convModule()->isHosting(conversationId, confId);
     589           10 :     if (confId == "0") {
     590            1 :         auto currentCalls = convModule()->getActiveCalls(conversationId);
     591            1 :         if (!currentCalls.empty()) {
     592            0 :             confId = currentCalls[0]["id"];
     593            0 :             isNotHosting = false;
     594              :         } else {
     595            1 :             confId = callId;
     596            1 :             JAMI_DEBUG("No active call to join, create conference");
     597              :         }
     598            1 :     }
     599           10 :     auto preferences = convModule()->getConversationPreferences(conversationId);
     600           10 :     auto canHost = true;
     601              : #if defined(__ANDROID__) || defined(__APPLE__)
     602              :     // By default, mobile devices SHOULD NOT host conferences.
     603              :     canHost = false;
     604              : #endif
     605           10 :     auto itPref = preferences.find(ConversationPreferences::HOST_CONFERENCES);
     606           10 :     if (itPref != preferences.end()) {
     607            0 :         canHost = itPref->second == TRUE_STR;
     608              :     }
     609              : 
     610           10 :     auto call = getCall(callId);
     611           10 :     if (!call) {
     612            0 :         JAMI_ERROR("Call {} not found", callId);
     613            0 :         return;
     614              :     }
     615              : 
     616           10 :     if (isNotHosting && !canHost) {
     617            0 :         JAMI_DEBUG("Request for hosting a conference declined");
     618            0 :         Manager::instance().hangupCall(getAccountID(), callId);
     619            0 :         return;
     620              :     }
     621              :     // Due to the fact that in a conference, the host is not the one who
     622              :     // provides the initial sdp offer, the following block of code is responsible
     623              :     // for handling the medialist that the host will form his response with.
     624              :     // We always want the hosts response to be the same length as that of the
     625              :     // peer who is asking to join (providing the offer). A priori though the peer
     626              :     // doesn't know what active media streams the host will have so we deal with the
     627              :     // possible cases here.
     628           10 :     std::shared_ptr<Conference> conf;
     629           10 :     std::vector<libjami::MediaMap> currentMediaList;
     630           10 :     if (!isNotHosting) {
     631            7 :         conf = getConference(confId);
     632            7 :         if (!conf) {
     633            0 :             JAMI_ERROR("[conf:{}] Conference not found", confId);
     634            0 :             return;
     635              :         }
     636            7 :         auto hostMedias = conf->currentMediaList();
     637            7 :         auto sipCall = std::dynamic_pointer_cast<SIPCall>(call);
     638            7 :         if (hostMedias.empty()) {
     639            0 :             currentMediaList = MediaAttribute::mediaAttributesToMediaMaps(
     640            0 :                 createDefaultMediaList(call->hasVideo(), true));
     641            7 :         } else if (hostMedias.size() < sipCall->getRtpSessionList().size()) {
     642              :             // First case: host has less media streams than the other person is joining
     643              :             // with. We need to add video media to the host before accepting the offer
     644              :             // This can happen if we host an audio call and someone joins with video
     645            0 :             currentMediaList = hostMedias;
     646            0 :             currentMediaList.push_back(
     647              :                 {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO},
     648              :                  {libjami::Media::MediaAttributeKey::ENABLED, TRUE_STR},
     649              :                  {libjami::Media::MediaAttributeKey::MUTED, TRUE_STR},
     650              :                  {libjami::Media::MediaAttributeKey::SOURCE, ""},
     651              :                  {libjami::Media::MediaAttributeKey::LABEL, "video_0"}});
     652              :         } else {
     653            7 :             bool hasVideo = false;
     654            7 :             if (sipCall) {
     655            7 :                 const auto rtpSessions = sipCall->getRtpSessionList();
     656            7 :                 hasVideo = std::any_of(rtpSessions.begin(), rtpSessions.end(), [](const auto& session) {
     657           13 :                     return session && session->getMediaType() == MediaType::MEDIA_VIDEO;
     658              :                 });
     659            7 :             }
     660              :             // The second case is that the host has the same or more media
     661              :             // streams than the person joining. In this case we match all their
     662              :             // medias to form our offer. They will then potentially join the call without seeing
     663              :             // seeing all of our medias. For now we deal with this by calling a
     664              :             // requestmediachange once they've joined.
     665           14 :             for (const auto& m : conf->currentMediaList()) {
     666              :                 // We only expect to have 1 audio stream, add it.
     667           26 :                 if (m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE) == libjami::Media::MediaAttributeValue::AUDIO) {
     668            7 :                     currentMediaList.emplace_back(m);
     669            6 :                 } else if (hasVideo
     670           24 :                            && m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE)
     671            6 :                                   == libjami::Media::MediaAttributeValue::VIDEO) {
     672            6 :                     currentMediaList.emplace_back(m);
     673            6 :                     break;
     674              :                 }
     675            7 :             }
     676              :         }
     677            7 :     }
     678           10 :     Manager::instance().acceptCall(*call, currentMediaList);
     679              : 
     680           10 :     if (isNotHosting) {
     681            3 :         JAMI_DEBUG("Creating conference for swarm {} with ID {}", conversationId, confId);
     682              :         // Create conference and host it.
     683            3 :         convModule()->hostConference(conversationId, confId, callId);
     684              :     } else {
     685            7 :         JAMI_DEBUG("Adding participant {} for swarm {} with ID {}", callId, conversationId, confId);
     686            7 :         Manager::instance().addAudio(*call);
     687            7 :         conf->addSubCall(callId);
     688            7 :         emitSignal<libjami::CallSignal::ConferenceChanged>(getAccountID(), conf->getConfId(), conf->getStateStr());
     689              :     }
     690           10 : }
     691              : 
     692              : std::shared_ptr<SIPCall>
     693          178 : JamiAccount::createSubCall(const std::shared_ptr<SIPCall>& mainCall)
     694              : {
     695          178 :     auto mediaList = MediaAttribute::mediaAttributesToMediaMaps(mainCall->getMediaAttributeList());
     696          356 :     return Manager::instance().callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
     697          178 : }
     698              : 
     699              : void
     700           89 : JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::string& toUri)
     701              : {
     702           89 :     if (not accountManager_ or not dht_) {
     703            0 :         call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
     704            0 :         return;
     705              :     }
     706              : 
     707              :     // TODO: for now, we automatically trust all explicitly called peers
     708           89 :     setCertificateStatus(toUri, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
     709              : 
     710           89 :     call->setState(Call::ConnectionState::TRYING);
     711           89 :     std::weak_ptr<SIPCall> wCall = call;
     712              : 
     713          178 :     accountManager_->lookupAddress(toUri,
     714          178 :                                    [wCall](const std::string& regName,
     715              :                                            const std::string& /*address*/,
     716              :                                            const NameDirectory::Response& response) {
     717           89 :                                        if (response == NameDirectory::Response::found)
     718            1 :                                            if (auto call = wCall.lock()) {
     719            1 :                                                call->setPeerRegisteredName(regName);
     720            1 :                                            }
     721           89 :                                    });
     722              : 
     723           89 :     dht::InfoHash peer_account(toUri);
     724           89 :     if (!peer_account) {
     725            0 :         throw std::invalid_argument("Invalid peer account: " + toUri);
     726              :     }
     727              : 
     728              :     // Call connected devices
     729           89 :     std::set<DeviceId> devices;
     730           89 :     std::unique_lock lkSipConn(sipConnsMtx_);
     731              :     // NOTE: dummyCall is a call used to avoid to mark the call as failed if the
     732              :     // cached connection is failing with ICE (close event still not detected).
     733           89 :     auto dummyCall = createSubCall(call);
     734              : 
     735           89 :     if (!dummyCall) {
     736            0 :         call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
     737            0 :         return;
     738              :     }
     739              : 
     740           89 :     call->addSubCall(*dummyCall);
     741           89 :     dummyCall->setIceMedia(call->getIceMedia());
     742          263 :     auto sendRequest = [this, wCall, toUri, dummyCall = std::move(dummyCall)](const DeviceId& deviceId,
     743              :                                                                               bool eraseDummy) {
     744          174 :         if (eraseDummy) {
     745              :             // Mark the temp call as failed to stop the main call if necessary
     746           89 :             if (dummyCall)
     747           89 :                 dummyCall->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
     748           89 :             return;
     749              :         }
     750           85 :         auto call = wCall.lock();
     751           85 :         if (not call)
     752            0 :             return;
     753           85 :         auto state = call->getConnectionState();
     754           85 :         if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING)
     755            0 :             return;
     756              : 
     757           85 :         auto dev_call = createSubCall(call);
     758           85 :         dev_call->setPeerNumber(call->getPeerNumber());
     759           85 :         dev_call->setState(Call::ConnectionState::TRYING);
     760           85 :         call->addStateListener([w = weak(), deviceId](Call::CallState, Call::ConnectionState state, int) {
     761          251 :             if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
     762           85 :                 if (auto shared = w.lock())
     763           85 :                     shared->callConnectionClosed(deviceId, true);
     764           85 :                 return false;
     765              :             }
     766          166 :             return true;
     767              :         });
     768           85 :         call->addSubCall(*dev_call);
     769           85 :         dev_call->setIceMedia(call->getIceMedia());
     770              :         {
     771           85 :             std::lock_guard lk(pendingCallsMutex_);
     772           85 :             pendingCalls_[deviceId].emplace_back(dev_call);
     773           85 :         }
     774              : 
     775           85 :         JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
     776              :         // Else, ask for a channel (for future calls/text messages)
     777           85 :         const auto* type = call->hasVideo() ? "videoCall" : "audioCall";
     778          170 :         requestSIPConnection(toUri, deviceId, type, true, dev_call);
     779          174 :     };
     780              : 
     781           89 :     std::vector<std::shared_ptr<dhtnet::ChannelSocket>> channels;
     782          141 :     for (auto& [key, value] : sipConns_) {
     783           52 :         if (key.first != toUri)
     784           48 :             continue;
     785            4 :         if (value.empty())
     786            0 :             continue;
     787            4 :         auto& sipConn = value.back();
     788              : 
     789            4 :         if (!sipConn.channel) {
     790            0 :             JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
     791            0 :             continue;
     792              :         }
     793              : 
     794            4 :         auto transport = sipConn.transport;
     795            4 :         auto remote_address = sipConn.channel->getRemoteAddress();
     796            4 :         if (!transport or !remote_address)
     797            0 :             continue;
     798              : 
     799            4 :         channels.emplace_back(sipConn.channel);
     800              : 
     801            4 :         JAMI_WARNING("[call {}] A channeled socket is detected with this peer.", call->getCallId());
     802              : 
     803            4 :         auto dev_call = createSubCall(call);
     804            4 :         dev_call->setPeerNumber(call->getPeerNumber());
     805            4 :         dev_call->setSipTransport(transport, getContactHeader(transport));
     806            4 :         call->addSubCall(*dev_call);
     807            4 :         dev_call->setIceMedia(call->getIceMedia());
     808              : 
     809              :         // Set the call in PROGRESSING State because the ICE session
     810              :         // is already ready. Note that this line should be after
     811              :         // addSubcall() to change the state of the main call
     812              :         // and avoid to get an active call in a TRYING state.
     813            4 :         dev_call->setState(Call::ConnectionState::PROGRESSING);
     814              : 
     815              :         {
     816            4 :             std::lock_guard lk(onConnectionClosedMtx_);
     817            4 :             onConnectionClosed_[key.second] = sendRequest;
     818            4 :         }
     819              : 
     820            4 :         call->addStateListener([w = weak(), deviceId = key.second](Call::CallState, Call::ConnectionState state, int) {
     821           12 :             if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
     822            4 :                 if (auto shared = w.lock())
     823            4 :                     shared->callConnectionClosed(deviceId, true);
     824            4 :                 return false;
     825              :             }
     826            8 :             return true;
     827              :         });
     828              : 
     829              :         try {
     830            4 :             onConnectedOutgoingCall(dev_call, toUri, remote_address);
     831            0 :         } catch (const VoipLinkException&) {
     832              :             // In this case, the main scenario is that SIPStartCall failed because
     833              :             // the ICE is dead and the TLS session didn't send any packet on that dead
     834              :             // link (connectivity change, killed by the os, etc)
     835              :             // Here, we don't need to do anything, the TLS will fail and will delete
     836              :             // the cached transport
     837            0 :             continue;
     838            0 :         }
     839            4 :         devices.emplace(key.second);
     840            4 :     }
     841              : 
     842           89 :     lkSipConn.unlock();
     843              :     // Note: Send beacon can destroy the socket (if storing last occurence of shared_ptr)
     844              :     // causing sipConn to be destroyed. So, do it while sipConns_ not locked.
     845           93 :     for (const auto& channel : channels)
     846            4 :         channel->sendBeacon();
     847              : 
     848              :     // Find listening devices for this account
     849          267 :     accountManager_->forEachDevice(
     850              :         peer_account,
     851          178 :         [this, devices = std::move(devices), sendRequest](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
     852              :             // Test if already sent via a SIP transport
     853           89 :             auto deviceId = dev->getLongId();
     854           89 :             if (devices.find(deviceId) != devices.end())
     855            4 :                 return;
     856              :             {
     857           85 :                 std::lock_guard lk(onConnectionClosedMtx_);
     858           85 :                 onConnectionClosed_[deviceId] = sendRequest;
     859           85 :             }
     860           85 :             sendRequest(deviceId, false);
     861              :         },
     862          178 :         [wCall](bool ok) {
     863           89 :             if (not ok) {
     864            1 :                 if (auto call = wCall.lock()) {
     865            1 :                     JAMI_WARNING("[call:{}] No devices found", call->getCallId());
     866              :                     // Note: if a P2P connection exists, the call will be at least in CONNECTING
     867            1 :                     if (call->getConnectionState() == Call::ConnectionState::TRYING)
     868            1 :                         call->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
     869            1 :                 }
     870              :             }
     871           89 :         });
     872           89 : }
     873              : 
     874              : void
     875           97 : JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call,
     876              :                                      const std::string& to_id,
     877              :                                      dhtnet::IpAddr target)
     878              : {
     879           97 :     if (!call)
     880            0 :         return;
     881           97 :     JAMI_LOG("[call:{}] Outgoing call connected to {}", call->getCallId(), to_id);
     882              : 
     883           97 :     const auto localAddress = dhtnet::ip_utils::getInterfaceAddr(getLocalInterface(), target.getFamily());
     884              : 
     885           97 :     dhtnet::IpAddr addrSdp = getPublishedSameasLocal() ? localAddress
     886           97 :                                                        : connectionManager_->getPublishedIpAddress(target.getFamily());
     887              : 
     888              :     // fallback on local address
     889           97 :     if (not addrSdp)
     890            0 :         addrSdp = localAddress;
     891              : 
     892              :     // Building the local SDP offer
     893           97 :     auto& sdp = call->getSDP();
     894              : 
     895           97 :     sdp.setPublishedIP(addrSdp);
     896              : 
     897           97 :     auto mediaAttrList = call->getMediaAttributeList();
     898           97 :     if (mediaAttrList.empty()) {
     899            0 :         JAMI_ERROR("[call:{}] No media. Abort!", call->getCallId());
     900            0 :         return;
     901              :     }
     902              : 
     903           97 :     if (not sdp.createOffer(mediaAttrList)) {
     904            0 :         JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
     905            0 :         return;
     906              :     }
     907              : 
     908              :     // Note: pj_ice_strans_create can call onComplete in the same thread
     909              :     // This means that iceMutex_ in IceTransport can be locked when onInitDone is called
     910              :     // So, we need to run the call creation in the main thread
     911              :     // Also, we do not directly call SIPStartCall before receiving onInitDone, because
     912              :     // there is an inside waitForInitialization that can block the thread.
     913              :     // Note: avoid runMainThread as SIPStartCall use transportMutex
     914           97 :     dht::ThreadPool::io().run([w = weak(), call = std::move(call), target] {
     915           97 :         auto account = w.lock();
     916           97 :         if (not account)
     917            0 :             return;
     918              : 
     919           97 :         if (not account->SIPStartCall(*call, target)) {
     920            0 :             JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
     921              :         }
     922           97 :     });
     923           97 : }
     924              : 
     925              : bool
     926           97 : JamiAccount::SIPStartCall(SIPCall& call, const dhtnet::IpAddr& target)
     927              : {
     928           97 :     JAMI_LOG("[call:{}] Start SIP call", call.getCallId());
     929              : 
     930           97 :     if (call.isIceEnabled())
     931           97 :         call.addLocalIceAttributes();
     932              : 
     933              :     std::string toUri(
     934           97 :         getToUri(call.getPeerNumber() + "@" + target.toString(true))); // expecting a fully well formed sip uri
     935              : 
     936           97 :     pj_str_t pjTo = sip_utils::CONST_PJ_STR(toUri);
     937              : 
     938              :     // Create the from header
     939           97 :     std::string from(getFromUri());
     940           97 :     pj_str_t pjFrom = sip_utils::CONST_PJ_STR(from);
     941              : 
     942           97 :     std::string targetStr = getToUri(target.toString(true));
     943           97 :     pj_str_t pjTarget = sip_utils::CONST_PJ_STR(targetStr);
     944              : 
     945           97 :     auto contact = call.getContactHeader();
     946           97 :     auto pjContact = sip_utils::CONST_PJ_STR(contact);
     947              : 
     948           97 :     JAMI_LOG("[call:{}] Contact header: {} / {} -> {} / {}", call.getCallId(), contact, from, toUri, targetStr);
     949              : 
     950           97 :     auto* local_sdp = call.getSDP().getLocalSdpSession();
     951           97 :     pjsip_dialog* dialog {nullptr};
     952           97 :     pjsip_inv_session* inv {nullptr};
     953           97 :     if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, &pjTarget, local_sdp, &dialog, &inv))
     954            0 :         return false;
     955              : 
     956           97 :     inv->mod_data[link_.getModId()] = &call;
     957           97 :     call.setInviteSession(inv);
     958              : 
     959              :     pjsip_tx_data* tdata;
     960              : 
     961           97 :     if (pjsip_inv_invite(call.inviteSession_.get(), &tdata) != PJ_SUCCESS) {
     962            0 :         JAMI_ERROR("[call:{}] Unable to initialize invite", call.getCallId());
     963            0 :         return false;
     964              :     }
     965              : 
     966              :     pjsip_tpselector tp_sel;
     967           97 :     tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
     968           97 :     if (!call.getTransport()) {
     969            0 :         JAMI_ERROR("[call:{}] Unable to get transport", call.getCallId());
     970            0 :         return false;
     971              :     }
     972           97 :     tp_sel.u.transport = call.getTransport()->get();
     973           97 :     if (pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
     974            0 :         JAMI_ERROR("[call:{}] Unable to associate transport for invite session dialog", call.getCallId());
     975            0 :         return false;
     976              :     }
     977              : 
     978           97 :     JAMI_LOG("[call:{}] Sending SIP invite", call.getCallId());
     979              : 
     980              :     // Add user-agent header
     981           97 :     sip_utils::addUserAgentHeader(getUserAgentName(), tdata);
     982              : 
     983           97 :     if (pjsip_inv_send_msg(call.inviteSession_.get(), tdata) != PJ_SUCCESS) {
     984            0 :         JAMI_ERROR("[call:{}] Unable to send invite message", call.getCallId());
     985            0 :         return false;
     986              :     }
     987              : 
     988           97 :     call.setState(Call::CallState::ACTIVE, Call::ConnectionState::PROGRESSING);
     989           97 :     return true;
     990           97 : }
     991              : 
     992              : void
     993         2688 : JamiAccount::saveConfig() const
     994              : {
     995              :     try {
     996         2688 :         auto accountConfig = config().path / "config.yml";
     997         2688 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(accountConfig));
     998         2688 :         std::ofstream fout(accountConfig);
     999         2688 :         YAML::Emitter accountOut(fout);
    1000         2688 :         config().serialize(accountOut);
    1001         2688 :         JAMI_LOG("Saved account config to {}", accountConfig);
    1002         2688 :     } catch (const std::exception& e) {
    1003            0 :         JAMI_ERROR("Error saving account config: {}", e.what());
    1004            0 :     }
    1005         2688 : }
    1006              : 
    1007              : void
    1008          840 : JamiAccount::loadConfig()
    1009              : {
    1010          840 :     SIPAccountBase::loadConfig();
    1011          840 :     registeredName_ = config().registeredName;
    1012          840 :     if (accountManager_)
    1013           22 :         accountManager_->setAccountDeviceName(config().deviceName);
    1014          840 :     if (connectionManager_) {
    1015           18 :         if (auto c = connectionManager_->getConfig()) {
    1016              :             // Update connectionManager's config
    1017           18 :             c->upnpEnabled = config().upnpEnabled;
    1018           18 :             c->turnEnabled = config().turnEnabled;
    1019           18 :             c->turnServer = config().turnServer;
    1020           18 :             c->turnServerUserName = config().turnServerUserName;
    1021           18 :             c->turnServerPwd = config().turnServerPwd;
    1022           18 :             c->turnServerRealm = config().turnServerRealm;
    1023           18 :         }
    1024              :     }
    1025          840 :     if (config().proxyEnabled) {
    1026              :         try {
    1027            0 :             auto str = fileutils::loadCacheTextFile(cachePath_ / "dhtproxy", std::chrono::hours(24 * 14));
    1028            0 :             Json::Value root;
    1029            0 :             if (json::parse(str, root)) {
    1030            0 :                 proxyServerCached_ = root[getProxyConfigKey()].asString();
    1031              :             }
    1032            0 :         } catch (const std::exception& e) {
    1033            0 :             JAMI_LOG("[Account {}] Unable to load proxy URL from cache: {}", getAccountID(), e.what());
    1034            0 :             proxyServerCached_.clear();
    1035            0 :         }
    1036              :     } else {
    1037          840 :         proxyServerCached_.clear();
    1038          840 :         std::error_code ec;
    1039          840 :         std::filesystem::remove(cachePath_ / "dhtproxy", ec);
    1040              :     }
    1041          840 :     if (not config().dhtProxyServerEnabled) {
    1042          840 :         dhtProxyServer_.reset();
    1043              :     }
    1044          840 :     auto credentials = consumeConfigCredentials();
    1045          840 :     loadAccount(credentials.archive_password_scheme, credentials.archive_password, credentials.archive_path);
    1046          840 : }
    1047              : 
    1048              : bool
    1049            7 : JamiAccount::changeArchivePassword(const std::string& password_old, const std::string& password_new)
    1050              : {
    1051              :     try {
    1052            7 :         if (!accountManager_->changePassword(password_old, password_new)) {
    1053            2 :             JAMI_ERROR("[Account {}] Unable to change archive password", getAccountID());
    1054            2 :             return false;
    1055              :         }
    1056           10 :         editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = not password_new.empty(); });
    1057            0 :     } catch (const std::exception& ex) {
    1058            0 :         JAMI_ERROR("[Account {}] Unable to change archive password: {}", getAccountID(), ex.what());
    1059            0 :         if (password_old.empty()) {
    1060            0 :             editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = true; });
    1061            0 :             emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
    1062              :         }
    1063            0 :         return false;
    1064            0 :     }
    1065            5 :     if (password_old != password_new)
    1066            5 :         emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
    1067            5 :     return true;
    1068              : }
    1069              : 
    1070              : bool
    1071            3 : JamiAccount::isPasswordValid(const std::string& password)
    1072              : {
    1073            3 :     return accountManager_ and accountManager_->isPasswordValid(password);
    1074              : }
    1075              : 
    1076              : std::vector<uint8_t>
    1077            0 : JamiAccount::getPasswordKey(const std::string& password)
    1078              : {
    1079            0 :     return accountManager_ ? accountManager_->getPasswordKey(password) : std::vector<uint8_t>();
    1080              : }
    1081              : 
    1082              : bool
    1083            7 : JamiAccount::provideAccountAuthentication(const std::string& credentialsFromUser, const std::string& scheme)
    1084              : {
    1085            7 :     if (auto manager = std::dynamic_pointer_cast<ArchiveAccountManager>(accountManager_)) {
    1086            7 :         return manager->provideAccountAuthentication(credentialsFromUser, scheme);
    1087            7 :     }
    1088            0 :     JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while providing current account authentication.");
    1089            0 :     return false;
    1090              : }
    1091              : 
    1092              : int32_t
    1093            5 : JamiAccount::addDevice(const std::string& uriProvided)
    1094              : {
    1095            5 :     JAMI_LOG("[LinkDevice] JamiAccount::addDevice({}, {})", getAccountID(), uriProvided);
    1096            5 :     if (not accountManager_) {
    1097            0 :         JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while adding a device.");
    1098            0 :         return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
    1099              :     }
    1100            5 :     auto authHandler = channelHandlers_.find(Uri::Scheme::AUTH);
    1101            5 :     if (authHandler == channelHandlers_.end())
    1102            0 :         return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
    1103           10 :     return accountManager_->addDevice(uriProvided,
    1104            5 :                                       config().archiveHasPassword ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
    1105              :                                                                   : fileutils::ARCHIVE_AUTH_SCHEME_NONE,
    1106           10 :                                       (AuthChannelHandler*) authHandler->second.get());
    1107              : }
    1108              : 
    1109              : bool
    1110            0 : JamiAccount::cancelAddDevice(uint32_t op_token)
    1111              : {
    1112            0 :     if (!accountManager_)
    1113            0 :         return false;
    1114            0 :     return accountManager_->cancelAddDevice(op_token);
    1115              : }
    1116              : 
    1117              : bool
    1118            4 : JamiAccount::confirmAddDevice(uint32_t op_token)
    1119              : {
    1120            4 :     if (!accountManager_)
    1121            0 :         return false;
    1122            4 :     return accountManager_->confirmAddDevice(op_token);
    1123              : }
    1124              : 
    1125              : bool
    1126           38 : JamiAccount::exportArchive(const std::string& destinationPath, std::string_view scheme, const std::string& password)
    1127              : {
    1128           38 :     if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
    1129           38 :         return manager->exportArchive(destinationPath, scheme, password);
    1130              :     }
    1131            0 :     return false;
    1132              : }
    1133              : 
    1134              : bool
    1135            2 : JamiAccount::setValidity(std::string_view scheme, const std::string& pwd, const dht::InfoHash& id, int64_t validity)
    1136              : {
    1137            2 :     if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
    1138            2 :         if (manager->setValidity(scheme, pwd, id_, id, validity)) {
    1139            2 :             saveIdentity(id_, idPath_, DEVICE_ID_PATH);
    1140            2 :             return true;
    1141              :         }
    1142              :     }
    1143            0 :     return false;
    1144              : }
    1145              : 
    1146              : void
    1147            4 : JamiAccount::forceReloadAccount()
    1148              : {
    1149            4 :     editConfig([&](JamiAccountConfig& conf) {
    1150            4 :         conf.receipt.clear();
    1151            4 :         conf.receiptSignature.clear();
    1152            4 :     });
    1153            4 :     loadAccount();
    1154            4 : }
    1155              : 
    1156              : void
    1157            2 : JamiAccount::unlinkConversations(const std::set<std::string>& removed)
    1158              : {
    1159            2 :     std::lock_guard lock(configurationMutex_);
    1160            2 :     if (const auto* info = accountManager_->getInfo()) {
    1161            2 :         auto contacts = info->contacts->getContacts();
    1162            4 :         for (auto& [id, c] : contacts) {
    1163            2 :             if (removed.find(c.conversationId) != removed.end()) {
    1164            2 :                 info->contacts->updateConversation(id, "");
    1165            1 :                 JAMI_WARNING("[Account {}] Detected removed conversation ({}) in contact details for {}",
    1166              :                              getAccountID(),
    1167              :                              c.conversationId,
    1168              :                              id.toString());
    1169              :             }
    1170              :         }
    1171            2 :     }
    1172            2 : }
    1173              : 
    1174              : bool
    1175         1298 : JamiAccount::isValidAccountDevice(const dht::crypto::Certificate& cert) const
    1176              : {
    1177         1298 :     if (accountManager_) {
    1178         1298 :         if (const auto* info = accountManager_->getInfo()) {
    1179         1298 :             if (info->contacts)
    1180         1298 :                 return info->contacts->isValidAccountDevice(cert).isValid();
    1181              :         }
    1182              :     }
    1183            0 :     return false;
    1184              : }
    1185              : 
    1186              : bool
    1187            3 : JamiAccount::revokeDevice(const std::string& device, std::string_view scheme, const std::string& password)
    1188              : {
    1189            3 :     if (not accountManager_)
    1190            0 :         return false;
    1191            3 :     return accountManager_
    1192            6 :         ->revokeDevice(device, scheme, password, [this, device](AccountManager::RevokeDeviceResult result) {
    1193            3 :             emitSignal<libjami::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(),
    1194            3 :                                                                             device,
    1195              :                                                                             static_cast<int>(result));
    1196            6 :         });
    1197              :     return true;
    1198              : }
    1199              : 
    1200              : std::pair<std::string, std::string>
    1201          822 : JamiAccount::saveIdentity(const dht::crypto::Identity& id, const std::filesystem::path& path, const std::string& name)
    1202              : {
    1203          822 :     auto names = std::make_pair(name + ".key", name + ".crt");
    1204          822 :     if (id.first)
    1205          822 :         fileutils::saveFile(path / names.first, id.first->serialize(), 0600);
    1206          822 :     if (id.second)
    1207          822 :         fileutils::saveFile(path / names.second, id.second->getPacked(), 0600);
    1208          822 :     return names;
    1209            0 : }
    1210              : 
    1211              : void
    1212          820 : JamiAccount::scheduleAccountReady() const
    1213              : {
    1214          820 :     const auto accountId = getAccountID();
    1215         1640 :     runOnMainThread([accountId] { Manager::instance().markAccountReady(accountId); });
    1216          820 : }
    1217              : 
    1218              : AccountManager::OnChangeCallback
    1219          840 : JamiAccount::setupAccountCallbacks()
    1220              : {
    1221          840 :     return AccountManager::OnChangeCallback {[this](const std::string& uri, bool confirmed) {
    1222          128 :                                                  onContactAdded(uri, confirmed);
    1223          128 :                                              },
    1224          840 :                                              [this](const std::string& uri, bool banned) {
    1225           11 :                                                  onContactRemoved(uri, banned);
    1226           11 :                                              },
    1227          840 :                                              [this](const std::string& uri,
    1228              :                                                     const std::string& conversationId,
    1229              :                                                     const std::vector<uint8_t>& payload,
    1230              :                                                     TimePoint received,
    1231              :                                                     TimePoint invited) {
    1232           86 :                                                  onIncomingTrustRequest(uri, conversationId, payload, received, invited);
    1233           86 :                                              },
    1234          840 :                                              [this](const std::map<DeviceId, KnownDevice>& devices) {
    1235         2976 :                                                  onKnownDevicesChanged(devices);
    1236         2977 :                                              },
    1237          840 :                                              [this](const std::string& conversationId, const std::string& deviceId) {
    1238           62 :                                                  onConversationRequestAccepted(conversationId, deviceId);
    1239           62 :                                              },
    1240         1680 :                                              [this](const std::string& uri, const std::string& convFromReq) {
    1241           63 :                                                  onContactConfirmed(uri, convFromReq);
    1242          840 :                                              }};
    1243              : }
    1244              : 
    1245              : void
    1246          128 : JamiAccount::onContactAdded(const std::string& uri, bool confirmed)
    1247              : {
    1248          128 :     if (!id_.first)
    1249            3 :         return;
    1250          125 :     if (jami::Manager::instance().syncOnRegister) {
    1251          125 :         dht::ThreadPool::io().run([w = weak(), uri, confirmed] {
    1252          125 :             if (auto shared = w.lock()) {
    1253          125 :                 if (auto* cm = shared->convModule(true)) {
    1254          125 :                     auto activeConv = cm->getOneToOneConversation(uri);
    1255          125 :                     if (!activeConv.empty())
    1256          125 :                         cm->bootstrap(activeConv);
    1257          125 :                 }
    1258              :                 // Propagate the new contact to our other devices.
    1259          125 :                 shared->onSyncListChanged();
    1260          125 :                 emitSignal<libjami::ConfigurationSignal::ContactAdded>(shared->getAccountID(), uri, confirmed);
    1261          125 :             }
    1262          125 :         });
    1263              :     }
    1264              : }
    1265              : 
    1266              : void
    1267           11 : JamiAccount::onContactRemoved(const std::string& uri, bool banned)
    1268              : {
    1269           11 :     if (!id_.first)
    1270            0 :         return;
    1271           11 :     dht::ThreadPool::io().run([w = weak(), uri, banned] {
    1272           11 :         if (auto shared = w.lock()) {
    1273              :             // Erase linked conversation's requests
    1274           11 :             if (auto* convModule = shared->convModule(true))
    1275           11 :                 convModule->removeContact(uri, banned);
    1276              :             // Remove current connections with contact
    1277              :             // Note: if contact is ourself, we don't close the connection
    1278              :             // because it's used for syncing other conversations.
    1279           11 :             if (shared->connectionManager_ && uri != shared->getUsername()) {
    1280           11 :                 shared->connectionManager_->closeConnectionsWith(uri);
    1281              :             }
    1282              :             // Propagate the removal to our other devices.
    1283           11 :             shared->onSyncListChanged();
    1284              :             // Update client.
    1285           11 :             emitSignal<libjami::ConfigurationSignal::ContactRemoved>(shared->getAccountID(), uri, banned);
    1286           11 :         }
    1287           11 :     });
    1288              : }
    1289              : 
    1290              : void
    1291           86 : JamiAccount::onIncomingTrustRequest(const std::string& uri,
    1292              :                                     const std::string& conversationId,
    1293              :                                     const std::vector<uint8_t>& payload,
    1294              :                                     TimePoint received,
    1295              :                                     TimePoint invited)
    1296              : {
    1297           86 :     if (!id_.first)
    1298            0 :         return;
    1299           86 :     dht::ThreadPool::io().run([w = weak(), uri, conversationId, payload, received, invited] {
    1300           86 :         if (auto shared = w.lock()) {
    1301           86 :             shared->clearProfileCache(uri);
    1302           86 :             if (conversationId.empty()) {
    1303              :                 // Old path
    1304            0 :                 emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(shared->getAccountID(),
    1305            0 :                                                                                conversationId,
    1306            0 :                                                                                uri,
    1307            0 :                                                                                payload,
    1308            0 :                                                                                toSecondsSinceEpoch(received));
    1309            0 :                 return;
    1310              :             }
    1311              :             // Here account can be initializing
    1312           86 :             if (auto* cm = shared->convModule(true)) {
    1313           86 :                 auto activeConv = cm->getOneToOneConversation(uri);
    1314           86 :                 if (activeConv != conversationId)
    1315           60 :                     cm->onTrustRequest(uri, conversationId, payload, received, invited);
    1316           86 :             }
    1317           86 :         }
    1318              :     });
    1319              : }
    1320              : 
    1321              : void
    1322         2976 : JamiAccount::onKnownDevicesChanged(const std::map<DeviceId, KnownDevice>& devices)
    1323              : {
    1324         2976 :     std::map<std::string, std::string> ids;
    1325      1010999 :     for (auto& d : devices) {
    1326      1008033 :         auto id = d.first.toString();
    1327      1008020 :         auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
    1328      1008025 :         ids.emplace(std::move(id), std::move(label));
    1329      1008017 :     }
    1330         2972 :     runOnMainThread([id = getAccountID(), devices = std::move(ids)] {
    1331         2977 :         emitSignal<libjami::ConfigurationSignal::KnownDevicesChanged>(id, devices);
    1332         2977 :     });
    1333         2977 : }
    1334              : 
    1335              : void
    1336           62 : JamiAccount::onConversationRequestAccepted(const std::string& conversationId, const std::string& deviceId)
    1337              : {
    1338              :     // Note: Do not retrigger on another thread. This has to be done
    1339              :     // at the same time of acceptTrustRequest a synced state between TrustRequest
    1340              :     // and convRequests.
    1341           62 :     if (auto* cm = convModule(true))
    1342           62 :         cm->acceptConversationRequest(conversationId, deviceId);
    1343           62 : }
    1344              : 
    1345              : void
    1346           63 : JamiAccount::onContactConfirmed(const std::string& uri, const std::string& convFromReq)
    1347              : {
    1348           63 :     dht::ThreadPool::io().run([w = weak(), convFromReq, uri] {
    1349           63 :         if (auto shared = w.lock()) {
    1350           63 :             shared->convModule(true);
    1351              :             // Remove cached payload if there is one
    1352           63 :             auto requestPath = shared->cachePath_ / "requests" / uri;
    1353           63 :             dhtnet::fileutils::remove(requestPath);
    1354           63 :             dhtnet::fileutils::remove(shared->cachePath_ / "requests" / (uri + ".ts"));
    1355          126 :         }
    1356           63 :     });
    1357           63 : }
    1358              : 
    1359              : std::unique_ptr<AccountManager::AccountCredentials>
    1360          822 : JamiAccount::buildAccountCredentials(const JamiAccountConfig& conf,
    1361              :                                      const dht::crypto::Identity& id,
    1362              :                                      const std::string& archive_password_scheme,
    1363              :                                      const std::string& archive_password,
    1364              :                                      const std::string& archive_path,
    1365              :                                      bool& migrating,
    1366              :                                      bool& hasPassword)
    1367              : {
    1368          822 :     std::unique_ptr<AccountManager::AccountCredentials> creds;
    1369              : 
    1370          822 :     if (conf.managerUri.empty()) {
    1371          822 :         auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
    1372          822 :         auto archivePath = fileutils::getFullPath(idPath_, conf.archivePath);
    1373              : 
    1374          822 :         if (!archive_path.empty()) {
    1375           39 :             acreds->scheme = "file";
    1376           39 :             acreds->uri = archive_path;
    1377          783 :         } else if (!conf.archive_url.empty() && conf.archive_url == "jami-auth") {
    1378            5 :             JAMI_DEBUG("[Account {}] [LinkDevice] scheme p2p & uri {}", getAccountID(), conf.archive_url);
    1379            5 :             acreds->scheme = "p2p";
    1380            5 :             acreds->uri = conf.archive_url;
    1381          778 :         } else if (std::filesystem::is_regular_file(archivePath)) {
    1382            4 :             acreds->scheme = "local";
    1383            4 :             acreds->uri = archivePath.string();
    1384            4 :             acreds->updateIdentity = id;
    1385            4 :             migrating = true;
    1386              :         }
    1387              : 
    1388          822 :         creds = std::move(acreds);
    1389          822 :     } else {
    1390            0 :         auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
    1391            0 :         screds->username = conf.managerUsername;
    1392            0 :         screds->identity = id;
    1393            0 :         creds = std::move(screds);
    1394            0 :     }
    1395              : 
    1396          822 :     creds->password = archive_password;
    1397          822 :     hasPassword = !archive_password.empty();
    1398         1634 :     creds->password_scheme = (hasPassword && archive_password_scheme.empty()) ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
    1399         1634 :                                                                               : archive_password_scheme;
    1400              : 
    1401          822 :     return creds;
    1402            0 : }
    1403              : 
    1404              : void
    1405          820 : JamiAccount::onAuthenticationSuccess(bool migrating,
    1406              :                                      bool hasPassword,
    1407              :                                      const AccountInfo& info,
    1408              :                                      const std::map<std::string, std::string>& configMap,
    1409              :                                      std::string&& receipt,
    1410              :                                      std::vector<uint8_t>&& receiptSignature)
    1411              : {
    1412          820 :     JAMI_LOG("[Account {}] Auth success! Device: {}", getAccountID(), info.deviceId);
    1413              : 
    1414          820 :     dhtnet::fileutils::check_dir(idPath_, 0700);
    1415              : 
    1416          820 :     auto id = info.identity;
    1417         1640 :     editConfig([&](JamiAccountConfig& conf) {
    1418          820 :         std::tie(conf.tlsPrivateKeyFile, conf.tlsCertificateFile) = saveIdentity(id, idPath_, DEVICE_ID_PATH);
    1419          820 :         conf.tlsPassword = {};
    1420              : 
    1421         1640 :         auto passwordIt = configMap.find(libjami::Account::ConfProperties::ARCHIVE_HAS_PASSWORD);
    1422         1640 :         conf.archiveHasPassword = (passwordIt != configMap.end() && !passwordIt->second.empty())
    1423         1640 :                                       ? passwordIt->second == "true"
    1424            0 :                                       : hasPassword;
    1425              : 
    1426          820 :         if (not conf.managerUri.empty()) {
    1427            0 :             conf.registeredName = conf.managerUsername;
    1428            0 :             registeredName_ = conf.managerUsername;
    1429              :         }
    1430              : 
    1431          820 :         conf.username = info.accountId;
    1432          820 :         conf.deviceName = accountManager_->getAccountDeviceName();
    1433              : 
    1434         1640 :         auto nameServerIt = configMap.find(libjami::Account::ConfProperties::Nameserver::URI);
    1435          820 :         if (nameServerIt != configMap.end() && !nameServerIt->second.empty())
    1436            0 :             conf.nameServer = nameServerIt->second;
    1437              : 
    1438         1640 :         auto displayNameIt = configMap.find(libjami::Account::ConfProperties::DISPLAYNAME);
    1439          820 :         if (displayNameIt != configMap.end() && !displayNameIt->second.empty())
    1440           42 :             conf.displayName = displayNameIt->second;
    1441              : 
    1442          820 :         conf.receipt = std::move(receipt);
    1443          820 :         conf.receiptSignature = std::move(receiptSignature);
    1444          820 :         conf.fromMap(configMap);
    1445          820 :     });
    1446              : 
    1447          820 :     id_ = std::move(id);
    1448              :     {
    1449          820 :         std::lock_guard lk(moduleMtx_);
    1450          820 :         convModule_.reset();
    1451          820 :     }
    1452              : 
    1453          820 :     if (migrating)
    1454            4 :         Migration::setState(getAccountID(), Migration::State::SUCCESS);
    1455              : 
    1456          820 :     setRegistrationState(RegistrationState::UNREGISTERED);
    1457              : 
    1458          820 :     if (!info.photo.empty() || !info.displayName.empty()) {
    1459              :         try {
    1460            0 :             auto newProfile = vCard::utils::initVcard();
    1461            0 :             newProfile[std::string(vCard::Property::FORMATTED_NAME)] = info.displayName;
    1462            0 :             newProfile[std::string(vCard::Property::PHOTO)] = info.photo;
    1463              : 
    1464            0 :             const auto& profiles = idPath_ / "profiles";
    1465            0 :             const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(info.accountId));
    1466            0 :             vCard::utils::save(newProfile, vCardPath, profilePath());
    1467              : 
    1468            0 :             runOnMainThread([w = weak(), id = info.accountId, vCardPath] {
    1469            0 :                 if (auto shared = w.lock()) {
    1470            0 :                     emitSignal<libjami::ConfigurationSignal::ProfileReceived>(shared->getAccountID(),
    1471            0 :                                                                               id,
    1472            0 :                                                                               vCardPath.string());
    1473            0 :                 }
    1474            0 :             });
    1475            0 :         } catch (const std::exception& e) {
    1476            0 :             JAMI_WARNING("[Account {}] Unable to save profile after authentication: {}", getAccountID(), e.what());
    1477            0 :         }
    1478              :     }
    1479              : 
    1480          820 :     updateTrustedCa();
    1481          820 :     doRegister();
    1482          820 :     scheduleAccountReady();
    1483          820 : }
    1484              : 
    1485              : void
    1486            0 : JamiAccount::onAuthenticationError(const std::weak_ptr<JamiAccount>& w,
    1487              :                                    bool hadIdentity,
    1488              :                                    bool migrating,
    1489              :                                    std::string accountId,
    1490              :                                    AccountManager::AuthError error,
    1491              :                                    const std::string& message)
    1492              : {
    1493            0 :     JAMI_WARNING("[Account {}] Auth error: {} {}", accountId, (int) error, message);
    1494              : 
    1495            0 :     if ((hadIdentity || migrating) && error == AccountManager::AuthError::INVALID_ARGUMENTS) {
    1496            0 :         Migration::setState(accountId, Migration::State::INVALID);
    1497            0 :         if (auto acc = w.lock())
    1498            0 :             acc->setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
    1499            0 :         return;
    1500              :     }
    1501              : 
    1502            0 :     if (auto acc = w.lock())
    1503            0 :         acc->setRegistrationState(RegistrationState::ERROR_GENERIC);
    1504              : 
    1505            0 :     runOnMainThread([accountId = std::move(accountId)] { Manager::instance().removeAccount(accountId, true); });
    1506              : }
    1507              : 
    1508              : // must be called while configurationMutex_ is locked
    1509              : void
    1510          844 : JamiAccount::loadAccount(const std::string& archive_password_scheme,
    1511              :                          const std::string& archive_password,
    1512              :                          const std::string& archive_path)
    1513              : {
    1514          844 :     if (registrationState_ == RegistrationState::INITIALIZING)
    1515           22 :         return;
    1516              : 
    1517          840 :     JAMI_DEBUG("[Account {:s}] Loading account", getAccountID());
    1518              : 
    1519          840 :     const auto scheduleAccountReady = [accountId = getAccountID()] {
    1520           18 :         runOnMainThread([accountId] {
    1521           18 :             auto& manager = Manager::instance();
    1522           18 :             manager.markAccountReady(accountId);
    1523           18 :         });
    1524          858 :     };
    1525              : 
    1526          840 :     const auto& conf = config();
    1527          840 :     auto callbacks = setupAccountCallbacks();
    1528              : 
    1529              :     try {
    1530          840 :         auto oldIdentity = id_.first ? id_.first->getPublicKey().getLongId() : DeviceId();
    1531              : 
    1532          840 :         if (conf.managerUri.empty()) {
    1533         1680 :             accountManager_ = std::make_shared<ArchiveAccountManager>(
    1534          840 :                 getAccountID(),
    1535              :                 getPath(),
    1536           44 :                 [this]() { return getAccountDetails(); },
    1537          840 :                 [this](DeviceSync&& syncData) {
    1538          820 :                     if (auto* sm = syncModule()) {
    1539          820 :                         auto syncDataPtr = std::make_shared<SyncMsg>();
    1540          820 :                         syncDataPtr->ds = std::move(syncData);
    1541          820 :                         sm->syncWithConnected(syncDataPtr);
    1542          820 :                     }
    1543          820 :                 },
    1544         1680 :                 conf.archivePath.empty() ? "archive.gz" : conf.archivePath,
    1545         1680 :                 conf.nameServer);
    1546              :         } else {
    1547            0 :             accountManager_ = std::make_shared<ServerAccountManager>(getAccountID(),
    1548              :                                                                      getPath(),
    1549            0 :                                                                      conf.managerUri,
    1550            0 :                                                                      conf.nameServer);
    1551              :         }
    1552              : 
    1553          840 :         auto id = accountManager_->loadIdentity(conf.tlsCertificateFile, conf.tlsPrivateKeyFile, conf.tlsPassword);
    1554              : 
    1555          840 :         if (const auto* info
    1556          840 :             = accountManager_->useIdentity(id, conf.receipt, conf.receiptSignature, conf.managerUsername, callbacks)) {
    1557           18 :             id_ = std::move(id);
    1558           18 :             config_->username = info->accountId;
    1559           18 :             JAMI_WARNING("[Account {:s}] Loaded account identity", getAccountID());
    1560              : 
    1561           18 :             if (info->identity.first->getPublicKey().getLongId() != oldIdentity) {
    1562            0 :                 JAMI_WARNING("[Account {:s}] Identity changed", getAccountID());
    1563              :                 {
    1564            0 :                     std::lock_guard lk(moduleMtx_);
    1565            0 :                     convModule_.reset();
    1566            0 :                 }
    1567            0 :                 convModule();
    1568              :             } else {
    1569           18 :                 convModule()->setAccountManager(accountManager_);
    1570              :             }
    1571              : 
    1572           18 :             convModule()->initPresence();
    1573           18 :             if (not isEnabled())
    1574            0 :                 setRegistrationState(RegistrationState::UNREGISTERED);
    1575              : 
    1576           18 :             updateTrustedCa();
    1577           18 :             scheduleAccountReady();
    1578           18 :             return;
    1579              :         }
    1580              : 
    1581          822 :         if (!isEnabled())
    1582            0 :             return;
    1583              : 
    1584          822 :         JAMI_WARNING("[Account {}] useIdentity failed!", getAccountID());
    1585              : 
    1586          822 :         if (not conf.managerUri.empty() && archive_password.empty()) {
    1587            0 :             Migration::setState(accountID_, Migration::State::INVALID);
    1588            0 :             setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
    1589            0 :             return;
    1590              :         }
    1591              : 
    1592          822 :         bool migrating = registrationState_ == RegistrationState::ERROR_NEED_MIGRATION;
    1593          822 :         setRegistrationState(RegistrationState::INITIALIZING);
    1594              : 
    1595          822 :         bool hasPassword = false;
    1596              :         auto creds = buildAccountCredentials(conf,
    1597              :                                              id,
    1598              :                                              archive_password_scheme,
    1599              :                                              archive_password,
    1600              :                                              archive_path,
    1601              :                                              migrating,
    1602          822 :                                              hasPassword);
    1603              : 
    1604          822 :         JAMI_WARNING("[Account {}] initAuthentication {}", getAccountID(), fmt::ptr(this));
    1605              : 
    1606          822 :         const bool hadIdentity = static_cast<bool>(id.first);
    1607         3288 :         accountManager_->initAuthentication(
    1608         1644 :             ip_utils::getDeviceName(),
    1609          822 :             std::move(creds),
    1610         1644 :             [w = weak(), migrating, hasPassword](const AccountInfo& info,
    1611              :                                                  const std::map<std::string, std::string>& configMap,
    1612              :                                                  std::string&& receipt,
    1613              :                                                  std::vector<uint8_t>&& receiptSignature) {
    1614          820 :                 if (auto self = w.lock())
    1615         1640 :                     self->onAuthenticationSuccess(migrating,
    1616              :                                                   hasPassword,
    1617              :                                                   info,
    1618              :                                                   configMap,
    1619          820 :                                                   std::move(receipt),
    1620         1640 :                                                   std::move(receiptSignature));
    1621          820 :             },
    1622         1644 :             [w = weak(), hadIdentity, accountId = getAccountID(), migrating](AccountManager::AuthError error,
    1623              :                                                                              const std::string& message) {
    1624            0 :                 JamiAccount::onAuthenticationError(w, hadIdentity, migrating, accountId, error, message);
    1625            0 :             },
    1626              :             callbacks);
    1627          840 :     } catch (const std::exception& e) {
    1628            0 :         JAMI_WARNING("[Account {}] Error loading account: {}", getAccountID(), e.what());
    1629            0 :         accountManager_.reset();
    1630            0 :         setRegistrationState(RegistrationState::ERROR_GENERIC);
    1631            0 :     }
    1632          858 : }
    1633              : 
    1634              : std::map<std::string, std::string>
    1635         4870 : JamiAccount::getVolatileAccountDetails() const
    1636              : {
    1637         4870 :     auto a = SIPAccountBase::getVolatileAccountDetails();
    1638         4870 :     a.emplace(libjami::Account::VolatileProperties::InstantMessaging::OFF_CALL, TRUE_STR);
    1639         4870 :     auto registeredName = getRegisteredName();
    1640         4870 :     if (not registeredName.empty())
    1641            3 :         a.emplace(libjami::Account::VolatileProperties::REGISTERED_NAME, registeredName);
    1642         4870 :     a.emplace(libjami::Account::ConfProperties::PROXY_SERVER, proxyServerCached_);
    1643         4870 :     a.emplace(libjami::Account::VolatileProperties::DHT_BOUND_PORT, std::to_string(dhtBoundPort_));
    1644         4870 :     a.emplace(libjami::Account::VolatileProperties::DEVICE_ANNOUNCED, deviceAnnounced_ ? TRUE_STR : FALSE_STR);
    1645         4870 :     if (accountManager_) {
    1646         4870 :         if (const auto* info = accountManager_->getInfo()) {
    1647         4006 :             a.emplace(libjami::Account::ConfProperties::DEVICE_ID, info->deviceId);
    1648              :         }
    1649              :     }
    1650         9740 :     return a;
    1651         4870 : }
    1652              : 
    1653              : void
    1654            3 : JamiAccount::lookupName(const std::string& name)
    1655              : {
    1656            3 :     std::lock_guard lock(configurationMutex_);
    1657            3 :     auto acc = getAccountID();
    1658            3 :     if (!accountManager_) {
    1659            0 :         JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, name);
    1660            0 :         emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
    1661              :                                                                       name,
    1662              :                                                                       (int) NameDirectory::Response::error,
    1663              :                                                                       "",
    1664              :                                                                       "");
    1665            0 :         return;
    1666              :     }
    1667            6 :     accountManager_->lookupUri(name,
    1668            3 :                                config().nameServer,
    1669            6 :                                [acc, name](const std::string& regName,
    1670              :                                            const std::string& address,
    1671              :                                            NameDirectory::Response response) {
    1672            6 :                                    emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
    1673            3 :                                                                                                  name,
    1674              :                                                                                                  (int) response,
    1675              :                                                                                                  address,
    1676              :                                                                                                  regName);
    1677            3 :                                });
    1678            3 : }
    1679              : 
    1680              : void
    1681            3 : JamiAccount::lookupAddress(const std::string& addr)
    1682              : {
    1683            3 :     std::lock_guard lock(configurationMutex_);
    1684            3 :     auto acc = getAccountID();
    1685            3 :     if (!accountManager_) {
    1686            0 :         JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, addr);
    1687            0 :         emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
    1688              :                                                                       addr,
    1689              :                                                                       (int) NameDirectory::Response::error,
    1690              :                                                                       "",
    1691              :                                                                       "");
    1692            0 :         return;
    1693              :     }
    1694            6 :     accountManager_->lookupAddress(
    1695            6 :         addr, [acc, addr](const std::string& regName, const std::string& address, NameDirectory::Response response) {
    1696            3 :             emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc, addr, (int) response, address, regName);
    1697            3 :         });
    1698            3 : }
    1699              : 
    1700              : void
    1701            1 : JamiAccount::registerName(const std::string& name, const std::string& scheme, const std::string& password)
    1702              : {
    1703            1 :     std::lock_guard lock(configurationMutex_);
    1704            1 :     if (accountManager_)
    1705            1 :         accountManager_
    1706            2 :             ->registerName(name,
    1707              :                            scheme,
    1708              :                            password,
    1709            2 :                            [acc = getAccountID(), name, w = weak()](NameDirectory::RegistrationResponse response,
    1710              :                                                                     const std::string& regName) {
    1711            1 :                                auto res = (int) std::min(response, NameDirectory::RegistrationResponse::error);
    1712            1 :                                if (response == NameDirectory::RegistrationResponse::success) {
    1713            1 :                                    if (auto this_ = w.lock()) {
    1714            1 :                                        if (this_->setRegisteredName(regName)) {
    1715            2 :                                            this_->editConfig(
    1716            2 :                                                [&](JamiAccountConfig& config) { config.registeredName = regName; });
    1717            1 :                                            emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
    1718            2 :                                                this_->accountID_, this_->getVolatileAccountDetails());
    1719              :                                        }
    1720            1 :                                    }
    1721              :                                }
    1722            1 :                                emitSignal<libjami::ConfigurationSignal::NameRegistrationEnded>(acc, res, name);
    1723            1 :                            });
    1724            1 : }
    1725              : 
    1726              : bool
    1727            0 : JamiAccount::searchUser(const std::string& query)
    1728              : {
    1729            0 :     if (accountManager_)
    1730            0 :         return accountManager_
    1731            0 :             ->searchUser(query,
    1732            0 :                          [acc = getAccountID(), query](const jami::NameDirectory::SearchResult& result,
    1733              :                                                        jami::NameDirectory::Response response) {
    1734            0 :                              jami::emitSignal<libjami::ConfigurationSignal::UserSearchEnded>(acc,
    1735              :                                                                                              (int) response,
    1736            0 :                                                                                              query,
    1737              :                                                                                              result);
    1738            0 :                          });
    1739            0 :     return false;
    1740              : }
    1741              : 
    1742              : void
    1743          186 : JamiAccount::forEachPendingCall(const DeviceId& deviceId, const std::function<void(const std::shared_ptr<SIPCall>&)>& cb)
    1744              : {
    1745          186 :     std::vector<std::shared_ptr<SIPCall>> pc;
    1746              :     {
    1747          186 :         std::lock_guard lk(pendingCallsMutex_);
    1748          186 :         pc = std::move(pendingCalls_[deviceId]);
    1749          186 :     }
    1750          278 :     for (const auto& pendingCall : pc) {
    1751           92 :         cb(pendingCall);
    1752              :     }
    1753          186 : }
    1754              : 
    1755              : void
    1756          746 : JamiAccount::registerAsyncOps()
    1757              : {
    1758          746 :     loadCachedProxyServer([w = weak()](const std::string&) {
    1759          746 :         runOnMainThread([w] {
    1760          746 :             if (auto s = w.lock()) {
    1761          746 :                 std::lock_guard lock(s->configurationMutex_);
    1762          746 :                 s->doRegister_();
    1763         1492 :             }
    1764          746 :         });
    1765          746 :     });
    1766          746 : }
    1767              : 
    1768              : void
    1769         1698 : JamiAccount::doRegister()
    1770              : {
    1771         1698 :     std::lock_guard lock(configurationMutex_);
    1772         1698 :     if (not isUsable()) {
    1773          134 :         JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring", getAccountID());
    1774          134 :         return;
    1775              :     }
    1776              : 
    1777         1564 :     JAMI_LOG("[Account {:s}] Starting account…", getAccountID());
    1778              : 
    1779              :     // invalid state transitions:
    1780              :     // INITIALIZING: generating/loading certificates, unable to register
    1781              :     // NEED_MIGRATION: old account detected, user needs to migrate
    1782         1564 :     if (registrationState_ == RegistrationState::INITIALIZING
    1783          746 :         || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION)
    1784          818 :         return;
    1785              : 
    1786          746 :     convModule(); // Init conv module before passing in trying
    1787          746 :     setRegistrationState(RegistrationState::TRYING);
    1788          746 :     if (proxyServerCached_.empty()) {
    1789          746 :         registerAsyncOps();
    1790              :     } else {
    1791            0 :         doRegister_();
    1792              :     }
    1793         1698 : }
    1794              : 
    1795              : std::vector<std::string>
    1796          746 : JamiAccount::loadBootstrap() const
    1797              : {
    1798          746 :     std::vector<std::string> bootstrap;
    1799          746 :     std::string_view stream(config().hostname), node_addr;
    1800         1492 :     while (jami::getline(stream, node_addr, ';'))
    1801          746 :         bootstrap.emplace_back(node_addr);
    1802         1492 :     for (const auto& b : bootstrap)
    1803          746 :         JAMI_LOG("[Account {}] Bootstrap node: {}", getAccountID(), b);
    1804         1492 :     return bootstrap;
    1805            0 : }
    1806              : 
    1807              : void
    1808           31 : JamiAccount::trackBuddyPresence(const std::string& buddy_id, bool track)
    1809              : {
    1810           31 :     std::string buddyUri;
    1811              :     try {
    1812           31 :         buddyUri = parseJamiUri(buddy_id);
    1813            0 :     } catch (...) {
    1814            0 :         JAMI_ERROR("[Account {:s}] Failed to track presence: invalid URI {:s}", getAccountID(), buddy_id);
    1815            0 :         return;
    1816            0 :     }
    1817           31 :     JAMI_LOG("[Account {:s}] {:s} presence for {:s}", getAccountID(), track ? "Track" : "Untrack", buddy_id);
    1818              : 
    1819           31 :     if (!presenceManager_)
    1820            0 :         return;
    1821              : 
    1822           31 :     if (track) {
    1823           31 :         presenceManager_->trackBuddy(buddyUri);
    1824           31 :         std::lock_guard lock(presenceStateMtx_);
    1825           31 :         auto it = presenceState_.find(buddyUri);
    1826           31 :         if (it != presenceState_.end() && it->second != PresenceState::DISCONNECTED) {
    1827            1 :             emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
    1828              :                                                                       buddyUri,
    1829            1 :                                                                       static_cast<int>(it->second),
    1830              :                                                                       "");
    1831              :         }
    1832           31 :     } else {
    1833            0 :         presenceManager_->untrackBuddy(buddyUri);
    1834              :     }
    1835           31 : }
    1836              : 
    1837              : std::map<std::string, bool>
    1838            2 : JamiAccount::getTrackedBuddyPresence() const
    1839              : {
    1840            2 :     if (!presenceManager_)
    1841            0 :         return {};
    1842            2 :     return presenceManager_->getTrackedBuddyPresence();
    1843              : }
    1844              : 
    1845              : void
    1846          601 : JamiAccount::onTrackedBuddyOnline(const std::string& contactId)
    1847              : {
    1848          601 :     JAMI_DEBUG("[Account {:s}] Buddy {} online", getAccountID(), contactId);
    1849          601 :     std::lock_guard lock(presenceStateMtx_);
    1850          601 :     auto& state = presenceState_[contactId];
    1851          601 :     if (state < PresenceState::AVAILABLE) {
    1852          399 :         state = PresenceState::AVAILABLE;
    1853          399 :         emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
    1854              :                                                                   contactId,
    1855              :                                                                   static_cast<int>(PresenceState::AVAILABLE),
    1856              :                                                                   "");
    1857              :     }
    1858              : 
    1859          601 :     if (auto details = getContactInfo(contactId)) {
    1860           75 :         if (!details->confirmed) {
    1861           33 :             auto convId = convModule()->getOneToOneConversation(contactId);
    1862           33 :             if (convId.empty())
    1863            1 :                 return;
    1864              :             // In this case, the TrustRequest was sent but never confirmed (cause the contact was
    1865              :             // offline maybe) To avoid the contact to never receive the conv request, retry there
    1866           32 :             std::lock_guard lock(configurationMutex_);
    1867           32 :             if (accountManager_) {
    1868              :                 // Retrieve cached payload for trust request.
    1869           32 :                 auto requestPath = cachePath_ / "requests" / contactId;
    1870           32 :                 std::vector<uint8_t> payload;
    1871              :                 try {
    1872           38 :                     payload = fileutils::loadFile(requestPath);
    1873            6 :                 } catch (...) {
    1874            6 :                 }
    1875           32 :                 if (payload.size() >= 64000) {
    1876            0 :                     JAMI_WARNING("[Account {:s}] Trust request for contact {:s} is too big, reset payload",
    1877              :                                  getAccountID(),
    1878              :                                  contactId);
    1879            0 :                     payload.clear();
    1880              :                 }
    1881              :                 // Reuse the original invite timestamp so this passive retry isn't treated as a
    1882              :                 // fresh invitation; fall back to now() if no cached timestamp is found (e.g.
    1883              :                 // cache predates this mechanism, or the sidecar failed to write previously).
    1884           32 :                 auto tsPath = cachePath_ / "requests" / (contactId + ".ts");
    1885           32 :                 TimePoint invited {};
    1886              :                 try {
    1887           38 :                     auto tsData = fileutils::loadFile(tsPath);
    1888           26 :                     auto invitedMs = std::stoll(std::string(tsData.begin(), tsData.end()));
    1889           26 :                     if (invitedMs > 0)
    1890           26 :                         invited = timePointFromMilliseconds(invitedMs);
    1891           32 :                 } catch (const std::exception& e) {
    1892            6 :                     JAMI_WARNING("[Account {:s}] Unable to load invite timestamp for {:s}: {}",
    1893              :                                  getAccountID(),
    1894              :                                  contactId,
    1895              :                                  e.what());
    1896            6 :                 }
    1897           32 :                 if (invited == TimePoint {}) {
    1898            6 :                     invited = nowMs();
    1899              :                     // Persist the fallback so subsequent retries stay stable instead of minting
    1900              :                     // a new timestamp (and thus looking like a fresh re-invitation) every time.
    1901            6 :                     std::ofstream tsFile(tsPath, std::ios::trunc | std::ios::binary);
    1902            6 :                     if (tsFile.is_open())
    1903            0 :                         tsFile << toMillisecondsSinceEpoch(invited);
    1904              :                     else
    1905            6 :                         JAMI_WARNING("[Account {:s}] Unable to write invite timestamp for {:s}",
    1906              :                                      getAccountID(),
    1907              :                                      contactId);
    1908            6 :                 }
    1909           32 :                 accountManager_->sendTrustRequest(contactId, convId, payload, invited);
    1910           32 :             }
    1911           33 :         }
    1912          601 :     }
    1913          601 : }
    1914              : 
    1915              : void
    1916           55 : JamiAccount::onTrackedBuddyOffline(const std::string& contactId)
    1917              : {
    1918           55 :     JAMI_DEBUG("[Account {:s}] Buddy {} offline", getAccountID(), contactId);
    1919           55 :     std::lock_guard lock(presenceStateMtx_);
    1920           55 :     auto& state = presenceState_[contactId];
    1921           55 :     if (state > PresenceState::DISCONNECTED) {
    1922           55 :         if (state == PresenceState::CONNECTED) {
    1923            0 :             JAMI_WARNING("[Account {:s}] Buddy {} is not present on the DHT, but P2P connected",
    1924              :                          getAccountID(),
    1925              :                          contactId);
    1926            0 :             return;
    1927              :         }
    1928           55 :         state = PresenceState::DISCONNECTED;
    1929           55 :         emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
    1930              :                                                                   contactId,
    1931              :                                                                   static_cast<int>(PresenceState::DISCONNECTED),
    1932              :                                                                   "");
    1933              :     }
    1934           55 : }
    1935              : 
    1936              : void
    1937          746 : JamiAccount::doRegister_()
    1938              : {
    1939          746 :     if (registrationState_ != RegistrationState::TRYING) {
    1940            0 :         JAMI_ERROR("[Account {}] Already registered", getAccountID());
    1941            0 :         return;
    1942              :     }
    1943              : 
    1944          746 :     JAMI_DEBUG("[Account {}] Starting account…", getAccountID());
    1945          746 :     const auto& conf = config();
    1946              : 
    1947              :     try {
    1948          746 :         if (not accountManager_ or not accountManager_->getInfo())
    1949            0 :             throw std::runtime_error("No identity configured for this account.");
    1950              : 
    1951          746 :         if (dht_->isRunning()) {
    1952            2 :             JAMI_ERROR("[Account {}] DHT already running (stopping it first).", getAccountID());
    1953            2 :             dht_->join();
    1954              :         }
    1955              : 
    1956          746 :         convModule()->clearPendingFetch();
    1957              : 
    1958              :         // Look for registered name
    1959         1492 :         accountManager_->lookupAddress(accountManager_->getInfo()->accountId,
    1960         1492 :                                        [w = weak()](const std::string& regName,
    1961              :                                                     const std::string& /*address*/,
    1962              :                                                     const NameDirectory::Response& response) {
    1963          746 :                                            if (auto this_ = w.lock())
    1964          746 :                                                this_->lookupRegisteredName(regName, response);
    1965          746 :                                        });
    1966              : 
    1967          746 :         dht::DhtRunner::Config config = initDhtConfig(conf);
    1968              : 
    1969              :         // check if dht peer service is enabled
    1970          746 :         if (conf.accountPeerDiscovery or conf.accountPublish) {
    1971            0 :             peerDiscovery_ = std::make_shared<dht::PeerDiscovery>();
    1972            0 :             if (conf.accountPeerDiscovery) {
    1973            0 :                 JAMI_LOG("[Account {}] Starting Jami account discovery…", getAccountID());
    1974            0 :                 startAccountDiscovery();
    1975              :             }
    1976            0 :             if (conf.accountPublish)
    1977            0 :                 startAccountPublish();
    1978              :         }
    1979              : 
    1980          746 :         dht::DhtRunner::Context context = initDhtContext();
    1981              : 
    1982          746 :         accountManager_->setDht(dht_);
    1983          746 :         dht_->run(conf.dhtPort, config, std::move(context));
    1984              : 
    1985          746 :         dhtBoundPort_ = dht_->getBoundPort();
    1986              : 
    1987              :         // Now that the DHT is running and we know the actual bound port,
    1988              :         // request a UPnP mapping for it. A bound port of 0 means the DHT has no
    1989              :         // socket to forward (proxy-only mode): requesting a mapping would make the
    1990              :         // UPnP context allocate an arbitrary port that nothing listens on.
    1991          746 :         if (upnpCtrl_ and dhtUpnpMapping_.getInternalPort() != dhtBoundPort_) {
    1992          680 :             releaseDhtUpnpMapping();
    1993              : 
    1994          680 :             if (dhtBoundPort_ != 0) {
    1995          680 :                 JAMI_LOG("[Account {:s}] UPnP: requesting mapping for DHT port {}", getAccountID(), dhtBoundPort_);
    1996              : 
    1997          680 :                 dhtUpnpMapping_.enableAutoUpdate(true);
    1998              : 
    1999          680 :                 dhtnet::upnp::Mapping desired(dhtnet::upnp::PortType::UDP, dhtBoundPort_, dhtBoundPort_);
    2000          680 :                 dhtUpnpMapping_.updateFrom(desired);
    2001              : 
    2002          680 :                 dhtUpnpMapping_.setNotifyCallback([w = weak()](const dhtnet::upnp::Mapping::sharedPtr_t& mapRes) {
    2003          694 :                     if (auto accPtr = w.lock()) {
    2004          694 :                         auto& dhtMap = accPtr->dhtUpnpMapping_;
    2005          694 :                         const auto& accId = accPtr->getAccountID();
    2006              : 
    2007          694 :                         JAMI_LOG("[Account {:s}] DHT UPnP mapping changed to {:s}", accId, mapRes->toString(true));
    2008              : 
    2009          694 :                         if (dhtMap.getMapKey() != mapRes->getMapKey() or dhtMap.getState() != mapRes->getState()) {
    2010          673 :                             dhtMap.updateFrom(mapRes);
    2011          673 :                             if (mapRes->getState() == dhtnet::upnp::MappingState::OPEN) {
    2012            0 :                                 JAMI_LOG("[Account {:s}] Mapping {:s} successfully allocated", accId, dhtMap.toString());
    2013            0 :                                 accPtr->dht_->connectivityChanged();
    2014          673 :                             } else if (mapRes->getState() == dhtnet::upnp::MappingState::FAILED) {
    2015          673 :                                 JAMI_WARNING("[Account {:s}] UPnP mapping failed", accId);
    2016              :                             }
    2017              :                         } else {
    2018           21 :                             dhtMap.updateFrom(mapRes);
    2019              :                         }
    2020          694 :                     }
    2021          694 :                 });
    2022              : 
    2023          680 :                 upnpCtrl_->reserveMapping(dhtUpnpMapping_);
    2024          680 :             }
    2025              :         }
    2026              : 
    2027         1492 :         for (const auto& bootstrap : loadBootstrap())
    2028         1492 :             dht_->bootstrap(bootstrap);
    2029              : 
    2030          746 :         if (conf.dhtProxyServerEnabled) {
    2031            0 :             dht::ProxyServerConfig proxyConfig;
    2032            0 :             proxyConfig.port = conf.dhtProxyServerPort;
    2033            0 :             proxyConfig.identity = id_;
    2034            0 :             dhtProxyServer_ = std::make_shared<dht::DhtProxyServer>(dht_, proxyConfig);
    2035            0 :         } else {
    2036          746 :             dhtProxyServer_.reset();
    2037              :         }
    2038              : 
    2039          746 :         std::unique_lock lkCM(connManagerMtx_);
    2040          746 :         initConnectionManager();
    2041          746 :         connectionManager_->dhtStarted();
    2042         1538 :         connectionManager_->onICERequest([this](const DeviceId& deviceId) { return onICERequest(deviceId); });
    2043          746 :         connectionManager_->onChannelRequest([this](const std::shared_ptr<dht::crypto::Certificate>& cert,
    2044         4446 :                                                     const std::string& name) { return onChannelRequest(cert, name); });
    2045         1492 :         connectionManager_->onNewDeviceConnection(
    2046         2007 :             [this](const std::shared_ptr<dht::crypto::Certificate>& cert) { onNewDeviceConnection(cert); });
    2047         1492 :         connectionManager_->onConnectionReady(
    2048          746 :             [this](const DeviceId& deviceId, const std::string& name, std::shared_ptr<dhtnet::ChannelSocket> channel) {
    2049         8670 :                 onConnectionReady(deviceId, name, std::move(channel));
    2050         8672 :             });
    2051          746 :         lkCM.unlock();
    2052              : 
    2053          746 :         if (!conf.managerUri.empty() && accountManager_) {
    2054            0 :             dynamic_cast<ServerAccountManager*>(accountManager_.get())->onDeviceRevoked([this]() {
    2055            0 :                 JAMI_WARNING("[Account {}] Device revoked by server, deleting identity", getAccountID());
    2056            0 :                 editConfig([&](JamiAccountConfig& conf) {
    2057              :                     // Delete the revoked device's key and certificate files
    2058            0 :                     std::error_code ec;
    2059            0 :                     if (!conf.tlsPrivateKeyFile.empty())
    2060            0 :                         std::filesystem::remove(idPath_ / conf.tlsPrivateKeyFile, ec);
    2061            0 :                     if (!conf.tlsCertificateFile.empty())
    2062            0 :                         std::filesystem::remove(idPath_ / conf.tlsCertificateFile, ec);
    2063            0 :                     conf.tlsPrivateKeyFile.clear();
    2064            0 :                     conf.tlsCertificateFile.clear();
    2065            0 :                     conf.receipt.clear();
    2066            0 :                     conf.receiptSignature.clear();
    2067            0 :                 });
    2068            0 :                 Migration::setState(accountID_, Migration::State::INVALID);
    2069            0 :                 setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
    2070            0 :             });
    2071            0 :             dynamic_cast<ServerAccountManager*>(accountManager_.get())
    2072            0 :                 ->syncBlueprintConfig([this](const std::map<std::string, std::string>& config) {
    2073            0 :                     editConfig([&](JamiAccountConfig& conf) { conf.fromMap(config); });
    2074            0 :                     emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
    2075            0 :                 });
    2076              :         }
    2077              : 
    2078          746 :         if (presenceManager_)
    2079          746 :             presenceManager_->refresh();
    2080          746 :     } catch (const std::exception& e) {
    2081            0 :         JAMI_ERROR("Error registering DHT account: {}", e.what());
    2082            0 :         setRegistrationState(RegistrationState::ERROR_GENERIC);
    2083            0 :     }
    2084              : }
    2085              : 
    2086              : void
    2087          739 : JamiAccount::lookupRegisteredName(const std::string& regName, const NameDirectory::Response& response)
    2088              : {
    2089          739 :     if (response == NameDirectory::Response::found or response == NameDirectory::Response::notFound) {
    2090         1478 :         const auto& nameResult = response == NameDirectory::Response::found ? regName : "";
    2091          739 :         if (setRegisteredName(nameResult)) {
    2092            0 :             editConfig([&](JamiAccountConfig& config) { config.registeredName = nameResult; });
    2093            0 :             emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
    2094              :         }
    2095          739 :     }
    2096          739 : }
    2097              : 
    2098              : dht::DhtRunner::Config
    2099          746 : JamiAccount::initDhtConfig(const JamiAccountConfig& conf)
    2100              : {
    2101          746 :     dht::DhtRunner::Config config {};
    2102          746 :     config.dht_config.node_config.network = 0;
    2103          746 :     config.dht_config.node_config.maintain_storage = false;
    2104          746 :     config.dht_config.node_config.persist_path = (cachePath_ / "dhtstate").string();
    2105          746 :     config.dht_config.id = id_;
    2106          746 :     config.dht_config.cert_cache_all = true;
    2107          746 :     config.push_node_id = getAccountID();
    2108          746 :     config.push_token = conf.deviceKey;
    2109          746 :     config.push_topic = conf.notificationTopic;
    2110          746 :     config.push_platform = conf.platform;
    2111          746 :     config.proxy_user_agent = jami::userAgent();
    2112          746 :     config.threaded = true;
    2113          746 :     config.peer_discovery = conf.dhtPeerDiscovery;
    2114          746 :     config.peer_publish = conf.dhtPeerDiscovery;
    2115          746 :     if (conf.proxyEnabled)
    2116            0 :         config.proxy_server = proxyServerCached_;
    2117              : 
    2118          746 :     if (not config.proxy_server.empty()) {
    2119            0 :         JAMI_LOG("[Account {}] Using proxy server {}", getAccountID(), config.proxy_server);
    2120            0 :         if (not config.push_token.empty()) {
    2121            0 :             JAMI_LOG("[Account {}] using push notifications with platform: {}, topic: {}, token: {}",
    2122              :                      getAccountID(),
    2123              :                      config.push_platform,
    2124              :                      config.push_topic,
    2125              :                      config.push_token);
    2126              :         }
    2127              :     }
    2128          746 :     return config;
    2129            0 : }
    2130              : 
    2131              : dht::DhtRunner::Context
    2132          746 : JamiAccount::initDhtContext()
    2133              : {
    2134          746 :     dht::DhtRunner::Context context {};
    2135          746 :     context.peerDiscovery = peerDiscovery_;
    2136          746 :     context.rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
    2137              : 
    2138          746 :     auto dht_log_level = Manager::instance().dhtLogLevel;
    2139          746 :     if (dht_log_level > 0) {
    2140            0 :         context.logger = logger_;
    2141              :     }
    2142              : 
    2143         4094 :     context.certificateStore = [&](const DeviceId& pk_id) {
    2144         2602 :         std::vector<std::shared_ptr<dht::crypto::Certificate>> ret;
    2145         2602 :         if (auto cert = certStore().getCertificate(pk_id.toString()))
    2146         2602 :             ret.emplace_back(std::move(cert));
    2147         2602 :         JAMI_LOG("[Account {}] Query for local certificate store: {}: {} found.",
    2148              :                  getAccountID(),
    2149              :                  pk_id.toString(),
    2150              :                  ret.size());
    2151         2602 :         return ret;
    2152          746 :     };
    2153              : 
    2154         3693 :     context.statusChangedCallback = [this](dht::NodeStatus s4, dht::NodeStatus s6) {
    2155         2201 :         JAMI_LOG("[Account {}] DHT status: IPv4 {}; IPv6 {}", getAccountID(), dhtStatusStr(s4), dhtStatusStr(s6));
    2156              :         RegistrationState state;
    2157         2201 :         auto newStatus = std::max(s4, s6);
    2158         2201 :         switch (newStatus) {
    2159          725 :         case dht::NodeStatus::Connecting:
    2160          725 :             state = RegistrationState::TRYING;
    2161          725 :             break;
    2162         1476 :         case dht::NodeStatus::Connected:
    2163         1476 :             state = RegistrationState::REGISTERED;
    2164         1476 :             break;
    2165            0 :         case dht::NodeStatus::Disconnected:
    2166            0 :             state = RegistrationState::UNREGISTERED;
    2167            0 :             break;
    2168            0 :         default:
    2169            0 :             state = RegistrationState::ERROR_GENERIC;
    2170            0 :             break;
    2171              :         }
    2172              : 
    2173         2201 :         setRegistrationState(state);
    2174         2947 :     };
    2175              : 
    2176         2230 :     context.identityAnnouncedCb = [this](bool ok) {
    2177          738 :         if (!ok) {
    2178           13 :             JAMI_ERROR("[Account {}] Identity announcement failed", getAccountID());
    2179           13 :             return;
    2180              :         }
    2181          725 :         JAMI_WARNING("[Account {}] Identity announcement succeeded", getAccountID());
    2182          725 :         accountManager_
    2183         2259 :             ->startSync([this](const std::shared_ptr<dht::crypto::Certificate>& crt) { onAccountDeviceFound(crt); },
    2184         1450 :                         [this] { onAccountDeviceAnnounced(); },
    2185          725 :                         publishPresence_);
    2186          746 :     };
    2187              : 
    2188          746 :     return context;
    2189            0 : }
    2190              : 
    2191              : void
    2192          809 : JamiAccount::onAccountDeviceFound(const std::shared_ptr<dht::crypto::Certificate>& crt)
    2193              : {
    2194          809 :     if (jami::Manager::instance().syncOnRegister) {
    2195          809 :         if (!crt)
    2196            0 :             return;
    2197          809 :         auto deviceId = crt->getLongId().toString();
    2198          809 :         if (accountManager_->getInfo()->deviceId == deviceId)
    2199          736 :             return;
    2200              : 
    2201           73 :         dht::ThreadPool::io().run([w = weak(), crt] {
    2202           73 :             auto shared = w.lock();
    2203           73 :             if (!shared)
    2204            0 :                 return;
    2205              :             // Only establish a sync connection if this device may be missing a
    2206              :             // local contact/conversation-list change. This avoids waking up
    2207              :             // devices (especially mobiles) when there is nothing new to sync.
    2208           73 :             if (auto* sm = shared->syncModule()) {
    2209           73 :                 if (!sm->needsSync(crt->getLongId())) {
    2210            3 :                     JAMI_DEBUG("[Account {}] [device {}] up to date, skipping sync connection",
    2211              :                                shared->getAccountID(),
    2212              :                                crt->getLongId());
    2213            3 :                     return;
    2214              :                 }
    2215              :             }
    2216              :             // Initiate a message connection to create the first TCP link.
    2217              :             // Once established, onNewDeviceConnection will set up sync and
    2218              :             // swarm channels.
    2219           70 :             shared->connectSyncDevice(crt->getLongId());
    2220           73 :         });
    2221          809 :     }
    2222              : }
    2223              : 
    2224              : void
    2225         1118 : JamiAccount::connectSyncDevice(const DeviceId& deviceId)
    2226              : {
    2227         1118 :     requestMessageConnection(getUsername(), deviceId, "sync");
    2228         1118 : }
    2229              : 
    2230              : void
    2231          617 : JamiAccount::onSyncListChanged()
    2232              : {
    2233          617 :     if (!jami::Manager::instance().syncOnRegister)
    2234            0 :         return;
    2235              :     // Coalesce bursts of changes (e.g. initial sync delivering many contacts
    2236              :     // and conversations) into a single propagation pass. A single version bump
    2237              :     // already marks every device out of date, and sync is full-state, so
    2238              :     // collapsing many changes into one pass is also semantically correct.
    2239          617 :     std::lock_guard lk(syncListChangedMtx_);
    2240          617 :     if (!syncListChangedTimer_)
    2241          367 :         syncListChangedTimer_ = std::make_shared<asio::steady_timer>(*jami::Manager::instance().ioContext());
    2242          617 :     syncListChangedTimer_->expires_after(std::chrono::seconds(1));
    2243          617 :     syncListChangedTimer_->async_wait([w = weak()](const std::error_code& ec) {
    2244          617 :         if (ec) // cancelled by a more recent change (debounce) or shutting down
    2245          264 :             return;
    2246          353 :         dht::ThreadPool::io().run([w] {
    2247          353 :             auto shared = w.lock();
    2248          353 :             if (!shared)
    2249            0 :                 return;
    2250          353 :             auto* sm = shared->syncModule();
    2251          353 :             if (!sm)
    2252            0 :                 return;
    2253              :             // A list change makes every device potentially out of date.
    2254          353 :             sm->bumpVersion();
    2255              :             // (Re)connect to the account's other devices that are not up to
    2256              :             // date so the change is pushed. Offline ones are reached on their
    2257              :             // next presence announcement (onAccountDeviceFound).
    2258          353 :             auto am = shared->accountManager();
    2259          353 :             if (am && am->getInfo()) {
    2260          353 :                 auto currentDevice = shared->currentDeviceId();
    2261         1754 :                 for (const auto& [deviceId, device] : am->getKnownDevices()) {
    2262         1401 :                     if (deviceId.toString() == currentDevice)
    2263          353 :                         continue;
    2264         1048 :                     if (sm->needsSync(deviceId))
    2265         1048 :                         shared->connectSyncDevice(deviceId);
    2266              :                 }
    2267              :             }
    2268              :             // Push immediately to already-connected devices.
    2269          353 :             sm->syncWithConnected();
    2270          353 :         });
    2271              :     });
    2272          617 : }
    2273              : 
    2274              : void
    2275          725 : JamiAccount::onAccountDeviceAnnounced()
    2276              : {
    2277          725 :     if (jami::Manager::instance().syncOnRegister) {
    2278          725 :         deviceAnnounced_ = true;
    2279              : 
    2280              :         // Bootstrap at the end to avoid to be long to load.
    2281          725 :         dht::ThreadPool::io().run([w = weak()] {
    2282          725 :             if (auto shared = w.lock())
    2283         2175 :                 shared->convModule()->bootstrap();
    2284          725 :         });
    2285          725 :         emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
    2286              :     }
    2287          725 : }
    2288              : 
    2289              : void
    2290         1261 : JamiAccount::onNewDeviceConnection(const std::shared_ptr<dht::crypto::Certificate>& cert)
    2291              : {
    2292         1261 :     if (!cert || !cert->issuer)
    2293            0 :         return;
    2294              : 
    2295         1261 :     dht::ThreadPool::io().run([w = weak(), cert] {
    2296         1261 :         auto shared = w.lock();
    2297         1261 :         if (!shared)
    2298            0 :             return;
    2299              : 
    2300         1261 :         JAMI_WARNING("[Account {}] New device connection: {}", shared->getAccountID(), cert->getLongId());
    2301              : 
    2302         1261 :         const auto peerId = cert->issuer->getId().toString();
    2303         1261 :         const auto deviceId = cert->getLongId();
    2304         1261 :         auto am = shared->accountManager();
    2305         1261 :         if (!am || am->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
    2306            2 :             return;
    2307              :         }
    2308              : 
    2309         1259 :         const auto isSyncDevice = jami::Manager::instance().syncOnRegister && peerId == shared->getUsername();
    2310         2518 :         shared->requestMessageConnection(peerId, deviceId, isSyncDevice ? "sync" : "");
    2311              : 
    2312         1259 :         if (isSyncDevice) {
    2313           70 :             auto* sm = shared->syncModule();
    2314           70 :             if (sm && !sm->isConnected(deviceId)) {
    2315           69 :                 std::shared_lock lk(shared->connManagerMtx_);
    2316           69 :                 if (!shared->connectionManager_)
    2317            0 :                     return;
    2318              : 
    2319           69 :                 auto it = shared->channelHandlers_.find(Uri::Scheme::SYNC);
    2320           69 :                 if (it != shared->channelHandlers_.end() && it->second)
    2321          344 :                     it->second->connect(deviceId,
    2322              :                                         "",
    2323           69 :                                         [](const std::shared_ptr<dhtnet::ChannelSocket>& /*socket*/,
    2324           69 :                                            const DeviceId& /*deviceId*/) {});
    2325           69 :             }
    2326              :         }
    2327              : 
    2328              :         // Notify the DRT in all conversations where this peer is a member,
    2329              :         // so it can decide whether to open a swarm channel over the new connection.
    2330         1259 :         if (auto* cm = shared->convModule())
    2331         1259 :             cm->addKnownDevice(peerId, deviceId);
    2332              : 
    2333              :         // Proactively refresh the service cache for this device.
    2334              :         {
    2335         1258 :             std::shared_lock lk(shared->connManagerMtx_);
    2336         1258 :             auto it = shared->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
    2337         1259 :             if (it != shared->channelHandlers_.end() && it->second) {
    2338         1259 :                 static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->refreshDevice(peerId, deviceId);
    2339              :             }
    2340         1258 :         }
    2341         1265 :     });
    2342              : }
    2343              : 
    2344              : void
    2345          838 : JamiAccount::updateTrustedCa()
    2346              : {
    2347          838 :     if (!accountManager_)
    2348            0 :         return;
    2349          838 :     const auto* info = accountManager_->getInfo();
    2350          838 :     if (!info || !info->identity.second)
    2351            0 :         return;
    2352              : 
    2353          838 :     auto accountCert = info->identity.second->issuer;
    2354          838 :     if (!accountCert)
    2355            0 :         return;
    2356          838 :     auto caCert = accountCert->issuer;
    2357          838 :     if (!caCert)
    2358            0 :         return;
    2359              : 
    2360          838 :     auto status = config().allowPeersFromTrusted ? dhtnet::tls::TrustStore::PermissionStatus::ALLOWED
    2361          838 :                                                  : dhtnet::tls::TrustStore::PermissionStatus::UNDEFINED;
    2362          838 :     JAMI_LOG("[Account {}] {} organization CA {}",
    2363              :              getAccountID(),
    2364              :              config().allowPeersFromTrusted ? "Trusting" : "Untrusting",
    2365              :              caCert->getLongId());
    2366          838 :     setCertificateStatus(caCert, status, false);
    2367          838 : }
    2368              : 
    2369              : bool
    2370          792 : JamiAccount::onICERequest(const DeviceId& deviceId)
    2371              : {
    2372          792 :     std::promise<bool> accept;
    2373          792 :     std::future<bool> fut = accept.get_future();
    2374          792 :     accountManager_->findCertificate(deviceId, [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
    2375          792 :         if (!cert) {
    2376            0 :             accept.set_value(false);
    2377            0 :             return;
    2378              :         }
    2379          792 :         dht::InfoHash peer_account_id;
    2380          792 :         auto res = accountManager_->onPeerCertificate(cert, this->config().allowPublicIncoming, peer_account_id);
    2381          792 :         JAMI_LOG("[Account {}] [device {}] {} ICE request from {}",
    2382              :                  getAccountID(),
    2383              :                  cert->getLongId(),
    2384              :                  res ? "Accepting" : "Discarding",
    2385              :                  peer_account_id);
    2386          792 :         accept.set_value(res);
    2387              :     });
    2388          792 :     fut.wait();
    2389          792 :     auto result = fut.get();
    2390          792 :     return result;
    2391          792 : }
    2392              : 
    2393              : bool
    2394         4449 : JamiAccount::onChannelRequest(const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name)
    2395              : {
    2396         4449 :     JAMI_LOG("[Account {}] [device {}] New channel requested: '{}'", getAccountID(), cert->getLongId(), name);
    2397              : 
    2398         4458 :     if (this->config().turnEnabled && turnCache_) {
    2399         4454 :         auto addr = turnCache_->getResolvedTurn();
    2400         4458 :         if (addr == std::nullopt) {
    2401              :             // If TURN is enabled, but no TURN cached, there can be a temporary
    2402              :             // resolution error to solve. Sometimes, a connectivity change is not
    2403              :             // enough, so even if this case is really rare, it should be easy to avoid.
    2404            2 :             turnCache_->refresh();
    2405              :         }
    2406              :     }
    2407              : 
    2408         4457 :     auto uri = Uri(name);
    2409         4450 :     std::shared_lock lk(connManagerMtx_);
    2410         4450 :     auto itHandler = channelHandlers_.find(uri.scheme());
    2411         4449 :     if (itHandler != channelHandlers_.end() && itHandler->second)
    2412         4337 :         return itHandler->second->onRequest(cert, name);
    2413          112 :     return name == "sip";
    2414         4452 : }
    2415              : 
    2416              : void
    2417           20 : JamiAccount::connectYdocDevice(const DeviceId& deviceId, const std::string& documentId)
    2418              : {
    2419           20 :     std::shared_lock lk(connManagerMtx_);
    2420           20 :     auto itHandler = channelHandlers_.find(Uri::Scheme::YDOC);
    2421           20 :     if (itHandler == channelHandlers_.end() || !itHandler->second)
    2422            0 :         return;
    2423           40 :     itHandler->second->connect(deviceId, documentId, [](std::shared_ptr<dhtnet::ChannelSocket>, const DeviceId&) {});
    2424           20 : }
    2425              : 
    2426              : void
    2427         8677 : JamiAccount::onConnectionReady(const DeviceId& deviceId,
    2428              :                                const std::string& name,
    2429              :                                std::shared_ptr<dhtnet::ChannelSocket> channel)
    2430              : {
    2431         8677 :     if (channel) {
    2432         8676 :         auto cert = channel->peerCertificate();
    2433         8674 :         if (!cert || !cert->issuer)
    2434            0 :             return;
    2435         8675 :         auto peerId = cert->issuer->getId().toString();
    2436              :         // A connection request can be sent just before member is banned and this must be ignored.
    2437         8669 :         if (accountManager()->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
    2438            7 :             channel->shutdown();
    2439            7 :             return;
    2440              :         }
    2441         8670 :         if (name == "sip") {
    2442          186 :             cacheSIPConnection(std::move(channel), peerId, deviceId);
    2443         8484 :         } else if (name.find("git://") == 0) {
    2444         2085 :             auto sep = name.find_last_of('/');
    2445         2086 :             auto conversationId = name.substr(sep + 1);
    2446         2085 :             auto targetDevice = name.substr(6, sep - 6);
    2447         2085 :             auto remoteDevice = deviceId.toString();
    2448              : 
    2449         2085 :             if (channel->isInitiator()) {
    2450              :                 // Check if wanted remote is our side (git://targetDevice/conversationId)
    2451         1043 :                 return;
    2452              :             }
    2453              : 
    2454         1043 :             if (targetDevice != currentDeviceId()) {
    2455            0 :                 JAMI_WARNING(
    2456              :                     "[Account {:s}] [Conversation {}] Git server requested for device {}, but this is not ours.",
    2457              :                     getAccountID(),
    2458              :                     conversationId,
    2459              :                     targetDevice);
    2460            0 :                 channel->shutdown();
    2461            0 :                 return;
    2462              :             }
    2463              : 
    2464         1043 :             auto sock = convModule()->gitSocket(remoteDevice, conversationId);
    2465         1043 :             if (sock == channel) {
    2466              :                 // The onConnectionReady is already used as client (for retrieving messages)
    2467              :                 // So it's not the server socket
    2468            0 :                 return;
    2469              :             }
    2470              : 
    2471         1043 :             auto startGitServer = [w = weak(), conversationId, remoteDevice, channel]() {
    2472         1043 :                 auto shared = w.lock();
    2473         1043 :                 if (!shared)
    2474            0 :                     return;
    2475         1043 :                 JAMI_LOG("[Account {:s}] [Conversation {}] [device {}] Git server requested",
    2476              :                          shared->accountID_,
    2477              :                          conversationId,
    2478              :                          remoteDevice);
    2479         1043 :                 auto gs = std::make_unique<GitServer>(shared->accountID_, conversationId, channel);
    2480         1043 :                 shared->syncCnt_.fetch_add(1);
    2481              :                 // Balances the increment above exactly once, whether the peer ends up
    2482              :                 // fetching or the channel simply dies.
    2483         4046 :                 auto endSync = [w, done = std::make_shared<std::once_flag>()] {
    2484         3003 :                     std::call_once(*done, [&] {
    2485         1043 :                         if (auto shared = w.lock())
    2486         2086 :                             if (shared->syncCnt_.fetch_sub(1) == 1)
    2487          991 :                                 emitSignal<libjami::ConversationSignal::ConversationCloned>(
    2488         2034 :                                     shared->getAccountID().c_str());
    2489         1043 :                     });
    2490         4047 :                 };
    2491         1043 :                 gs->setOnFetched([w, conversationId, remoteDevice, endSync](const std::string& commit) {
    2492         1963 :                     dht::ThreadPool::computation().run([w, conversationId, remoteDevice, commit, endSync]() {
    2493         1962 :                         if (auto shared = w.lock()) {
    2494         1963 :                             shared->convModule()->setFetched(conversationId, remoteDevice, commit);
    2495         1963 :                             endSync();
    2496         1963 :                         }
    2497         1963 :                     });
    2498         1963 :                 });
    2499         1043 :                 const dht::Value::Id serverId = ValueIdDist()(shared->rand);
    2500              :                 {
    2501         1043 :                     std::lock_guard lk(shared->gitServersMtx_);
    2502         1043 :                     shared->gitServers_[serverId] = std::move(gs);
    2503         1043 :                 }
    2504         1043 :                 channel->onShutdown([w, serverId, endSync](const std::error_code&) {
    2505         1038 :                     endSync();
    2506              :                     // Run on main thread to avoid to be in mxSock's eventLoop
    2507         1041 :                     runOnMainThread([serverId, w]() {
    2508         1043 :                         if (auto sthis = w.lock()) {
    2509         1043 :                             std::lock_guard lk(sthis->gitServersMtx_);
    2510         1043 :                             sthis->gitServers_.erase(serverId);
    2511         2086 :                         }
    2512         1043 :                     });
    2513         1042 :                 });
    2514         2086 :             };
    2515              : 
    2516         1043 :             if (!convModule()->isPeerAuthorized(conversationId, peerId, remoteDevice, true)) {
    2517              :                 // Not a member — but if this is a collaborative document held
    2518              :                 // here, a member of its parent conversation may still open it:
    2519              :                 // the serving holder writes the add commit, then serves.
    2520              :                 convModule()
    2521           30 :                     ->authorizeDocumentPeer(conversationId,
    2522              :                                             peerId,
    2523              :                                             remoteDevice,
    2524           30 :                                             [w = weak(), conversationId, peerId, remoteDevice, channel, startGitServer](
    2525              :                                                 bool authorized) {
    2526           15 :                                                 if (authorized) {
    2527           15 :                                                     startGitServer();
    2528           15 :                                                     return;
    2529              :                                                 }
    2530            0 :                                                 if (auto shared = w.lock())
    2531            0 :                                                     JAMI_WARNING("[Account {:s}] [Conversation {}] Git "
    2532              :                                                                  "server requested, but peer {}/{} is "
    2533              :                                                                  "not authorized",
    2534              :                                                                  shared->getAccountID(),
    2535              :                                                                  conversationId,
    2536              :                                                                  peerId,
    2537            0 :                                                                  remoteDevice);
    2538            0 :                                                 channel->shutdown();
    2539              :                                             });
    2540           15 :                 return;
    2541              :             }
    2542              : 
    2543         1028 :             startGitServer();
    2544         4231 :         } else {
    2545              :             // TODO move git://
    2546         6396 :             std::shared_lock lk(connManagerMtx_);
    2547         6396 :             auto uri = Uri(name);
    2548         6394 :             auto itHandler = channelHandlers_.find(uri.scheme());
    2549         6394 :             if (itHandler != channelHandlers_.end() && itHandler->second)
    2550         6389 :                 itHandler->second->onReady(cert, name, std::move(channel));
    2551         6400 :         }
    2552         9741 :     }
    2553              : }
    2554              : 
    2555              : void
    2556         2139 : JamiAccount::conversationNeedsSyncing(std::shared_ptr<SyncMsg>&& syncMsg)
    2557              : {
    2558              :     // Decide from the message *content* whether it can require (re)opening sync
    2559              :     // connections. A change to the contact/conversation list (or requests) must
    2560              :     // reach devices that are not currently connected; a metadata-only update
    2561              :     // (read status, preferences) only rides the existing connections. Checking
    2562              :     // the content rather than merely "is syncMsg null" keeps this correct if
    2563              :     // list data is ever attached to a syncMsg in the future.
    2564         2139 :     if (syncMsg && !syncMsg->affectsList()) {
    2565              :         // Metadata-only update: ride the existing sync connections, never open
    2566              :         // new ones.
    2567         1658 :         dht::ThreadPool::computation().run([w = weak(), syncMsg = std::move(syncMsg)] {
    2568         1658 :             if (auto shared = w.lock())
    2569         1658 :                 if (auto* sm = shared->syncModule())
    2570         1658 :                     sm->syncWithConnected(syncMsg);
    2571         1658 :         });
    2572         1658 :         return;
    2573              :     }
    2574              :     // Contact/conversation-list change: for JAMS accounts, update the server;
    2575              :     // then bump the local sync version and (re)connect/push to other devices.
    2576              :     // A non-null syncMsg carrying list state is covered by the full-state push
    2577              :     // performed by onSyncListChanged().
    2578          481 :     dht::ThreadPool::computation().run([w = weak()] {
    2579          481 :         auto shared = w.lock();
    2580          481 :         if (!shared)
    2581            0 :             return;
    2582          481 :         const auto& config = shared->config();
    2583          481 :         if (!config.managerUri.empty())
    2584            0 :             if (auto am = shared->accountManager())
    2585            0 :                 am->syncDevices();
    2586          481 :         shared->onSyncListChanged();
    2587          481 :     });
    2588              : }
    2589              : 
    2590              : uint64_t
    2591        13358 : JamiAccount::conversationSendMessage(const std::string& uri,
    2592              :                                      const DeviceId& device,
    2593              :                                      const std::map<std::string, std::string>& msg,
    2594              :                                      uint64_t token)
    2595              : {
    2596              :     // No need to retrigger, sendTextMessage will call
    2597              :     // messageEngine_.sendMessage, already retriggering on
    2598              :     // main thread.
    2599        15512 :     auto deviceId = device ? device.toString() : "";
    2600        26673 :     return sendTextMessage(uri, deviceId, msg, token);
    2601        13342 : }
    2602              : 
    2603              : void
    2604         2120 : JamiAccount::onConversationNeedSocket(const std::string& convId,
    2605              :                                       const std::string& deviceId,
    2606              :                                       ChannelCb&& cb,
    2607              :                                       const std::string& type,
    2608              :                                       bool /*noNewSocket*/)
    2609              : {
    2610         2120 :     dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::move(cb), type] {
    2611         2120 :         auto shared = w.lock();
    2612         2120 :         if (!shared)
    2613            0 :             return;
    2614         2120 :         if (auto socket = shared->convModule()->gitSocket(deviceId, convId)) {
    2615         1014 :             auto remoteCert = socket->peerCertificate();
    2616         1014 :             if (!remoteCert || !remoteCert->issuer
    2617         3042 :                 || !shared->convModule()->isPeerAuthorized(convId,
    2618         2028 :                                                            remoteCert->issuer->getId().toString(),
    2619         2028 :                                                            socket->deviceId().toString(),
    2620              :                                                            true)) {
    2621            0 :                 socket->shutdown();
    2622            0 :                 shared->convModule()->removeGitSocket(socket->deviceId().toString(), convId, socket);
    2623            0 :                 cb({});
    2624            0 :                 return;
    2625              :             }
    2626         1013 :             if (!cb(socket))
    2627            0 :                 socket->shutdown();
    2628         1014 :             return;
    2629         3134 :         }
    2630         1106 :         std::shared_lock lkCM(shared->connManagerMtx_);
    2631         1106 :         if (!shared->connectionManager_) {
    2632           10 :             lkCM.unlock();
    2633           10 :             cb({});
    2634           10 :             return;
    2635              :         }
    2636              : 
    2637         2192 :         shared->connectionManager_->connectDevice(
    2638         1096 :             DeviceId(deviceId),
    2639         3288 :             fmt::format("git://{}/{}", deviceId, convId),
    2640         2192 :             [w, cb = std::move(cb), convId, requestedDeviceId = deviceId](std::shared_ptr<dhtnet::ChannelSocket> socket,
    2641              :                                                                           const DeviceId&) {
    2642         2192 :                 dht::ThreadPool::io().run(
    2643         2192 :                     [w, cb = std::move(cb), socket = std::move(socket), convId, requestedDeviceId] {
    2644         1096 :                         if (socket) {
    2645         1043 :                             auto shared = w.lock();
    2646         1043 :                             auto remoteCert = socket->peerCertificate();
    2647         1043 :                             auto remoteDeviceId = socket->deviceId().toString();
    2648         1043 :                             if (!shared || !remoteCert || !remoteCert->issuer || remoteDeviceId != requestedDeviceId
    2649         3129 :                                 || !shared->convModule()->isPeerAuthorized(convId,
    2650         2086 :                                                                            remoteCert->issuer->getId().toString(),
    2651              :                                                                            remoteDeviceId,
    2652              :                                                                            true)) {
    2653            0 :                                 socket->shutdown();
    2654            0 :                                 cb({});
    2655            0 :                                 return;
    2656              :                             }
    2657         1043 :                             socket->onShutdown([w, deviceId = socket->deviceId(), convId, ws = std::weak_ptr(socket)](
    2658              :                                                    const std::error_code&) {
    2659         1043 :                                 dht::ThreadPool::io().run([w, deviceId, convId, ws] {
    2660         1042 :                                     auto shared = w.lock();
    2661              :                                     // Identify the channel, so that one dying after it has already
    2662              :                                     // been replaced doesn't unregister its replacement.
    2663         1041 :                                     auto socket = ws.lock();
    2664         1042 :                                     if (shared && socket)
    2665         1027 :                                         shared->convModule()->removeGitSocket(deviceId.toString(), convId, socket);
    2666         1038 :                                 });
    2667         1043 :                             });
    2668         1043 :                             if (!cb(socket))
    2669            7 :                                 socket->shutdown();
    2670         1043 :                         } else
    2671           53 :                             cb({});
    2672              :                     });
    2673         1096 :             },
    2674              :             false,
    2675              :             false,
    2676         1096 :             type);
    2677         2130 :     });
    2678         2120 : }
    2679              : 
    2680              : void
    2681         1175 : JamiAccount::onConversationNeedSwarmSocket(const std::string& convId,
    2682              :                                            const std::string& deviceId,
    2683              :                                            ChannelCb&& cb,
    2684              :                                            const std::string& /*type*/,
    2685              :                                            bool noNewSocket)
    2686              : {
    2687         1175 :     dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::forward<ChannelCb&&>(cb), noNewSocket] {
    2688         1174 :         auto shared = w.lock();
    2689         1174 :         if (!shared)
    2690            0 :             return;
    2691         1173 :         auto* cm = shared->convModule();
    2692         1175 :         std::shared_lock lkCM(shared->connManagerMtx_);
    2693         1175 :         if (!shared->connectionManager_ || !cm || cm->isDeviceBanned(convId, deviceId)) {
    2694           90 :             asio::post(*Manager::instance().ioContext(), [cb = std::move(cb)] { cb({}); });
    2695           45 :             return;
    2696              :         }
    2697         1130 :         DeviceId device(deviceId);
    2698         1130 :         auto swarmUri = fmt::format("swarm://{}", convId);
    2699         1130 :         dhtnet::ConnectDeviceOptions opts;
    2700         1130 :         opts.connType = "";
    2701         1130 :         opts.noNewSocket = noNewSocket;
    2702         1130 :         opts.uniqueName = true;
    2703         2259 :         shared->connectionManager_->connectDevice(
    2704              :             device,
    2705              :             swarmUri,
    2706         3389 :             [w,
    2707         1130 :              cb = std::move(cb),
    2708              :              wam = std::weak_ptr(shared->accountManager())](std::shared_ptr<dhtnet::ChannelSocket> socket,
    2709              :                                                             const DeviceId&) {
    2710         1130 :                 dht::ThreadPool::io().run([w, wam, cb = std::move(cb), socket = std::move(socket)] {
    2711         1129 :                     if (socket) {
    2712          828 :                         auto shared = w.lock();
    2713          827 :                         auto am = wam.lock();
    2714          827 :                         auto remoteCert = socket->peerCertificate();
    2715          827 :                         if (!remoteCert || !remoteCert->issuer) {
    2716            0 :                             cb(nullptr);
    2717            0 :                             return;
    2718              :                         }
    2719          828 :                         auto uri = remoteCert->issuer->getId().toString();
    2720         1646 :                         if (!shared || !am
    2721         1649 :                             || am->getCertificateStatus(uri) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
    2722            0 :                             cb(nullptr);
    2723            0 :                             return;
    2724              :                         }
    2725          829 :                     }
    2726         1129 :                     cb(socket);
    2727              :                 });
    2728         1130 :             },
    2729              :             opts);
    2730         1220 :     });
    2731         1175 : }
    2732              : 
    2733              : void
    2734            1 : JamiAccount::conversationOneToOneReceive(const std::string& convId, const std::string& from)
    2735              : {
    2736            2 :     accountManager_->findCertificate(dht::InfoHash(from),
    2737            2 :                                      [this, from, convId](const std::shared_ptr<dht::crypto::Certificate>& cert) {
    2738            1 :                                          const auto* info = accountManager_->getInfo();
    2739            1 :                                          if (!cert || !info)
    2740            0 :                                              return;
    2741            2 :                                          info->contacts->onTrustRequest(dht::InfoHash(from),
    2742              :                                                                         cert->getSharedPublicKey(),
    2743              :                                                                         nowMs(),
    2744              :                                                                         false,
    2745            1 :                                                                         convId,
    2746              :                                                                         {});
    2747              :                                      });
    2748            1 : }
    2749              : 
    2750              : ConversationModule*
    2751        35004 : JamiAccount::convModule(bool noCreation)
    2752              : {
    2753        35004 :     if (noCreation)
    2754         6845 :         return convModule_.get();
    2755        28159 :     if (!accountManager() || currentDeviceId() == "") {
    2756            0 :         JAMI_ERROR("[Account {}] Calling convModule() with an uninitialized account", getAccountID());
    2757            0 :         return nullptr;
    2758              :     }
    2759        28153 :     std::unique_lock lock(configurationMutex_);
    2760        28159 :     std::lock_guard lk(moduleMtx_);
    2761        28156 :     if (!convModule_) {
    2762         1442 :         convModule_ = std::make_unique<ConversationModule>(
    2763          721 :             shared(),
    2764          721 :             accountManager_,
    2765         2139 :             [this](auto&& syncMsg) { conversationNeedsSyncing(std::forward<std::shared_ptr<SyncMsg>>(syncMsg)); },
    2766            0 :             [this](auto&& uri, auto&& device, auto&& msg, auto token = 0) {
    2767        13362 :                 return conversationSendMessage(uri, device, msg, token);
    2768              :             },
    2769            0 :             [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
    2770         2120 :                 onConversationNeedSocket(convId, deviceId, std::forward<decltype(cb)>(cb), connectionType, noNewSocket);
    2771         2120 :             },
    2772            0 :             [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
    2773         1173 :                 onConversationNeedSwarmSocket(convId,
    2774              :                                               deviceId,
    2775         1173 :                                               std::forward<decltype(cb)>(cb),
    2776              :                                               connectionType,
    2777              :                                               noNewSocket);
    2778         1175 :             },
    2779          722 :             [this](const auto& convId, const auto& from) { conversationOneToOneReceive(convId, from); },
    2780         1442 :             autoLoadConversations_);
    2781              :     }
    2782        28151 :     return convModule_.get();
    2783        28156 : }
    2784              : 
    2785              : SyncModule*
    2786         3112 : JamiAccount::syncModule()
    2787              : {
    2788         3112 :     if (!accountManager() || currentDeviceId() == "") {
    2789            0 :         JAMI_ERROR("Calling syncModule() with an uninitialized account.");
    2790            0 :         return nullptr;
    2791              :     }
    2792         3112 :     std::lock_guard lk(moduleMtx_);
    2793         3112 :     if (!syncModule_)
    2794          708 :         syncModule_ = std::make_unique<SyncModule>(shared());
    2795         3112 :     return syncModule_.get();
    2796         3112 : }
    2797              : 
    2798              : std::shared_ptr<CollaborativeEditing>
    2799          450 : JamiAccount::collaborativeEditing()
    2800              : {
    2801          450 :     std::lock_guard lk(moduleMtx_);
    2802          450 :     if (!collaborativeEditing_)
    2803          168 :         collaborativeEditing_ = std::make_shared<CollaborativeEditing>(shared());
    2804          900 :     return collaborativeEditing_;
    2805          450 : }
    2806              : 
    2807              : void
    2808        12386 : JamiAccount::onTextMessage(const std::string& id,
    2809              :                            const std::string& from,
    2810              :                            const std::shared_ptr<dht::crypto::Certificate>& peerCert,
    2811              :                            const std::map<std::string, std::string>& payloads)
    2812              : {
    2813              :     try {
    2814        24780 :         const std::string fromUri {parseJamiUri(from)};
    2815        12384 :         SIPAccountBase::onTextMessage(id, fromUri, peerCert, payloads);
    2816        12394 :     } catch (...) {
    2817            0 :     }
    2818        12396 : }
    2819              : 
    2820              : void
    2821            0 : JamiAccount::loadConversation(const std::string& convId)
    2822              : {
    2823            0 :     if (auto* cm = convModule(true))
    2824            0 :         cm->loadSingleConversation(convId);
    2825            0 : }
    2826              : 
    2827              : void
    2828          995 : JamiAccount::doUnregister(bool forceShutdownConnections)
    2829              : {
    2830          995 :     std::unique_lock lock(configurationMutex_);
    2831          995 :     if (registrationState_ >= RegistrationState::ERROR_GENERIC) {
    2832          136 :         return;
    2833              :     }
    2834              : 
    2835          859 :     std::mutex mtx;
    2836          859 :     std::condition_variable cv;
    2837          859 :     bool shutdown_complete {false};
    2838              : 
    2839          859 :     if (peerDiscovery_) {
    2840            0 :         peerDiscovery_->stopPublish(PEER_DISCOVERY_JAMI_SERVICE);
    2841            0 :         peerDiscovery_->stopDiscovery(PEER_DISCOVERY_JAMI_SERVICE);
    2842              :     }
    2843              : 
    2844              :     // Edits made here since the last checkpoint are pending for persistence only
    2845              :     // on this device. Online receivers may have merged them into live replicas,
    2846              :     // but they do not checkpoint remote updates, so those copies are volatile.
    2847              :     // Write the pending updates out before anything is torn down. Copy the
    2848              :     // pointer under its own lock: the manager is created lazily, and a channel
    2849              :     // request or a client call can still be assigning it on another thread.
    2850          859 :     std::shared_ptr<CollaborativeEditing> collab;
    2851              :     {
    2852          859 :         std::lock_guard lk(moduleMtx_);
    2853          859 :         collab = collaborativeEditing_;
    2854          859 :     }
    2855          859 :     if (collab) {
    2856              :         // flush() reaches convModule() and the CRDT document lock; both would
    2857              :         // deadlock against configurationMutex_, which we hold here.
    2858          174 :         lock.unlock();
    2859          174 :         collab->flush();
    2860          174 :         lock.lock();
    2861              :         // Another unregistration may have completed while the lock was down.
    2862          174 :         if (registrationState_ >= RegistrationState::ERROR_GENERIC)
    2863            0 :             return;
    2864              :     }
    2865              : 
    2866          859 :     JAMI_WARNING("[Account {}] Unregistering account {}", getAccountID(), fmt::ptr(this));
    2867         1718 :     dht_->shutdown(
    2868          859 :         [&] {
    2869          859 :             JAMI_WARNING("[Account {}] DHT shutdown complete", getAccountID());
    2870          859 :             std::lock_guard lock(mtx);
    2871          859 :             shutdown_complete = true;
    2872          859 :             cv.notify_all();
    2873          859 :         },
    2874              :         true);
    2875              : 
    2876              :     {
    2877          859 :         std::lock_guard lk(pendingCallsMutex_);
    2878          859 :         pendingCalls_.clear();
    2879          859 :     }
    2880              : 
    2881              :     // Stop all current P2P connections if account is disabled
    2882              :     // or if explicitly requested by the caller.
    2883              :     // NOTE: Leaving the connections open is useful when changing an account's config.
    2884          859 :     if (not isEnabled() || forceShutdownConnections)
    2885          840 :         shutdownConnections();
    2886              : 
    2887              :     // Keep the UPnP mapping across a plain config reload: the DHT will rebind the
    2888              :     // same port and doRegister_() then reuses the mapping instead of allocating a new one.
    2889          859 :     if (not isEnabled() || forceShutdownConnections)
    2890          840 :         releaseDhtUpnpMapping();
    2891              : 
    2892              :     {
    2893          859 :         std::unique_lock lock(mtx);
    2894         2458 :         cv.wait(lock, [&] { return shutdown_complete; });
    2895          859 :     }
    2896          859 :     dht_->join();
    2897          859 :     setRegistrationState(RegistrationState::UNREGISTERED);
    2898              : 
    2899          859 :     lock.unlock();
    2900              : 
    2901              : #ifdef ENABLE_PLUGIN
    2902         1718 :     jami::Manager::instance().getJamiPluginManager().getChatServicesManager().cleanChatSubjects(getAccountID());
    2903              : #endif
    2904          995 : }
    2905              : 
    2906              : void
    2907         1679 : JamiAccount::releaseDhtUpnpMapping()
    2908              : {
    2909         1679 :     if (dhtUpnpMapping_.getInternalPort() == 0)
    2910          999 :         return;
    2911              : 
    2912          680 :     if (upnpCtrl_)
    2913          680 :         upnpCtrl_->releaseMapping(dhtUpnpMapping_);
    2914          680 :     dhtUpnpMapping_.setNotifyCallback(nullptr);
    2915          680 :     dhtUpnpMapping_.updateFrom(dhtnet::upnp::Mapping(dhtnet::upnp::PortType::UDP));
    2916              : }
    2917              : 
    2918              : void
    2919         5448 : JamiAccount::setRegistrationState(RegistrationState state, int detail_code, const std::string& detail_str)
    2920              : {
    2921         5448 :     if (registrationState_ != state) {
    2922         3868 :         if (state == RegistrationState::REGISTERED) {
    2923          740 :             JAMI_WARNING("[Account {}] Connected", getAccountID());
    2924          740 :             turnCache_->refresh();
    2925          740 :             if (connectionManager_)
    2926          733 :                 connectionManager_->storeActiveIpAddress();
    2927         3128 :         } else if (state == RegistrationState::TRYING) {
    2928          746 :             JAMI_WARNING("[Account {}] Connecting…", getAccountID());
    2929              :         } else {
    2930         2382 :             deviceAnnounced_ = false;
    2931         2382 :             JAMI_WARNING("[Account {}] Disconnected", getAccountID());
    2932              :         }
    2933              :     }
    2934              :     // Update registrationState_ & emit signals
    2935         5448 :     Account::setRegistrationState(state, detail_code, detail_str);
    2936         5448 : }
    2937              : 
    2938              : void
    2939            0 : JamiAccount::reloadContacts()
    2940              : {
    2941            0 :     accountManager_->reloadContacts();
    2942            0 : }
    2943              : 
    2944              : void
    2945            0 : JamiAccount::connectivityChanged()
    2946              : {
    2947            0 :     if (not isUsable()) {
    2948              :         // nothing to do
    2949            0 :         return;
    2950              :     }
    2951            0 :     JAMI_WARNING("[{}] connectivityChanged", getAccountID());
    2952              : 
    2953            0 :     if (auto* cm = convModule())
    2954            0 :         cm->connectivityChanged();
    2955            0 :     dht_->connectivityChanged();
    2956              :     {
    2957            0 :         std::shared_lock lkCM(connManagerMtx_);
    2958            0 :         if (connectionManager_) {
    2959            0 :             connectionManager_->connectivityChanged();
    2960              :             // reset cache
    2961            0 :             connectionManager_->setPublishedAddress({});
    2962              :         }
    2963            0 :     }
    2964              : }
    2965              : 
    2966              : bool
    2967            0 : JamiAccount::findCertificate(const dht::InfoHash& h,
    2968              :                              std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
    2969              : {
    2970            0 :     if (accountManager_)
    2971            0 :         return accountManager_->findCertificate(h, std::move(cb));
    2972            0 :     return false;
    2973              : }
    2974              : 
    2975              : bool
    2976            0 : JamiAccount::findCertificate(const dht::PkId& id,
    2977              :                              std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
    2978              : {
    2979            0 :     if (accountManager_)
    2980            0 :         return accountManager_->findCertificate(id, std::move(cb));
    2981            0 :     return false;
    2982              : }
    2983              : 
    2984              : bool
    2985          924 : JamiAccount::findCertificate(const std::string& crt_id)
    2986              : {
    2987          924 :     if (accountManager_)
    2988          924 :         return accountManager_->findCertificate(dht::InfoHash(crt_id));
    2989            0 :     return false;
    2990              : }
    2991              : 
    2992              : bool
    2993           89 : JamiAccount::setCertificateStatus(const std::string& cert_id, dhtnet::tls::TrustStore::PermissionStatus status)
    2994              : {
    2995           89 :     bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
    2996           89 :     if (done) {
    2997           86 :         findCertificate(cert_id);
    2998           86 :         emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
    2999              :                                                                           cert_id,
    3000              :                                                                           dhtnet::tls::TrustStore::statusToStr(status));
    3001              :     }
    3002           89 :     return done;
    3003              : }
    3004              : 
    3005              : bool
    3006          838 : JamiAccount::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
    3007              :                                   dhtnet::tls::TrustStore::PermissionStatus status,
    3008              :                                   bool local)
    3009              : {
    3010          838 :     bool done = accountManager_ ? accountManager_->setCertificateStatus(cert, status, local) : false;
    3011          838 :     if (done) {
    3012          838 :         findCertificate(cert->getLongId().toString());
    3013         1676 :         emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
    3014         1676 :                                                                           cert->getLongId().toString(),
    3015              :                                                                           dhtnet::tls::TrustStore::statusToStr(status));
    3016              :     }
    3017          838 :     return done;
    3018              : }
    3019              : 
    3020              : std::vector<std::string>
    3021            0 : JamiAccount::getCertificatesByStatus(dhtnet::tls::TrustStore::PermissionStatus status)
    3022              : {
    3023            0 :     if (accountManager_)
    3024            0 :         return accountManager_->getCertificatesByStatus(status);
    3025            0 :     return {};
    3026              : }
    3027              : 
    3028              : bool
    3029            0 : JamiAccount::isMessageTreated(dht::Value::Id id)
    3030              : {
    3031            0 :     std::lock_guard lock(messageMutex_);
    3032            0 :     return !treatedMessages_.add(id);
    3033            0 : }
    3034              : 
    3035              : bool
    3036           13 : JamiAccount::sha3SumVerify() const
    3037              : {
    3038           13 :     return !noSha3sumVerification_;
    3039              : }
    3040              : 
    3041              : #ifdef LIBJAMI_TEST
    3042              : void
    3043            1 : JamiAccount::noSha3sumVerification(bool newValue)
    3044              : {
    3045            1 :     noSha3sumVerification_ = newValue;
    3046            1 : }
    3047              : #endif
    3048              : 
    3049              : std::map<std::string, std::string>
    3050            0 : JamiAccount::getKnownDevices() const
    3051              : {
    3052            0 :     std::lock_guard lock(configurationMutex_);
    3053            0 :     if (not accountManager_ or not accountManager_->getInfo())
    3054            0 :         return {};
    3055            0 :     std::map<std::string, std::string> ids;
    3056            0 :     for (const auto& d : accountManager_->getKnownDevices()) {
    3057            0 :         auto id = d.first.toString();
    3058            0 :         auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
    3059            0 :         ids.emplace(std::move(id), std::move(label));
    3060            0 :     }
    3061            0 :     return ids;
    3062            0 : }
    3063              : 
    3064              : void
    3065            0 : JamiAccount::loadCachedUrl(const std::string& url,
    3066              :                            const std::filesystem::path& cachePath,
    3067              :                            const std::chrono::seconds& cacheDuration,
    3068              :                            const std::function<void(const dht::http::Response& response)>& cb)
    3069              : {
    3070            0 :     dht::ThreadPool::io().run([cb, url, cachePath, cacheDuration, w = weak()]() {
    3071              :         try {
    3072            0 :             std::string data;
    3073              :             {
    3074            0 :                 std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
    3075            0 :                 data = fileutils::loadCacheTextFile(cachePath, cacheDuration);
    3076            0 :             }
    3077            0 :             dht::http::Response ret;
    3078            0 :             ret.body = std::move(data);
    3079            0 :             ret.status_code = 200;
    3080            0 :             cb(ret);
    3081            0 :         } catch (const std::exception& e) {
    3082            0 :             JAMI_LOG("Failed to load '{}' from '{}': {}", url, cachePath, e.what());
    3083              : 
    3084            0 :             if (auto sthis = w.lock()) {
    3085              :                 auto req = std::make_shared<dht::http::Request>(
    3086            0 :                     *Manager::instance().ioContext(), url, [cb, cachePath, w](const dht::http::Response& response) {
    3087            0 :                         if (response.status_code == 200) {
    3088              :                             try {
    3089            0 :                                 std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
    3090            0 :                                 fileutils::saveFile(cachePath,
    3091            0 :                                                     (const uint8_t*) response.body.data(),
    3092              :                                                     response.body.size(),
    3093              :                                                     0600);
    3094            0 :                                 JAMI_LOG("Cached result to '{}'", cachePath);
    3095            0 :                             } catch (const std::exception& ex) {
    3096            0 :                                 JAMI_WARNING("Failed to save result to '{}': {}", cachePath, ex.what());
    3097            0 :                             }
    3098            0 :                             cb(response);
    3099              :                         } else {
    3100              :                             try {
    3101            0 :                                 if (std::filesystem::exists(cachePath)) {
    3102            0 :                                     JAMI_WARNING("Failed to download URL, using cached data");
    3103            0 :                                     std::string data;
    3104              :                                     {
    3105            0 :                                         std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
    3106            0 :                                         data = fileutils::loadTextFile(cachePath);
    3107            0 :                                     }
    3108            0 :                                     dht::http::Response ret;
    3109            0 :                                     ret.body = std::move(data);
    3110            0 :                                     ret.status_code = 200;
    3111            0 :                                     cb(ret);
    3112            0 :                                 } else
    3113            0 :                                     throw std::runtime_error("No cached data");
    3114            0 :                             } catch (...) {
    3115            0 :                                 cb(response);
    3116            0 :                             }
    3117              :                         }
    3118            0 :                         if (auto req = response.request.lock())
    3119            0 :                             if (auto sthis = w.lock())
    3120            0 :                                 sthis->requests_.erase(req);
    3121            0 :                     });
    3122            0 :                 sthis->requests_.emplace(req);
    3123            0 :                 req->send();
    3124            0 :             }
    3125            0 :         }
    3126            0 :     });
    3127            0 : }
    3128              : 
    3129              : void
    3130          746 : JamiAccount::loadCachedProxyServer(std::function<void(const std::string& proxy)> cb)
    3131              : {
    3132          746 :     const auto& conf = config();
    3133          746 :     if (conf.proxyEnabled and proxyServerCached_.empty()) {
    3134            0 :         JAMI_DEBUG("[Account {:s}] Loading DHT proxy URL: {:s}", getAccountID(), conf.proxyListUrl);
    3135            0 :         if (conf.proxyListUrl.empty() or not conf.proxyListEnabled) {
    3136            0 :             cb(getDhtProxyServer(conf.proxyServer));
    3137              :         } else {
    3138            0 :             loadCachedUrl(conf.proxyListUrl,
    3139            0 :                           cachePath_ / "dhtproxylist",
    3140            0 :                           std::chrono::hours(24 * 3),
    3141            0 :                           [w = weak(), cb = std::move(cb)](const dht::http::Response& response) {
    3142            0 :                               if (auto sthis = w.lock()) {
    3143            0 :                                   if (response.status_code == 200) {
    3144            0 :                                       cb(sthis->getDhtProxyServer(response.body));
    3145              :                                   } else {
    3146            0 :                                       cb(sthis->getDhtProxyServer(sthis->config().proxyServer));
    3147              :                                   }
    3148            0 :                               }
    3149            0 :                           });
    3150              :         }
    3151              :     } else {
    3152          746 :         cb(proxyServerCached_);
    3153              :     }
    3154          746 : }
    3155              : 
    3156              : std::string
    3157            0 : JamiAccount::getDhtProxyServer(const std::string& serverList)
    3158              : {
    3159            0 :     if (proxyServerCached_.empty()) {
    3160            0 :         std::vector<std::string> proxys;
    3161              :         // Split the list of servers
    3162            0 :         std::sregex_iterator begin = {serverList.begin(), serverList.end(), PROXY_REGEX}, end;
    3163            0 :         for (auto it = begin; it != end; ++it) {
    3164            0 :             auto& match = *it;
    3165            0 :             if (match[5].matched and match[6].matched) {
    3166              :                 try {
    3167            0 :                     auto start = std::stoi(match[5]), end = std::stoi(match[6]);
    3168            0 :                     for (auto p = start; p <= end; p++)
    3169            0 :                         proxys.emplace_back(match[1].str() + match[2].str() + ":" + std::to_string(p));
    3170            0 :                 } catch (...) {
    3171            0 :                     JAMI_WARNING("Malformed proxy, ignore it");
    3172            0 :                     continue;
    3173            0 :                 }
    3174              :             } else {
    3175            0 :                 proxys.emplace_back(match[0].str());
    3176              :             }
    3177            0 :         }
    3178            0 :         if (proxys.empty())
    3179            0 :             return {};
    3180              :         // Select one of the list as the current proxy.
    3181            0 :         auto randIt = proxys.begin();
    3182            0 :         std::advance(randIt, std::uniform_int_distribution<unsigned long>(0, proxys.size() - 1)(rand));
    3183            0 :         proxyServerCached_ = *randIt;
    3184              :         // Cache it!
    3185            0 :         dhtnet::fileutils::check_dir(cachePath_, 0700);
    3186            0 :         auto proxyCachePath = cachePath_ / "dhtproxy";
    3187            0 :         std::ofstream file(proxyCachePath);
    3188            0 :         JAMI_DEBUG("Cache DHT proxy server: {}", proxyServerCached_);
    3189            0 :         Json::Value node(Json::objectValue);
    3190            0 :         node[getProxyConfigKey()] = proxyServerCached_;
    3191            0 :         if (file.is_open())
    3192            0 :             file << node;
    3193              :         else
    3194            0 :             JAMI_WARNING("Unable to write into {}", proxyCachePath);
    3195            0 :     }
    3196            0 :     return proxyServerCached_;
    3197              : }
    3198              : 
    3199              : MatchRank
    3200            0 : JamiAccount::matches(std::string_view userName, std::string_view server) const
    3201              : {
    3202            0 :     if (not accountManager_ or not accountManager_->getInfo())
    3203            0 :         return MatchRank::NONE;
    3204              : 
    3205            0 :     if (userName == accountManager_->getInfo()->accountId || server == accountManager_->getInfo()->accountId
    3206            0 :         || userName == accountManager_->getInfo()->deviceId) {
    3207            0 :         JAMI_LOG("Matching account ID in request with username {}", userName);
    3208            0 :         return MatchRank::FULL;
    3209              :     } else {
    3210            0 :         return MatchRank::NONE;
    3211              :     }
    3212              : }
    3213              : 
    3214              : std::string
    3215          341 : JamiAccount::getFromUri() const
    3216              : {
    3217          341 :     std::string uri = "<sip:" + accountManager_->getInfo()->accountId + "@ring.dht>";
    3218          343 :     if (not config().displayName.empty())
    3219          341 :         return "\"" + config().displayName + "\" " + uri;
    3220            0 :     return uri;
    3221          342 : }
    3222              : 
    3223              : std::string
    3224          292 : JamiAccount::getToUri(const std::string& to) const
    3225              : {
    3226          292 :     auto username = to;
    3227          876 :     string_replace(username, "sip:", "");
    3228          584 :     return fmt::format("<sips:{};transport=tls>", username);
    3229          292 : }
    3230              : 
    3231              : std::string
    3232            7 : getDisplayed(const std::string& conversationId, const std::string& messageId)
    3233              : {
    3234              :     // implementing https://tools.ietf.org/rfc/rfc5438.txt
    3235              :     return fmt::format("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
    3236              :                        "<imdn><message-id>{}</message-id>\n"
    3237              :                        "{}"
    3238              :                        "<display-notification><status><displayed/></status></display-notification>\n"
    3239              :                        "</imdn>",
    3240              :                        messageId,
    3241           14 :                        conversationId.empty() ? "" : "<conversation>" + conversationId + "</conversation>");
    3242              : }
    3243              : 
    3244              : std::string
    3245            6 : getPIDF(const std::string& note)
    3246              : {
    3247              :     // implementing https://datatracker.ietf.org/doc/html/rfc3863
    3248              :     return fmt::format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
    3249              :                        "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\">\n"
    3250              :                        "    <tuple>\n"
    3251              :                        "    <status>\n"
    3252              :                        "        <basic>{}</basic>\n"
    3253              :                        "    </status>\n"
    3254              :                        "    </tuple>\n"
    3255              :                        "</presence>",
    3256           12 :                        note);
    3257              : }
    3258              : 
    3259              : void
    3260            5 : JamiAccount::setIsComposing(const std::string& conversationUri, bool isWriting)
    3261              : {
    3262            5 :     Uri uri(conversationUri);
    3263            5 :     std::string conversationId = {};
    3264            5 :     if (uri.scheme() == Uri::Scheme::SWARM) {
    3265            5 :         conversationId = uri.authority();
    3266              :     } else {
    3267            0 :         return;
    3268              :     }
    3269              : 
    3270            5 :     if (auto* cm = convModule(true)) {
    3271            5 :         if (auto typer = cm->getTypers(conversationId)) {
    3272            5 :             if (isWriting)
    3273            4 :                 typer->addTyper(getUsername(), true);
    3274              :             else
    3275            1 :                 typer->removeTyper(getUsername(), true);
    3276            5 :         }
    3277              :     }
    3278            5 : }
    3279              : 
    3280              : bool
    3281            9 : JamiAccount::setMessageDisplayed(const std::string& conversationUri, const std::string& messageId, int status)
    3282              : {
    3283            9 :     Uri uri(conversationUri);
    3284            9 :     std::string conversationId = {};
    3285            9 :     if (uri.scheme() == Uri::Scheme::SWARM)
    3286            9 :         conversationId = uri.authority();
    3287            9 :     auto sendMessage = status == (int) libjami::Account::MessageStates::DISPLAYED && isReadReceiptEnabled();
    3288            9 :     if (!conversationId.empty())
    3289            9 :         sendMessage &= convModule()->onMessageDisplayed(getUsername(), conversationId, messageId);
    3290            9 :     if (sendMessage)
    3291           21 :         sendInstantMessage(uri.authority(), {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}});
    3292            9 :     return true;
    3293           16 : }
    3294              : 
    3295              : std::string
    3296          194 : JamiAccount::getContactHeader(const std::shared_ptr<SipTransport>& sipTransport)
    3297              : {
    3298          194 :     if (sipTransport and sipTransport->get() != nullptr) {
    3299          194 :         auto* transport = sipTransport->get();
    3300          194 :         auto* td = reinterpret_cast<tls::AbstractSIPTransport::TransportData*>(transport);
    3301          194 :         auto address = td->self->getLocalAddress().toString(true);
    3302          194 :         bool reliable = transport->flag & PJSIP_TRANSPORT_RELIABLE;
    3303              :         return fmt::format("\"{}\" <sips:{}{}{};transport={}>",
    3304          194 :                            config().displayName,
    3305          194 :                            id_.second->getId().toString(),
    3306          194 :                            address.empty() ? "" : "@",
    3307              :                            address,
    3308          582 :                            reliable ? "tls" : "dtls");
    3309          194 :     } else {
    3310            0 :         JAMI_ERROR("getContactHeader: no SIP transport provided");
    3311            0 :         return fmt::format("\"{}\" <sips:{}@ring.dht>", config().displayName, id_.second->getId().toString());
    3312              :     }
    3313              : }
    3314              : 
    3315              : void
    3316           48 : JamiAccount::addContact(const std::string& uri, bool confirmed)
    3317              : {
    3318           48 :     dht::InfoHash h(uri);
    3319           48 :     if (not h) {
    3320            1 :         JAMI_ERROR("addContact: invalid contact URI");
    3321            1 :         return;
    3322              :     }
    3323           47 :     auto conversation = convModule()->getOneToOneConversation(uri);
    3324           47 :     if (!confirmed && conversation.empty())
    3325           47 :         conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
    3326           47 :     std::unique_lock lock(configurationMutex_);
    3327           47 :     if (accountManager_)
    3328           47 :         accountManager_->addContact(h, confirmed, conversation);
    3329              :     else
    3330            0 :         JAMI_WARNING("[Account {}] addContact: account not loaded", getAccountID());
    3331           47 : }
    3332              : 
    3333              : void
    3334            8 : JamiAccount::removeContact(const std::string& uri, bool ban)
    3335              : {
    3336            8 :     std::lock_guard lock(configurationMutex_);
    3337            8 :     if (accountManager_)
    3338            8 :         accountManager_->removeContact(uri, ban);
    3339              :     else
    3340            0 :         JAMI_WARNING("[Account {}] removeContact: account not loaded", getAccountID());
    3341            8 : }
    3342              : 
    3343              : std::map<std::string, std::string>
    3344            4 : JamiAccount::getContactDetails(const std::string& uri) const
    3345              : {
    3346            4 :     std::lock_guard lock(configurationMutex_);
    3347            8 :     return accountManager_ ? accountManager_->getContactDetails(uri) : std::map<std::string, std::string> {};
    3348            4 : }
    3349              : 
    3350              : std::optional<Contact>
    3351          601 : JamiAccount::getContactInfo(const std::string& uri) const
    3352              : {
    3353          601 :     std::lock_guard lock(configurationMutex_);
    3354         1202 :     return accountManager_ ? accountManager_->getContactInfo(uri) : std::nullopt;
    3355          601 : }
    3356              : 
    3357              : bool
    3358            0 : JamiAccount::isContact(const std::string& peerAccountUri) const
    3359              : {
    3360            0 :     auto info = getContactInfo(peerAccountUri);
    3361            0 :     return info && info->isActive();
    3362            0 : }
    3363              : 
    3364              : // ----------------------------------------------------------------------------
    3365              : // Service-exposure: peer discovery & tunnel orchestration.
    3366              : // ----------------------------------------------------------------------------
    3367              : 
    3368              : struct JamiAccount::PendingSvcQuery
    3369              : {
    3370              :     uint32_t requestId {0};
    3371              :     std::string peerUri;
    3372              : };
    3373              : 
    3374              : void
    3375            1 : JamiAccount::finalizeSvcQuery(uint32_t requestId, int status, const std::string& servicesJson)
    3376              : {
    3377            1 :     JAMI_LOG("[Account {}] finalizeSvcQuery req={} status={}", getAccountID(), requestId, status);
    3378            1 :     std::shared_ptr<PendingSvcQuery> q;
    3379              :     {
    3380            1 :         std::lock_guard lk(pendingSvcQueriesMtx_);
    3381            1 :         auto it = pendingSvcQueries_.find(requestId);
    3382            1 :         if (it == pendingSvcQueries_.end())
    3383            0 :             return;
    3384            1 :         q = std::move(it->second);
    3385            1 :         pendingSvcQueries_.erase(it);
    3386            1 :     }
    3387            1 :     emitSignal<libjami::ServiceSignal::PeerServicesReceived>(requestId, getAccountID(), q->peerUri, status, servicesJson);
    3388            1 : }
    3389              : 
    3390              : std::string
    3391         1992 : JamiAccount::buildPeerServicesJson(const std::string& peerUri, const DeviceId* forceAvailableDevice)
    3392              : {
    3393         1992 :     std::vector<SvcDiscoveryChannelHandler::CachedSvcInfo> services;
    3394              :     {
    3395         1992 :         std::shared_lock lk(connManagerMtx_);
    3396         1992 :         auto it = channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
    3397         1992 :         if (it == channelHandlers_.end() || !it->second)
    3398           64 :             return {};
    3399         1928 :         services = static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->getCachedServices(peerUri);
    3400         1992 :     }
    3401         1928 :     if (services.empty())
    3402         1918 :         return {};
    3403              : 
    3404           10 :     std::vector<DeviceId> onlineDevices;
    3405           10 :     if (presenceManager_)
    3406           10 :         onlineDevices = presenceManager_->getDevices(peerUri);
    3407              : 
    3408           10 :     Json::Value arr(Json::arrayValue);
    3409           20 :     for (const auto& s : services) {
    3410           10 :         Json::Value v(Json::objectValue);
    3411           10 :         v["id"] = s.info.id;
    3412           10 :         v["name"] = s.info.name;
    3413           10 :         v["description"] = s.info.description;
    3414           10 :         v["proto"] = s.info.proto;
    3415           10 :         v["scheme"] = s.info.scheme;
    3416           10 :         v["preferredPort"] = s.info.preferred_port;
    3417           10 :         v["device"] = s.deviceId.toString();
    3418           19 :         v["available"] = (forceAvailableDevice && *forceAvailableDevice == s.deviceId)
    3419           29 :                          || std::find(onlineDevices.begin(), onlineDevices.end(), s.deviceId) != onlineDevices.end();
    3420           10 :         arr.append(std::move(v));
    3421           10 :     }
    3422           10 :     return json::toString(arr);
    3423         1992 : }
    3424              : 
    3425              : uint32_t
    3426            1 : JamiAccount::queryPeerServices(const std::string& peerUri)
    3427              : {
    3428              :     using PSStatus = libjami::ServiceSignal::PeerServicesStatus;
    3429              : 
    3430              :     static std::atomic<uint32_t> sQueryCounter {0};
    3431            1 :     const auto requestId = ++sQueryCounter;
    3432              : 
    3433            1 :     JAMI_LOG("[Account {}] queryPeerServices req={} peer={}", getAccountID(), requestId, peerUri);
    3434              : 
    3435            1 :     auto state = std::make_shared<PendingSvcQuery>();
    3436            1 :     state->requestId = requestId;
    3437            1 :     state->peerUri = peerUri;
    3438              :     {
    3439            1 :         std::lock_guard lk(pendingSvcQueriesMtx_);
    3440            1 :         pendingSvcQueries_.emplace(requestId, state);
    3441            1 :     }
    3442              : 
    3443            1 :     std::shared_lock lk(connManagerMtx_);
    3444            1 :     auto* handler = static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get());
    3445            1 :     if (!handler) {
    3446            0 :         return 0;
    3447              :     }
    3448              : 
    3449            1 :     runOnMainThread([w = weak(), requestId, peerUri] {
    3450            1 :         auto sthis = w.lock();
    3451            1 :         if (!sthis)
    3452            0 :             return;
    3453            1 :         auto servicesJson = sthis->buildPeerServicesJson(peerUri);
    3454            2 :         sthis->finalizeSvcQuery(requestId,
    3455            1 :                                 static_cast<int>(servicesJson.empty() ? PSStatus::NoDevices : PSStatus::OK),
    3456              :                                 servicesJson);
    3457            1 :     });
    3458            1 :     return requestId;
    3459            1 : }
    3460              : 
    3461              : std::string
    3462            1 : JamiAccount::openServiceTunnel(const std::string& peerUri,
    3463              :                                const std::string& deviceId,
    3464              :                                const std::string& serviceId,
    3465              :                                const std::string& serviceName,
    3466              :                                uint16_t localPort)
    3467              : {
    3468            1 :     auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
    3469            1 :     if (!handler)
    3470            0 :         return {};
    3471            1 :     DeviceId dev;
    3472              :     try {
    3473            1 :         dev = DeviceId(deviceId);
    3474            0 :     } catch (...) {
    3475            0 :         return {};
    3476            0 :     }
    3477            1 :     auto accId = getAccountID();
    3478              :     return handler->openTunnel(
    3479              :         peerUri,
    3480              :         dev,
    3481              :         serviceId,
    3482              :         serviceName,
    3483              :         localPort,
    3484            2 :         [accId](const std::string& tunnelId, uint16_t port) {
    3485            1 :             emitSignal<libjami::ServiceSignal::TunnelOpened>(accId, tunnelId, port);
    3486            1 :         },
    3487            1 :         [accId](const std::string& tunnelId, const std::string& reason) {
    3488            1 :             emitSignal<libjami::ServiceSignal::TunnelClosed>(accId, tunnelId, reason);
    3489            4 :         });
    3490            1 : }
    3491              : 
    3492              : bool
    3493            1 : JamiAccount::closeServiceTunnel(const std::string& tunnelId)
    3494              : {
    3495            1 :     auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
    3496            1 :     if (!handler)
    3497            0 :         return false;
    3498            1 :     return handler->closeTunnel(tunnelId);
    3499              : }
    3500              : 
    3501              : void
    3502            0 : JamiAccount::closeServerTunnelsForService(const std::string& serviceId)
    3503              : {
    3504            0 :     auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
    3505            0 :     if (!handler)
    3506            0 :         return;
    3507            0 :     handler->closeServerChannelsForService(serviceId);
    3508              : }
    3509              : 
    3510              : std::vector<std::map<std::string, std::string>>
    3511            0 : JamiAccount::getActiveServiceTunnels() const
    3512              : {
    3513            0 :     auto it = channelHandlers_.find(Uri::Scheme::SVC_TUNNEL);
    3514            0 :     if (it == channelHandlers_.end())
    3515            0 :         return {};
    3516            0 :     auto* handler = static_cast<SvcTunnelChannelHandler*>(it->second.get());
    3517            0 :     if (!handler)
    3518            0 :         return {};
    3519            0 :     auto tunnels = handler->activeTunnels();
    3520            0 :     std::vector<std::map<std::string, std::string>> out;
    3521            0 :     out.reserve(tunnels.size());
    3522            0 :     for (auto& t : tunnels) {
    3523            0 :         out.push_back({{"id", t.id},
    3524            0 :                        {"peerUri", t.peerUri},
    3525            0 :                        {"deviceId", t.peerDevice},
    3526            0 :                        {"serviceId", t.serviceId},
    3527            0 :                        {"serviceName", t.serviceName},
    3528            0 :                        {"localPort", std::to_string(t.localPort)}});
    3529              :     }
    3530            0 :     return out;
    3531            0 : }
    3532              : 
    3533              : std::vector<std::map<std::string, std::string>>
    3534            1 : JamiAccount::getContacts(bool includeRemoved) const
    3535              : {
    3536            1 :     std::lock_guard lock(configurationMutex_);
    3537            1 :     if (not accountManager_)
    3538            0 :         return {};
    3539            1 :     const auto& contacts = accountManager_->getContacts(includeRemoved);
    3540            1 :     std::vector<std::map<std::string, std::string>> ret;
    3541            1 :     ret.reserve(contacts.size());
    3542            2 :     for (const auto& c : contacts) {
    3543            1 :         auto details = c.second.toMap();
    3544            1 :         if (not details.empty()) {
    3545            3 :             details["id"] = c.first.toString();
    3546            1 :             ret.emplace_back(std::move(details));
    3547              :         }
    3548            1 :     }
    3549            1 :     return ret;
    3550            1 : }
    3551              : 
    3552              : /* trust requests */
    3553              : 
    3554              : std::vector<std::map<std::string, std::string>>
    3555          736 : JamiAccount::getTrustRequests() const
    3556              : {
    3557          736 :     std::lock_guard lock(configurationMutex_);
    3558         1472 :     return accountManager_ ? accountManager_->getTrustRequests() : std::vector<std::map<std::string, std::string>> {};
    3559          736 : }
    3560              : 
    3561              : bool
    3562           21 : JamiAccount::acceptTrustRequest(const std::string& from, bool includeConversation)
    3563              : {
    3564           21 :     dht::InfoHash h(from);
    3565           21 :     if (not h) {
    3566            0 :         JAMI_ERROR("addContact: invalid contact URI");
    3567            0 :         return false;
    3568              :     }
    3569           21 :     std::unique_lock lock(configurationMutex_);
    3570           21 :     if (accountManager_) {
    3571           21 :         if (!accountManager_->acceptTrustRequest(from, includeConversation)) {
    3572              :             // Note: unused for swarm
    3573              :             // Typically the case where the trust request doesn't exists, only incoming DHT messages
    3574            0 :             return accountManager_->addContact(h, true);
    3575              :         }
    3576           21 :         return true;
    3577              :     }
    3578            0 :     JAMI_WARNING("[Account {}] acceptTrustRequest: account not loaded", getAccountID());
    3579            0 :     return false;
    3580           21 : }
    3581              : 
    3582              : bool
    3583            0 : JamiAccount::discardTrustRequest(const std::string& from)
    3584              : {
    3585              :     // Remove 1:1 generated conv requests
    3586            0 :     auto requests = getTrustRequests();
    3587            0 :     for (const auto& req : requests) {
    3588            0 :         if (req.at(libjami::Account::TrustRequest::FROM) == from) {
    3589            0 :             convModule()->declineConversationRequest(req.at(libjami::Account::TrustRequest::CONVERSATIONID));
    3590              :         }
    3591              :     }
    3592              : 
    3593              :     // Remove trust request
    3594            0 :     std::lock_guard lock(configurationMutex_);
    3595            0 :     if (accountManager_)
    3596            0 :         return accountManager_->discardTrustRequest(from);
    3597            0 :     JAMI_WARNING("[Account {:s}] discardTrustRequest: account not loaded", getAccountID());
    3598            0 :     return false;
    3599            0 : }
    3600              : 
    3601              : void
    3602            1 : JamiAccount::declineConversationRequest(const std::string& conversationId)
    3603              : {
    3604            1 :     auto peerId = convModule()->peerFromConversationRequest(conversationId);
    3605            1 :     convModule()->declineConversationRequest(conversationId);
    3606            1 :     if (!peerId.empty()) {
    3607            1 :         std::lock_guard lock(configurationMutex_);
    3608            1 :         if (const auto* info = accountManager_->getInfo()) {
    3609              :             // Verify if we have a trust request with this peer + convId
    3610            1 :             auto req = info->contacts->getTrustRequest(dht::InfoHash(peerId));
    3611            3 :             if (req.find(libjami::Account::TrustRequest::CONVERSATIONID) != req.end()
    3612            2 :                 && req.at(libjami::Account::TrustRequest::CONVERSATIONID) == conversationId) {
    3613            0 :                 accountManager_->discardTrustRequest(peerId);
    3614            0 :                 JAMI_DEBUG("[Account {:s}] Declined trust request with {:s}", getAccountID(), peerId);
    3615              :             }
    3616            1 :         }
    3617            1 :     }
    3618            1 : }
    3619              : 
    3620              : void
    3621           40 : JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
    3622              : {
    3623           40 :     dht::InfoHash h(to);
    3624           40 :     if (not h) {
    3625            0 :         JAMI_ERROR("addContact: invalid contact URI");
    3626            0 :         return;
    3627              :     }
    3628              : 
    3629              :     // The invite timestamp is now a native field on the trust request itself (see
    3630              :     // jami::TrustRequestMsg), no need to stamp it into the vCard payload anymore.
    3631           40 :     auto invited = nowMs();
    3632              : 
    3633              :     // Cache the payload (and the invite timestamp alongside it) so retries reuse the same
    3634              :     // timestamp instead of being treated as a new re-invite.
    3635           40 :     auto requestPath = cachePath_ / "requests";
    3636           40 :     dhtnet::fileutils::recursive_mkdir(requestPath, 0700);
    3637           40 :     auto cachedFile = requestPath / to;
    3638           40 :     std::ofstream req(cachedFile, std::ios::trunc | std::ios::binary);
    3639           40 :     if (!req.is_open()) {
    3640            0 :         JAMI_ERROR("Unable to write data to {}", cachedFile);
    3641            0 :         return;
    3642              :     }
    3643              : 
    3644           40 :     if (not payload.empty()) {
    3645            1 :         req.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
    3646              :     }
    3647           40 :     req.close();
    3648              : 
    3649           40 :     auto cachedTsFile = requestPath / (to + ".ts");
    3650           40 :     std::ofstream tsFile(cachedTsFile, std::ios::trunc | std::ios::binary);
    3651           40 :     if (tsFile.is_open())
    3652           40 :         tsFile << toMillisecondsSinceEpoch(invited);
    3653              :     else
    3654            0 :         JAMI_WARNING("Unable to write invite timestamp cache to {}", cachedTsFile);
    3655              : 
    3656           40 :     if (payload.size() >= 64000) {
    3657            0 :         JAMI_WARNING("Trust request is too big. Remove payload");
    3658              :     }
    3659              : 
    3660           40 :     auto conversation = convModule()->getOneToOneConversation(to);
    3661           40 :     if (conversation.empty())
    3662            0 :         conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
    3663           40 :     if (not conversation.empty()) {
    3664           40 :         std::lock_guard lock(configurationMutex_);
    3665           40 :         if (accountManager_)
    3666           80 :             accountManager_->sendTrustRequest(to,
    3667              :                                               conversation,
    3668           80 :                                               payload.size() >= 64000 ? std::vector<uint8_t> {} : payload,
    3669              :                                               invited);
    3670              :         else
    3671            0 :             JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
    3672           40 :     } else
    3673            0 :         JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
    3674           40 : }
    3675              : 
    3676              : void
    3677            0 : JamiAccount::forEachDevice(const dht::InfoHash& to,
    3678              :                            std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)>&& op,
    3679              :                            std::function<void(bool)>&& end)
    3680              : {
    3681            0 :     accountManager_->forEachDevice(to, std::move(op), std::move(end));
    3682            0 : }
    3683              : 
    3684              : uint64_t
    3685        13346 : JamiAccount::sendTextMessage(const std::string& to,
    3686              :                              const std::string& deviceId,
    3687              :                              const std::map<std::string, std::string>& payloads,
    3688              :                              uint64_t refreshToken,
    3689              :                              bool onlyConnected)
    3690              : {
    3691        13346 :     Uri uri(to);
    3692        13325 :     if (uri.scheme() == Uri::Scheme::SWARM) {
    3693            0 :         sendInstantMessage(uri.authority(), payloads);
    3694            0 :         return 0;
    3695              :     }
    3696              : 
    3697        13331 :     std::string toUri;
    3698              :     try {
    3699        13327 :         toUri = parseJamiUri(to);
    3700            0 :     } catch (...) {
    3701            0 :         JAMI_ERROR("Failed to send a text message due to an invalid URI {}", to);
    3702            0 :         return 0;
    3703            0 :     }
    3704        13308 :     if (payloads.size() != 1) {
    3705            0 :         JAMI_ERROR("Multi-part im is not supported yet by JamiAccount");
    3706            0 :         return 0;
    3707              :     }
    3708        13347 :     return SIPAccountBase::sendTextMessage(toUri, deviceId, payloads, refreshToken, onlyConnected);
    3709        13347 : }
    3710              : 
    3711              : void
    3712        14341 : JamiAccount::sendMessage(const std::string& to,
    3713              :                          const std::string& deviceId,
    3714              :                          const std::map<std::string, std::string>& payloads,
    3715              :                          uint64_t token,
    3716              :                          bool retryOnTimeout,
    3717              :                          bool onlyConnected)
    3718              : {
    3719        14341 :     std::string toUri;
    3720              :     try {
    3721        14341 :         toUri = parseJamiUri(to);
    3722            0 :     } catch (...) {
    3723            0 :         JAMI_ERROR("[Account {}] Failed to send a text message due to an invalid URI {}", getAccountID(), to);
    3724            0 :         if (!onlyConnected)
    3725            0 :             messageEngine_.onMessageSent(to, token, false, deviceId);
    3726            0 :         return;
    3727            0 :     }
    3728        14341 :     if (payloads.size() != 1) {
    3729            0 :         JAMI_ERROR("Multi-part im is not supported");
    3730            0 :         if (!onlyConnected)
    3731            0 :             messageEngine_.onMessageSent(toUri, token, false, deviceId);
    3732            0 :         return;
    3733              :     }
    3734              : 
    3735              :     // Use the Message channel if available
    3736        14341 :     std::shared_lock clk(connManagerMtx_);
    3737        14341 :     auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
    3738        14341 :     if (!handler) {
    3739           27 :         clk.unlock();
    3740           27 :         if (!onlyConnected)
    3741           27 :             messageEngine_.onMessageSent(to, token, false, deviceId);
    3742           27 :         return;
    3743              :     }
    3744              : 
    3745              :     auto devices = std::make_shared<SendMessageContext>(
    3746        28628 :         [w = weak(), to, token, deviceId, onlyConnected, retryOnTimeout](bool success, bool sent) {
    3747        14311 :             if (auto acc = w.lock())
    3748        14311 :                 acc->onMessageSent(to, token, deviceId, success, onlyConnected, sent && retryOnTimeout);
    3749        28628 :         });
    3750              : 
    3751        12404 :     auto completed = [w = weak(), to, devices](const DeviceId& device,
    3752              :                                                const std::shared_ptr<dhtnet::ChannelSocket>& conn,
    3753              :                                                bool success) {
    3754        12404 :         if (!success)
    3755            4 :             if (auto acc = w.lock()) {
    3756            4 :                 std::shared_lock clk(acc->connManagerMtx_);
    3757            4 :                 if (auto* handler = static_cast<MessageChannelHandler*>(
    3758            4 :                         acc->channelHandlers_[Uri::Scheme::MESSAGE].get())) {
    3759            4 :                     handler->closeChannel(to, device, conn);
    3760              :                 }
    3761            8 :             }
    3762        12404 :         devices->complete(device, success);
    3763        26716 :     };
    3764              : 
    3765        14314 :     const auto& payload = *payloads.begin();
    3766        14314 :     auto msg = std::make_shared<MessageChannelHandler::Message>();
    3767        14314 :     msg->id = token;
    3768        14314 :     msg->t = payload.first;
    3769        14313 :     msg->c = payload.second;
    3770        14314 :     auto device = deviceId.empty() ? DeviceId() : DeviceId(deviceId);
    3771        14314 :     if (deviceId.empty()) {
    3772         3094 :         auto conns = handler->getChannels(toUri);
    3773         3094 :         clk.unlock();
    3774         5241 :         for (const auto& conn : conns) {
    3775         2147 :             auto connDevice = conn->deviceId();
    3776         2147 :             if (!devices->add(connDevice))
    3777          954 :                 continue;
    3778         1192 :             dht::ThreadPool::io().run([completed, connDevice, conn, msg] {
    3779         1193 :                 completed(connDevice, conn, MessageChannelHandler::sendMessage(conn, *msg));
    3780         1193 :             });
    3781              :         }
    3782         3093 :     } else {
    3783        11220 :         if (auto conn = handler->getChannel(toUri, device)) {
    3784        11211 :             clk.unlock();
    3785        11211 :             devices->add(device);
    3786        11211 :             dht::ThreadPool::io().run([completed, device, conn, msg] {
    3787        11211 :                 completed(device, conn, MessageChannelHandler::sendMessage(conn, *msg));
    3788        11209 :             });
    3789        11211 :             devices->start();
    3790        11211 :             return;
    3791        11220 :         }
    3792              :     }
    3793         3103 :     if (clk)
    3794            9 :         clk.unlock();
    3795              : 
    3796         3103 :     devices->start();
    3797              : 
    3798         3103 :     if (onlyConnected)
    3799           22 :         return;
    3800              :     // We are unable to send the message directly, try connecting
    3801              : 
    3802              :     // Get conversation id, which will be used by the iOS notification extension
    3803              :     // to load the conversation.
    3804         2460 :     auto extractIdFromJson = [](const std::string& jsonData) -> std::string {
    3805         2460 :         Json::Value parsed;
    3806         2460 :         if (json::parse(jsonData, parsed)) {
    3807         2460 :             auto value = parsed.get("id", Json::nullValue);
    3808         2460 :             if (value && value.isString()) {
    3809         2460 :                 return value.asString();
    3810              :             }
    3811         2460 :         } else {
    3812            0 :             JAMI_WARNING("Unable to parse jsonData to get conversation ID");
    3813              :         }
    3814            0 :         return "";
    3815         2460 :     };
    3816              : 
    3817              :     // get request type
    3818         3081 :     auto payload_type = msg->t;
    3819         3081 :     if (payload_type == MIME_TYPE_GIT) {
    3820         2460 :         std::string id = extractIdFromJson(msg->c);
    3821         2460 :         if (!id.empty()) {
    3822         2460 :             payload_type += "/" + id;
    3823              :         }
    3824         2460 :     }
    3825              : 
    3826         3081 :     if (deviceId.empty()) {
    3827         3072 :         auto toH = dht::InfoHash(toUri);
    3828              :         // Find listening devices for this account
    3829         9215 :         accountManager_->forEachDevice(toH,
    3830         6144 :                                        [this, to, devices, payload_type, currentDevice = DeviceId(currentDeviceId())](
    3831              :                                            const std::shared_ptr<dht::crypto::PublicKey>& dev) {
    3832              :                                            // Test if already sent
    3833         3122 :                                            auto deviceId = dev->getLongId();
    3834         3122 :                                            if (deviceId == currentDevice || devices->pending(deviceId)) {
    3835          658 :                                                return;
    3836              :                                            }
    3837              : 
    3838              :                                            // Else, ask for a channel to send the message
    3839         2464 :                                            dht::ThreadPool::io().run([this, to, deviceId, payload_type]() {
    3840         2464 :                                                requestMessageConnection(to, deviceId, payload_type);
    3841         2464 :                                            });
    3842              :                                        });
    3843              :     } else {
    3844            9 :         requestMessageConnection(to, device, payload_type);
    3845              :     }
    3846        59300 : }
    3847              : 
    3848              : void
    3849        14311 : JamiAccount::onMessageSent(
    3850              :     const std::string& to, uint64_t id, const std::string& deviceId, bool success, bool onlyConnected, bool retry)
    3851              : {
    3852        14311 :     if (!onlyConnected)
    3853        14283 :         messageEngine_.onMessageSent(to, id, success, deviceId);
    3854              : 
    3855        14313 :     if (!success) {
    3856         1915 :         if (retry)
    3857            4 :             messageEngine_.onPeerOnline(to, deviceId);
    3858              :     }
    3859        14313 : }
    3860              : 
    3861              : dhtnet::IceTransportOptions
    3862          129 : JamiAccount::getIceOptions() const
    3863              : {
    3864          129 :     return connectionManager_->getIceOptions();
    3865              : }
    3866              : 
    3867              : void
    3868           11 : JamiAccount::getIceOptions(std::function<void(dhtnet::IceTransportOptions&&)> cb) const
    3869              : {
    3870           11 :     return connectionManager_->getIceOptions(std::move(cb));
    3871              : }
    3872              : 
    3873              : dhtnet::IpAddr
    3874          108 : JamiAccount::getPublishedIpAddress(uint16_t family) const
    3875              : {
    3876          108 :     return connectionManager_->getPublishedIpAddress(family);
    3877              : }
    3878              : 
    3879              : bool
    3880            0 : JamiAccount::setPushNotificationToken(const std::string& token)
    3881              : {
    3882            0 :     if (SIPAccountBase::setPushNotificationToken(token)) {
    3883            0 :         JAMI_WARNING("[Account {:s}] setPushNotificationToken: {:s}", getAccountID(), token);
    3884            0 :         if (dht_)
    3885            0 :             dht_->setPushNotificationToken(token);
    3886            0 :         return true;
    3887              :     }
    3888            0 :     return false;
    3889              : }
    3890              : 
    3891              : bool
    3892            0 : JamiAccount::setPushNotificationTopic(const std::string& topic)
    3893              : {
    3894            0 :     if (SIPAccountBase::setPushNotificationTopic(topic)) {
    3895            0 :         if (dht_)
    3896            0 :             dht_->setPushNotificationTopic(topic);
    3897            0 :         return true;
    3898              :     }
    3899            0 :     return false;
    3900              : }
    3901              : 
    3902              : bool
    3903            0 : JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data)
    3904              : {
    3905            0 :     if (SIPAccountBase::setPushNotificationConfig(data)) {
    3906            0 :         if (dht_) {
    3907            0 :             dht_->setPushNotificationPlatform(config_->platform);
    3908            0 :             dht_->setPushNotificationTopic(config_->notificationTopic);
    3909            0 :             dht_->setPushNotificationToken(config_->deviceKey);
    3910              :         }
    3911            0 :         return true;
    3912              :     }
    3913            0 :     return false;
    3914              : }
    3915              : 
    3916              : /**
    3917              :  * To be called by clients with relevant data when a push notification is received.
    3918              :  */
    3919              : void
    3920            0 : JamiAccount::pushNotificationReceived(const std::string& /*from*/, const std::map<std::string, std::string>& data)
    3921              : {
    3922            0 :     auto ret_future = dht_->pushNotificationReceived(data);
    3923            0 :     dht::ThreadPool::computation().run([id = getAccountID(), ret_future = ret_future.share()] {
    3924            0 :         JAMI_WARNING("[Account {:s}] pushNotificationReceived: {}", id, (uint8_t) ret_future.get());
    3925            0 :     });
    3926            0 : }
    3927              : 
    3928              : std::string
    3929            9 : JamiAccount::getUserUri() const
    3930              : {
    3931            9 :     if (not registeredName_.empty())
    3932            0 :         return JAMI_URI_PREFIX + registeredName_;
    3933            9 :     return JAMI_URI_PREFIX + config().username;
    3934              : }
    3935              : 
    3936              : std::vector<libjami::Message>
    3937            0 : JamiAccount::getLastMessages(const uint64_t& base_timestamp)
    3938              : {
    3939            0 :     return SIPAccountBase::getLastMessages(base_timestamp);
    3940              : }
    3941              : 
    3942              : void
    3943            0 : JamiAccount::startAccountPublish()
    3944              : {
    3945            0 :     AccountPeerInfo info_pub;
    3946            0 :     info_pub.accountId = dht::InfoHash(accountManager_->getInfo()->accountId);
    3947            0 :     info_pub.displayName = config().displayName;
    3948            0 :     peerDiscovery_->startPublish<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, info_pub);
    3949            0 : }
    3950              : 
    3951              : void
    3952            0 : JamiAccount::startAccountDiscovery()
    3953              : {
    3954            0 :     auto id = dht::InfoHash(accountManager_->getInfo()->accountId);
    3955            0 :     peerDiscovery_
    3956            0 :         ->startDiscovery<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, [this, id](AccountPeerInfo&& v, dht::SockAddr&&) {
    3957            0 :             std::lock_guard lc(discoveryMapMtx_);
    3958              :             // Make sure that account itself will not be recorded
    3959            0 :             if (v.accountId != id) {
    3960              :                 // Create or find the old one
    3961            0 :                 auto& dp = discoveredPeers_[v.accountId];
    3962            0 :                 dp.displayName = v.displayName;
    3963            0 :                 discoveredPeerMap_[v.accountId.toString()] = v.displayName;
    3964            0 :                 if (!dp.cleanupTimer) {
    3965              :                     // Avoid repeat reception of same peer
    3966            0 :                     JAMI_LOG("Account discovered: {}: {}", v.displayName, v.accountId.to_c_str());
    3967              :                     // Send Added Peer and corrsponding accoundID
    3968            0 :                     emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(getAccountID(),
    3969            0 :                                                                                 v.accountId.toString(),
    3970              :                                                                                 0,
    3971            0 :                                                                                 v.displayName);
    3972            0 :                     dp.cleanupTimer = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext(),
    3973            0 :                                                                            PEER_DISCOVERY_EXPIRATION);
    3974              :                 }
    3975            0 :                 dp.cleanupTimer->expires_after(PEER_DISCOVERY_EXPIRATION);
    3976            0 :                 dp.cleanupTimer->async_wait(
    3977            0 :                     [w = weak(), p = v.accountId, a = v.displayName](const asio::error_code& ec) {
    3978            0 :                         if (ec)
    3979            0 :                             return;
    3980            0 :                         if (auto this_ = w.lock()) {
    3981              :                             {
    3982            0 :                                 std::lock_guard lc(this_->discoveryMapMtx_);
    3983            0 :                                 this_->discoveredPeers_.erase(p);
    3984            0 :                                 this_->discoveredPeerMap_.erase(p.toString());
    3985            0 :                             }
    3986              :                             // Send deleted peer
    3987            0 :                             emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(this_->getAccountID(),
    3988            0 :                                                                                         p.toString(),
    3989              :                                                                                         1,
    3990            0 :                                                                                         a);
    3991            0 :                         }
    3992            0 :                         JAMI_LOG("Account removed from discovery list: {}", a);
    3993              :                     });
    3994              :             }
    3995            0 :         });
    3996            0 : }
    3997              : 
    3998              : std::map<std::string, std::string>
    3999            0 : JamiAccount::getNearbyPeers() const
    4000              : {
    4001            0 :     return discoveredPeerMap_;
    4002              : }
    4003              : 
    4004              : void
    4005            0 : JamiAccount::sendProfileToPeers()
    4006              : {
    4007            0 :     if (!connectionManager_)
    4008            0 :         return;
    4009            0 :     std::set<std::string> peers;
    4010            0 :     const auto& accountUri = accountManager_->getInfo()->accountId;
    4011              :     // TODO: avoid using getConnectionList
    4012            0 :     for (const auto& connection : connectionManager_->getConnectionList()) {
    4013            0 :         const auto& device = connection.at("device");
    4014            0 :         const auto& peer = connection.at("peer");
    4015            0 :         if (!peers.emplace(peer).second)
    4016            0 :             continue;
    4017            0 :         if (peer == accountUri) {
    4018            0 :             sendProfile("", accountUri, device);
    4019            0 :             continue;
    4020              :         }
    4021            0 :         const auto& conversationId = convModule()->getOneToOneConversation(peer);
    4022            0 :         if (!conversationId.empty()) {
    4023            0 :             sendProfile(conversationId, peer, device);
    4024              :         }
    4025            0 :     }
    4026            0 : }
    4027              : 
    4028              : void
    4029            0 : JamiAccount::updateProfile(const std::string& displayName,
    4030              :                            const std::string& avatar,
    4031              :                            const std::string& fileType,
    4032              :                            const std::string& botOwner,
    4033              :                            int32_t flag)
    4034              : {
    4035              :     // if the fileType is empty then only the display name will be upated
    4036              : 
    4037            0 :     const auto& accountUri = accountManager_->getInfo()->accountId;
    4038            0 :     const auto& path = profilePath();
    4039            0 :     const auto& profiles = idPath_ / "profiles";
    4040              : 
    4041              :     try {
    4042            0 :         if (!std::filesystem::exists(profiles)) {
    4043            0 :             std::filesystem::create_directories(profiles);
    4044              :         }
    4045            0 :     } catch (const std::exception& e) {
    4046            0 :         JAMI_ERROR("Failed to create profiles directory: {}", e.what());
    4047            0 :         return;
    4048            0 :     }
    4049              : 
    4050            0 :     const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(accountUri));
    4051              : 
    4052            0 :     auto profile = getProfileVcard();
    4053            0 :     if (profile.empty()) {
    4054            0 :         profile = vCard::utils::initVcard();
    4055              :     }
    4056              : 
    4057            0 :     profile[std::string(vCard::Property::FORMATTED_NAME)] = displayName;
    4058            0 :     editConfig([&](JamiAccountConfig& config) { config.displayName = displayName; });
    4059            0 :     emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
    4060              : 
    4061            0 :     if (!fileType.empty()) {
    4062            0 :         const std::string& key = "PHOTO;ENCODING=BASE64;TYPE=" + fileType;
    4063            0 :         if (flag == 0) {
    4064            0 :             vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
    4065            0 :             const auto& avatarPath = std::filesystem::path(avatar);
    4066            0 :             if (std::filesystem::exists(avatarPath)) {
    4067              :                 try {
    4068            0 :                     profile[key] = base64::encode(fileutils::loadFile(avatarPath));
    4069            0 :                 } catch (const std::exception& e) {
    4070            0 :                     JAMI_ERROR("Failed to load avatar: {}", e.what());
    4071            0 :                 }
    4072              :             }
    4073            0 :         } else if (flag == 1) {
    4074            0 :             vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
    4075            0 :             profile[key] = avatar;
    4076              :         }
    4077            0 :     }
    4078            0 :     if (flag == 2) {
    4079            0 :         vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
    4080              :     }
    4081            0 :     if (!botOwner.empty()) {
    4082              :         // See RFC 6473
    4083            0 :         profile[std::string(vCard::Property::KIND)] = "application";
    4084              :         // See RFC 6350
    4085            0 :         profile[std::string(vCard::Property::RELATED_OWNER)] = botOwner;
    4086              :     }
    4087              :     try {
    4088            0 :         vCard::utils::save(profile, vCardPath, path);
    4089            0 :         emitSignal<libjami::ConfigurationSignal::ProfileReceived>(getAccountID(), accountUri, path.string());
    4090              : 
    4091              :         // Delete all profile sent markers:
    4092            0 :         std::error_code ec;
    4093            0 :         std::filesystem::remove_all(cachePath_ / "vcard", ec);
    4094            0 :         sendProfileToPeers();
    4095            0 :     } catch (const std::exception& e) {
    4096            0 :         JAMI_ERROR("Error writing profile: {}", e.what());
    4097            0 :     }
    4098            0 : }
    4099              : 
    4100              : void
    4101          840 : JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
    4102              : {
    4103          840 :     Account::setActiveCodecs(list);
    4104          840 :     if (!hasActiveCodec(MEDIA_AUDIO))
    4105          818 :         setCodecActive(AV_CODEC_ID_OPUS);
    4106          840 :     if (!hasActiveCodec(MEDIA_VIDEO)) {
    4107          818 :         setCodecActive(AV_CODEC_ID_HEVC);
    4108          818 :         setCodecActive(AV_CODEC_ID_H264);
    4109          818 :         setCodecActive(AV_CODEC_ID_VP8);
    4110              :     }
    4111          840 :     config_->activeCodecs = getActiveCodecs(MEDIA_ALL);
    4112          840 : }
    4113              : 
    4114              : void
    4115           11 : JamiAccount::sendInstantMessage(const std::string& convId, const std::map<std::string, std::string>& msg)
    4116              : {
    4117           11 :     auto members = convModule()->getConversationMembers(convId);
    4118           11 :     if (convId.empty() && members.empty()) {
    4119              :         // TODO remove, it's for old API for contacts
    4120            0 :         sendTextMessage(convId, "", msg);
    4121            0 :         return;
    4122              :     }
    4123           33 :     for (const auto& m : members) {
    4124           22 :         const auto& uri = m.at("uri");
    4125           22 :         auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
    4126              :         // Announce to all members that a new message is sent
    4127           66 :         sendMessage(uri, "", msg, token, false, true);
    4128              :     }
    4129           11 : }
    4130              : 
    4131              : bool
    4132        12394 : JamiAccount::handleMessage(const std::shared_ptr<dht::crypto::Certificate>& cert,
    4133              :                            const std::string& from,
    4134              :                            const std::pair<std::string, std::string>& m)
    4135              : {
    4136        12394 :     if (not cert or not cert->issuer)
    4137            0 :         return true; // stop processing message
    4138              : 
    4139        12393 :     if (cert->issuer->getId().to_view() != from) {
    4140            0 :         JAMI_WARNING("[Account {}] [device {}] handleMessage: invalid author {}",
    4141              :                      getAccountID(),
    4142              :                      cert->issuer->getId().to_view(),
    4143              :                      from);
    4144            0 :         return true;
    4145              :     }
    4146        12396 :     if (m.first == MIME_TYPE_GIT) {
    4147        11933 :         Json::Value json;
    4148        11933 :         if (!json::parse(m.second, json)) {
    4149            0 :             return true;
    4150              :         }
    4151              : 
    4152              :         // fetchNewCommits will do heavy stuff like fetching, avoid to block SIP socket
    4153        23864 :         dht::ThreadPool::io().run([w = weak(),
    4154              :                                    from,
    4155        11929 :                                    deviceId = json["deviceId"].asString(),
    4156        11930 :                                    id = json["id"].asString(),
    4157        11931 :                                    commit = json["commit"].asString()] {
    4158        11933 :             if (auto shared = w.lock()) {
    4159        11931 :                 if (auto* cm = shared->convModule())
    4160        11931 :                     cm->fetchNewCommits(from, deviceId, id, commit);
    4161        11930 :             }
    4162        11927 :         });
    4163        11931 :         return true;
    4164        12394 :     } else if (m.first == MIME_TYPE_INVITE) {
    4165          149 :         convModule()->onNeedConversationRequest(from, m.second);
    4166          149 :         return true;
    4167          314 :     } else if (m.first == MIME_TYPE_INVITE_JSON) {
    4168          295 :         Json::Value json;
    4169          295 :         if (!json::parse(m.second, json)) {
    4170            0 :             return true;
    4171              :         }
    4172          295 :         convModule()->onConversationRequest(from, json);
    4173          295 :         return true;
    4174          314 :     } else if (m.first == MIME_TYPE_IM_COMPOSING) {
    4175              :         try {
    4176            4 :             static const std::regex COMPOSING_REGEX("<state>\\s*(\\w+)\\s*<\\/state>");
    4177            4 :             std::smatch matched_pattern;
    4178            4 :             std::regex_search(m.second, matched_pattern, COMPOSING_REGEX);
    4179            4 :             bool isComposing {false};
    4180            4 :             if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4181            4 :                 isComposing = matched_pattern[1] == "active";
    4182              :             }
    4183            4 :             static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
    4184            4 :             std::regex_search(m.second, matched_pattern, CONVID_REGEX);
    4185            4 :             std::string conversationId = "";
    4186            4 :             if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4187            4 :                 conversationId = matched_pattern[1];
    4188              :             }
    4189            4 :             if (!conversationId.empty()) {
    4190            4 :                 if (auto* cm = convModule(true)) {
    4191            4 :                     if (auto typer = cm->getTypers(conversationId)) {
    4192            4 :                         if (isComposing)
    4193            3 :                             typer->addTyper(from);
    4194              :                         else
    4195            1 :                             typer->removeTyper(from);
    4196            4 :                     }
    4197              :                 }
    4198              :             }
    4199            4 :             return true;
    4200            4 :         } catch (const std::exception& e) {
    4201            0 :             JAMI_WARNING("Error parsing composing state: {}", e.what());
    4202            0 :         }
    4203           15 :     } else if (m.first == MIME_TYPE_IMDN) {
    4204              :         try {
    4205            9 :             static const std::regex IMDN_MSG_ID_REGEX("<message-id>\\s*(\\w+)\\s*<\\/message-id>");
    4206            9 :             std::smatch matched_pattern;
    4207              : 
    4208            9 :             std::regex_search(m.second, matched_pattern, IMDN_MSG_ID_REGEX);
    4209            9 :             std::string messageId;
    4210            9 :             if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4211            9 :                 messageId = matched_pattern[1];
    4212              :             } else {
    4213            0 :                 JAMI_WARNING("Message displayed: unable to parse message ID");
    4214            0 :                 return true;
    4215              :             }
    4216              : 
    4217            9 :             static const std::regex STATUS_REGEX("<status>\\s*<(\\w+)\\/>\\s*<\\/status>");
    4218            9 :             std::regex_search(m.second, matched_pattern, STATUS_REGEX);
    4219            9 :             bool isDisplayed {false};
    4220            9 :             if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4221            9 :                 isDisplayed = matched_pattern[1] == "displayed";
    4222              :             } else {
    4223            0 :                 JAMI_WARNING("Message displayed: unable to parse status");
    4224            0 :                 return true;
    4225              :             }
    4226              : 
    4227            9 :             static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
    4228            9 :             std::regex_search(m.second, matched_pattern, CONVID_REGEX);
    4229            9 :             std::string conversationId = "";
    4230            9 :             if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4231            9 :                 conversationId = matched_pattern[1];
    4232              :             }
    4233              : 
    4234            9 :             if (!isReadReceiptEnabled())
    4235            0 :                 return true;
    4236            9 :             if (isDisplayed) {
    4237            9 :                 if (convModule()->onMessageDisplayed(from, conversationId, messageId)) {
    4238            8 :                     JAMI_DEBUG("[message {}] Displayed by peer", messageId);
    4239           16 :                     emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(
    4240            8 :                         accountID_,
    4241              :                         conversationId,
    4242              :                         from,
    4243              :                         messageId,
    4244              :                         static_cast<int>(libjami::Account::MessageStates::DISPLAYED));
    4245              :                 }
    4246              :             }
    4247            9 :             return true;
    4248            9 :         } catch (const std::exception& e) {
    4249            0 :             JAMI_ERROR("Error parsing display notification: {}", e.what());
    4250            0 :         }
    4251            6 :     } else if (m.first == MIME_TYPE_PIDF) {
    4252            6 :         std::smatch matched_pattern;
    4253            6 :         static const std::regex BASIC_REGEX("<basic>([\\w\\s]+)<\\/basic>");
    4254            6 :         std::regex_search(m.second, matched_pattern, BASIC_REGEX);
    4255            6 :         std::string customStatus {};
    4256            6 :         if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
    4257            6 :             customStatus = matched_pattern[1];
    4258            6 :             emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
    4259              :                                                                       from,
    4260              :                                                                       static_cast<int>(PresenceState::CONNECTED),
    4261              :                                                                       customStatus);
    4262              :         } else {
    4263            0 :             JAMI_WARNING("Presence: unable to parse status");
    4264              :         }
    4265            6 :         return true;
    4266            6 :     }
    4267              : 
    4268            0 :     return false;
    4269              : }
    4270              : 
    4271              : void
    4272          277 : JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy)
    4273              : {
    4274          277 :     std::function<void(const DeviceId&, bool)> cb;
    4275              :     {
    4276          277 :         std::lock_guard lk(onConnectionClosedMtx_);
    4277          277 :         auto it = onConnectionClosed_.find(deviceId);
    4278          277 :         if (it != onConnectionClosed_.end()) {
    4279           89 :             if (eraseDummy) {
    4280           89 :                 cb = std::move(it->second);
    4281           89 :                 onConnectionClosed_.erase(it);
    4282              :             } else {
    4283              :                 // In this case a new subcall is created and the callback
    4284              :                 // will be re-called once with eraseDummy = true
    4285            0 :                 cb = it->second;
    4286              :             }
    4287              :         }
    4288          277 :     }
    4289          277 :     dht::ThreadPool::io().run([w = weak(), cb = std::move(cb), id = deviceId, erase = std::move(eraseDummy)] {
    4290          277 :         if (auto acc = w.lock()) {
    4291          277 :             if (cb)
    4292           89 :                 cb(id, erase);
    4293          277 :         }
    4294          277 :     });
    4295          277 : }
    4296              : 
    4297              : void
    4298         4849 : JamiAccount::requestMessageConnection(const std::string& peerId,
    4299              :                                       const DeviceId& deviceId,
    4300              :                                       const std::string& connectionType)
    4301              : {
    4302         4849 :     std::shared_lock lk(connManagerMtx_);
    4303         4850 :     auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
    4304         4849 :     if (!handler)
    4305            4 :         return;
    4306         4845 :     if (deviceId) {
    4307         4846 :         if (auto connected = handler->getChannel(peerId, deviceId)) {
    4308         1228 :             return;
    4309         4845 :         }
    4310              :     } else {
    4311            0 :         auto connected = handler->getChannels(peerId);
    4312            0 :         if (!connected.empty()) {
    4313            0 :             return;
    4314              :         }
    4315            0 :     }
    4316              :     // Nothing to do on completion: the work that has to happen once the device is
    4317              :     // reachable is driven by onMessageChannelReady().
    4318        10851 :     handler->connect(deviceId, "", [](const std::shared_ptr<dhtnet::ChannelSocket>&, const DeviceId&) {}, connectionType);
    4319         4850 : }
    4320              : 
    4321              : void
    4322         1256 : JamiAccount::onMessageChannelReady(const std::string& peerId, const DeviceId& deviceId)
    4323              : {
    4324         1256 :     messageEngine_.onPeerOnline(peerId);
    4325         1259 :     messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
    4326         1256 :     if (!presenceNote_.empty()) {
    4327              :         // If a presence note is set, send it to this device.
    4328            3 :         auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
    4329            9 :         std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
    4330            3 :         sendMessage(peerId, deviceId.toString(), msg, token, false, true);
    4331            3 :     }
    4332         1257 :     convModule()->syncConversations(peerId, deviceId.toString());
    4333         1261 : }
    4334              : 
    4335              : void
    4336           95 : JamiAccount::requestSIPConnection(const std::string& peerId,
    4337              :                                   const DeviceId& deviceId,
    4338              :                                   const std::string& connectionType,
    4339              :                                   bool forceNewConnection,
    4340              :                                   const std::shared_ptr<SIPCall>& pc)
    4341              : {
    4342           95 :     JAMI_LOG("[Account {}] Request SIP connection to peer {} on device {}", getAccountID(), peerId, deviceId);
    4343              : 
    4344              :     // If a connection already exists or is in progress, no need to do this
    4345           95 :     std::lock_guard lk(sipConnsMtx_);
    4346           95 :     auto id = std::make_pair(peerId, deviceId);
    4347              : 
    4348           95 :     if (sipConns_.find(id) != sipConns_.end()) {
    4349            0 :         JAMI_LOG("[Account {}] A SIP connection with {} already exists", getAccountID(), deviceId);
    4350            0 :         return;
    4351              :     }
    4352              :     // If not present, create it
    4353           95 :     std::shared_lock lkCM(connManagerMtx_);
    4354           95 :     if (!connectionManager_)
    4355            0 :         return;
    4356              :     // Note, Even if we send 50 "sip" request, the connectionManager_ will only use one socket.
    4357              :     // however, this will still ask for multiple channels, so only ask
    4358              :     // if there is no pending request
    4359           95 :     if (!forceNewConnection && connectionManager_->isConnecting(deviceId, "sip")) {
    4360            0 :         JAMI_LOG("[Account {}] Already connecting to {}", getAccountID(), deviceId);
    4361            0 :         return;
    4362              :     }
    4363           95 :     JAMI_LOG("[Account {}] Ask {} for a new SIP channel", getAccountID(), deviceId);
    4364           95 :     dhtnet::ConnectDeviceOptions options;
    4365           95 :     options.noNewSocket = false;
    4366           95 :     options.forceNewSocket = forceNewConnection;
    4367           95 :     options.connType = connectionType;
    4368           95 :     options.channelTimeout = 3s;
    4369           95 :     options.uniqueName = true;
    4370          285 :     connectionManager_->connectDevice(
    4371              :         deviceId,
    4372              :         "sip",
    4373          190 :         [w = weak(), id = std::move(id), pc = std::move(pc)](const std::shared_ptr<dhtnet::ChannelSocket>& socket,
    4374              :                                                              const DeviceId&) {
    4375           95 :             if (socket)
    4376           92 :                 return;
    4377            3 :             auto shared = w.lock();
    4378            3 :             if (!shared)
    4379            0 :                 return;
    4380              :             // If this is triggered, this means that the
    4381              :             // connectDevice didn't get any response from the DHT.
    4382              :             // Stop searching pending call.
    4383            3 :             shared->callConnectionClosed(id.second, true);
    4384            3 :             if (pc)
    4385            3 :                 pc->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
    4386            3 :         },
    4387              :         options);
    4388           95 : }
    4389              : 
    4390              : bool
    4391          322 : JamiAccount::isConnectedWith(const DeviceId& deviceId) const
    4392              : {
    4393          322 :     std::shared_lock lkCM(connManagerMtx_);
    4394          322 :     if (connectionManager_)
    4395          321 :         return connectionManager_->isConnected(deviceId);
    4396            0 :     return false;
    4397          322 : }
    4398              : 
    4399              : void
    4400            3 : JamiAccount::sendPresenceNote(const std::string& note)
    4401              : {
    4402            3 :     if (const auto* info = accountManager_->getInfo()) {
    4403            3 :         if (!info || !info->contacts)
    4404            0 :             return;
    4405            3 :         presenceNote_ = note;
    4406            3 :         auto contacts = info->contacts->getContacts();
    4407            3 :         std::vector<std::pair<std::string, DeviceId>> keys;
    4408              :         {
    4409            3 :             std::shared_lock lkCM(connManagerMtx_);
    4410            3 :             auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
    4411            3 :             if (!handler)
    4412            0 :                 return;
    4413            5 :             for (const auto& contact : contacts) {
    4414            2 :                 auto peerId = contact.first.toString();
    4415            2 :                 auto channels = handler->getChannels(peerId);
    4416            5 :                 for (const auto& channel : channels) {
    4417            3 :                     keys.emplace_back(peerId, channel->deviceId());
    4418              :                 }
    4419            2 :             }
    4420            3 :         }
    4421            3 :         auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
    4422            9 :         std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
    4423            6 :         for (auto& key : keys) {
    4424            3 :             sendMessage(key.first, key.second.toString(), msg, token, false, true);
    4425              :         }
    4426            3 :     }
    4427            3 : }
    4428              : 
    4429              : void
    4430         1217 : JamiAccount::sendProfile(const std::string& convId, const std::string& peerUri, const std::string& deviceId)
    4431              : {
    4432         1217 :     auto accProfilePath = profilePath();
    4433         1217 :     if (not std::filesystem::is_regular_file(accProfilePath))
    4434         1213 :         return;
    4435            5 :     auto currentSha3 = fileutils::sha3File(accProfilePath);
    4436              :     // VCard sync for peerUri
    4437            5 :     if (not needToSendProfile(peerUri, deviceId, currentSha3)) {
    4438            0 :         JAMI_DEBUG("[Account {}] [device {}] Peer {} already got an up-to-date vCard",
    4439              :                    getAccountID(),
    4440              :                    deviceId,
    4441              :                    peerUri);
    4442            0 :         return;
    4443              :     }
    4444              :     // We need a new channel
    4445           30 :     transferFile(convId,
    4446           10 :                  accProfilePath.string(),
    4447              :                  deviceId,
    4448              :                  "profile.vcf",
    4449              :                  "",
    4450              :                  0,
    4451              :                  0,
    4452              :                  currentSha3,
    4453              :                  fileutils::lastWriteTimeInSeconds(accProfilePath),
    4454           10 :                  [accId = getAccountID(), peerUri, deviceId]() {
    4455              :                      // Mark the VCard as sent
    4456            3 :                      auto sendDir = fileutils::get_cache_dir() / accId / "vcard" / peerUri;
    4457            3 :                      auto path = sendDir / deviceId;
    4458            3 :                      dhtnet::fileutils::recursive_mkdir(sendDir);
    4459            3 :                      std::lock_guard lock(dhtnet::fileutils::getFileLock(path));
    4460            3 :                      if (std::filesystem::is_regular_file(path))
    4461            0 :                          return;
    4462            3 :                      std::ofstream p(path);
    4463            3 :                  });
    4464         1218 : }
    4465              : 
    4466              : bool
    4467            5 : JamiAccount::needToSendProfile(const std::string& peerUri, const std::string& deviceId, const std::string& sha3Sum)
    4468              : {
    4469            5 :     std::string previousSha3 {};
    4470            5 :     auto vCardPath = cachePath_ / "vcard";
    4471            5 :     auto sha3Path = vCardPath / "sha3";
    4472            5 :     dhtnet::fileutils::check_dir(vCardPath, 0700);
    4473              :     try {
    4474            7 :         previousSha3 = fileutils::loadTextFile(sha3Path);
    4475            2 :     } catch (...) {
    4476            2 :         fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
    4477            2 :         return true;
    4478            2 :     }
    4479            3 :     if (sha3Sum != previousSha3) {
    4480              :         // Incorrect sha3 stored. Update it
    4481            0 :         dhtnet::fileutils::removeAll(vCardPath, true);
    4482            0 :         dhtnet::fileutils::check_dir(vCardPath, 0700);
    4483            0 :         fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
    4484            0 :         return true;
    4485              :     }
    4486            3 :     auto peerPath = vCardPath / peerUri;
    4487            3 :     dhtnet::fileutils::recursive_mkdir(peerPath);
    4488            3 :     return not std::filesystem::is_regular_file(peerPath / deviceId);
    4489            5 : }
    4490              : 
    4491              : void
    4492           86 : JamiAccount::clearProfileCache(const std::string& peerUri)
    4493              : {
    4494           86 :     std::error_code ec;
    4495           86 :     std::filesystem::remove_all(cachePath_ / "vcard" / peerUri, ec);
    4496           86 : }
    4497              : 
    4498              : std::filesystem::path
    4499         1219 : JamiAccount::profilePath() const
    4500              : {
    4501         1219 :     return idPath_ / "profile.vcf";
    4502              : }
    4503              : 
    4504              : void
    4505          186 : JamiAccount::cacheSIPConnection(std::shared_ptr<dhtnet::ChannelSocket>&& socket,
    4506              :                                 const std::string& peerId,
    4507              :                                 const DeviceId& deviceId)
    4508              : {
    4509          186 :     std::unique_lock lk(sipConnsMtx_);
    4510              :     // Verify that the connection is not already cached
    4511          186 :     SipConnectionKey key(peerId, deviceId);
    4512          186 :     auto& connections = sipConns_[key];
    4513          186 :     auto conn = std::find_if(connections.begin(), connections.end(), [&](const auto& v) { return v.channel == socket; });
    4514          186 :     if (conn != connections.end()) {
    4515            0 :         JAMI_WARNING("[Account {}] Channel socket already cached with this peer", getAccountID());
    4516            0 :         return;
    4517              :     }
    4518              : 
    4519              :     // Convert to SIP transport
    4520          186 :     auto onShutdown = [w = weak(), peerId, key, socket]() {
    4521          186 :         dht::ThreadPool::io().run([w = std::move(w), peerId, key, socket] {
    4522          186 :             auto shared = w.lock();
    4523          186 :             if (!shared)
    4524            1 :                 return;
    4525          185 :             shared->shutdownSIPConnection(socket, key.first, key.second);
    4526              :             // The connection can be closed during the SIP initialization, so
    4527              :             // if this happens, the request should be re-sent to ask for a new
    4528              :             // SIP channel to make the call pass through
    4529          185 :             shared->callConnectionClosed(key.second, false);
    4530          186 :         });
    4531          372 :     };
    4532          186 :     auto sip_tr = link_.sipTransportBroker->getChanneledTransport(shared(), socket, std::move(onShutdown));
    4533          186 :     if (!sip_tr) {
    4534            0 :         JAMI_ERROR("No channeled transport found");
    4535            0 :         return;
    4536              :     }
    4537              :     // Store the connection
    4538          186 :     connections.emplace_back(SipConnection {sip_tr, socket});
    4539          186 :     JAMI_WARNING("[Account {:s}] [device {}] New SIP channel opened", getAccountID(), deviceId);
    4540          186 :     lk.unlock();
    4541              : 
    4542              :     // Retry messages
    4543          186 :     messageEngine_.onPeerOnline(peerId);
    4544          186 :     messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
    4545              : 
    4546              :     // Connect pending calls
    4547          186 :     forEachPendingCall(deviceId, [&](const auto& pc) {
    4548           92 :         if (pc->getConnectionState() != Call::ConnectionState::TRYING
    4549           92 :             and pc->getConnectionState() != Call::ConnectionState::PROGRESSING)
    4550            0 :             return;
    4551           92 :         pc->setSipTransport(sip_tr, getContactHeader(sip_tr));
    4552           92 :         pc->setState(Call::ConnectionState::PROGRESSING);
    4553           92 :         if (auto remote_address = socket->getRemoteAddress()) {
    4554              :             try {
    4555           92 :                 onConnectedOutgoingCall(pc, peerId, remote_address);
    4556            0 :             } catch (const VoipLinkException&) {
    4557              :                 // In this case, the main scenario is that SIPStartCall failed because
    4558              :                 // the ICE is dead and the TLS session didn't send any packet on that dead
    4559              :                 // link (connectivity change, killed by the os, etc)
    4560              :                 // Here, we don't need to do anything, the TLS will fail and will delete
    4561              :                 // the cached transport
    4562              :             }
    4563              :         }
    4564              :     });
    4565          186 : }
    4566              : 
    4567              : void
    4568          185 : JamiAccount::shutdownSIPConnection(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
    4569              :                                    const std::string& peerId,
    4570              :                                    const DeviceId& deviceId)
    4571              : {
    4572          185 :     std::unique_lock lk(sipConnsMtx_);
    4573          185 :     SipConnectionKey key(peerId, deviceId);
    4574          185 :     auto it = sipConns_.find(key);
    4575          185 :     if (it != sipConns_.end()) {
    4576           94 :         auto& conns = it->second;
    4577          188 :         conns.erase(std::remove_if(conns.begin(), conns.end(), [&](auto v) { return v.channel == channel; }),
    4578           94 :                     conns.end());
    4579           94 :         if (conns.empty()) {
    4580           94 :             sipConns_.erase(it);
    4581              :         }
    4582              :     }
    4583          185 :     lk.unlock();
    4584              :     // Shutdown after removal to let the callbacks do stuff if needed
    4585          185 :     if (channel)
    4586          185 :         channel->shutdown();
    4587          185 : }
    4588              : 
    4589              : std::string_view
    4590        37783 : JamiAccount::currentDeviceId() const
    4591              : {
    4592        37783 :     if (!accountManager_ or not accountManager_->getInfo())
    4593            0 :         return {};
    4594        37786 :     return accountManager_->getInfo()->deviceId;
    4595              : }
    4596              : 
    4597              : std::shared_ptr<TransferManager>
    4598          146 : JamiAccount::dataTransfer(const std::string& id)
    4599              : {
    4600          146 :     if (id.empty())
    4601           66 :         return nonSwarmTransferManager_;
    4602           80 :     if (auto* cm = convModule())
    4603           80 :         return cm->dataTransfer(id);
    4604            0 :     return {};
    4605              : }
    4606              : 
    4607              : void
    4608            0 : JamiAccount::monitor()
    4609              : {
    4610            0 :     JAMI_DEBUG("[Account {:s}] Monitor connections", getAccountID());
    4611            0 :     JAMI_DEBUG("[Account {:s}] Using proxy: {:s}", getAccountID(), proxyServerCached_);
    4612              : 
    4613            0 :     if (auto* cm = convModule())
    4614            0 :         cm->monitor();
    4615            0 :     std::shared_lock lkCM(connManagerMtx_);
    4616            0 :     if (connectionManager_)
    4617            0 :         connectionManager_->monitor();
    4618            0 : }
    4619              : 
    4620              : std::vector<std::map<std::string, std::string>>
    4621            0 : JamiAccount::getConnectionList(const std::string& conversationId)
    4622              : {
    4623            0 :     std::shared_lock lkCM(connManagerMtx_);
    4624            0 :     if (connectionManager_ && conversationId.empty()) {
    4625            0 :         return connectionManager_->getConnectionList();
    4626            0 :     } else if (connectionManager_ && convModule_) {
    4627            0 :         std::vector<std::map<std::string, std::string>> connectionList;
    4628            0 :         if (auto conv = convModule_->getConversation(conversationId)) {
    4629            0 :             for (const auto& deviceId : conv->getDeviceIdList()) {
    4630            0 :                 auto connections = connectionManager_->getConnectionList(deviceId);
    4631            0 :                 connectionList.reserve(connectionList.size() + connections.size());
    4632            0 :                 std::move(connections.begin(), connections.end(), std::back_inserter(connectionList));
    4633            0 :             }
    4634            0 :         }
    4635            0 :         return connectionList;
    4636            0 :     } else {
    4637            0 :         return {};
    4638              :     }
    4639            0 : }
    4640              : 
    4641              : std::vector<std::map<std::string, std::string>>
    4642            0 : JamiAccount::getConversationConnectivity(const std::string& conversationId)
    4643              : {
    4644            0 :     std::shared_lock lkCM(connManagerMtx_);
    4645            0 :     if (convModule_) {
    4646            0 :         if (auto conv = convModule_->getConversation(conversationId)) {
    4647            0 :             return conv->getConnectivity();
    4648            0 :         }
    4649              :     }
    4650            0 :     return {};
    4651            0 : }
    4652              : 
    4653              : std::vector<std::map<std::string, std::string>>
    4654            0 : JamiAccount::getConversationTrackedMembers(const std::string& conversationId)
    4655              : {
    4656            0 :     std::shared_lock lkCM(connManagerMtx_);
    4657            0 :     if (convModule_) {
    4658            0 :         if (auto conv = convModule_->getConversation(conversationId)) {
    4659            0 :             return conv->getTrackedMembers();
    4660            0 :         }
    4661              :     }
    4662            0 :     return {};
    4663            0 : }
    4664              : 
    4665              : std::vector<std::map<std::string, std::string>>
    4666            0 : JamiAccount::getChannelList(const std::string& connectionId)
    4667              : {
    4668            0 :     std::shared_lock lkCM(connManagerMtx_);
    4669            0 :     if (!connectionManager_)
    4670            0 :         return {};
    4671            0 :     return connectionManager_->getChannelList(connectionId);
    4672            0 : }
    4673              : 
    4674              : void
    4675           14 : JamiAccount::sendFile(const std::string& conversationId,
    4676              :                       const std::filesystem::path& path,
    4677              :                       const std::string& name,
    4678              :                       const std::string& replyTo)
    4679              : {
    4680           14 :     std::error_code ec;
    4681           14 :     if (!std::filesystem::is_regular_file(path, ec)) {
    4682            0 :         JAMI_ERROR("Invalid filename '{}'", path);
    4683            0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(getAccountID(),
    4684              :                                                                      conversationId,
    4685              :                                                                      EVALIDFETCH,
    4686              :                                                                      "Invalid filename.");
    4687            0 :         return;
    4688              :     }
    4689              : 
    4690           14 :     auto fileSize = std::filesystem::file_size(path, ec);
    4691           14 :     if (ec || fileSize == static_cast<decltype(fileSize)>(-1)) {
    4692            0 :         JAMI_ERROR("Negative file size, user probably doesn't have the appropriate permissions for '{}'", path);
    4693            0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(
    4694            0 :             getAccountID(),
    4695              :             conversationId,
    4696              :             EVALIDFETCH,
    4697              :             "Negative file size, could be due to insufficient file permissions.");
    4698            0 :         return;
    4699              :     }
    4700              : 
    4701              :     // NOTE: this sendMessage is in a computation thread because
    4702              :     // sha3sum can take quite some time to computer if the user decide
    4703              :     // to send a big file
    4704           14 :     dht::ThreadPool::computation().run([w = weak(), conversationId, path, name, fileSize, replyTo]() {
    4705           14 :         if (auto shared = w.lock()) {
    4706           14 :             auto tid = jami::generateUID(shared->rand);
    4707           14 :             auto displayName = name.empty() ? path.filename().string() : name;
    4708           14 :             auto commitMessage = CommitMessage::fileSent(displayName, fileutils::sha3File(path), tid, fileSize, replyTo);
    4709              : 
    4710           28 :             shared->convModule()->createCommit(
    4711           14 :                 conversationId,
    4712           14 :                 std::move(commitMessage),
    4713              :                 true,
    4714           28 :                 [accId = shared->getAccountID(), conversationId, tid, displayName, path](const std::string& commitId) {
    4715              :                     // Create a symlink to answer to re-ask
    4716           28 :                     auto filelinkPath = fileutils::get_data_dir() / accId / "conversation_data" / conversationId
    4717           42 :                                         / getFileId(commitId, std::to_string(tid), displayName);
    4718           14 :                     if (path != filelinkPath && !std::filesystem::is_symlink(filelinkPath)) {
    4719           14 :                         if (!fileutils::createFileLink(filelinkPath, path, true)) {
    4720            0 :                             JAMI_WARNING("Unable to create symlink for file transfer {} - {}. Copy file",
    4721              :                                          filelinkPath,
    4722              :                                          path);
    4723            0 :                             std::error_code ec;
    4724            0 :                             auto success = std::filesystem::copy_file(path, filelinkPath, ec);
    4725            0 :                             if (ec || !success) {
    4726            0 :                                 JAMI_ERROR("Unable to copy file for file transfer {} - {}", filelinkPath, path);
    4727              :                                 // Signal to notify clients that the operation failed.
    4728              :                                 // The fileId field sends the filePath.
    4729              :                                 // libjami::DataTransferEventCode::unsupported (2) is unused elsewhere.
    4730            0 :                                 emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
    4731            0 :                                     accId,
    4732            0 :                                     conversationId,
    4733              :                                     commitId,
    4734            0 :                                     path.string(),
    4735              :                                     uint32_t(libjami::DataTransferEventCode::invalid));
    4736              :                             } else {
    4737              :                                 // Signal to notify clients that the file is copied and can be
    4738              :                                 // safely deleted. The fileId field sends the filePath.
    4739              :                                 // libjami::DataTransferEventCode::created (1) is unused elsewhere.
    4740            0 :                                 emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
    4741            0 :                                     accId,
    4742            0 :                                     conversationId,
    4743              :                                     commitId,
    4744            0 :                                     path.string(),
    4745              :                                     uint32_t(libjami::DataTransferEventCode::created));
    4746              :                             }
    4747              :                         } else {
    4748           28 :                             emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
    4749           14 :                                 accId,
    4750           14 :                                 conversationId,
    4751              :                                 commitId,
    4752           28 :                                 path.string(),
    4753              :                                 uint32_t(libjami::DataTransferEventCode::created));
    4754              :                         }
    4755              :                     }
    4756           14 :                 });
    4757           28 :         }
    4758           14 :     });
    4759              : }
    4760              : 
    4761              : void
    4762            5 : JamiAccount::transferFile(const std::string& conversationId,
    4763              :                           const std::string& path,
    4764              :                           const std::string& deviceId,
    4765              :                           const std::string& fileId,
    4766              :                           const std::string& interactionId,
    4767              :                           size_t start,
    4768              :                           size_t end,
    4769              :                           const std::string& sha3Sum,
    4770              :                           uint64_t lastWriteTime,
    4771              :                           std::function<void()> onFinished)
    4772              : {
    4773            5 :     std::string modified;
    4774            5 :     if (lastWriteTime != 0) {
    4775           10 :         modified = fmt::format("&modified={}", lastWriteTime);
    4776              :     }
    4777           10 :     auto fid = fileId == "profile.vcf" ? fmt::format("profile.vcf?sha3={}{}", sha3Sum, modified) : fileId;
    4778            5 :     auto channelName = conversationId.empty()
    4779            5 :                            ? fmt::format("{}profile.vcf?sha3={}{}", DATA_TRANSFER_SCHEME, sha3Sum, modified)
    4780           10 :                            : fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fid);
    4781            5 :     std::shared_lock lkCM(connManagerMtx_);
    4782            5 :     if (!connectionManager_)
    4783            0 :         return;
    4784           10 :     connectionManager_->connectDevice(
    4785           10 :         DeviceId(deviceId),
    4786              :         channelName,
    4787           15 :         [this,
    4788              :          conversationId,
    4789            5 :          path = std::move(path),
    4790              :          fileId,
    4791              :          interactionId,
    4792              :          start,
    4793              :          end,
    4794            5 :          onFinished = std::move(onFinished)](std::shared_ptr<dhtnet::ChannelSocket> socket, const DeviceId&) {
    4795            5 :             if (!socket)
    4796            0 :                 return;
    4797           25 :             dht::ThreadPool::io().run([w = weak(),
    4798            5 :                                        path = std::move(path),
    4799            5 :                                        socket = std::move(socket),
    4800            5 :                                        conversationId = std::move(conversationId),
    4801            5 :                                        fileId,
    4802            5 :                                        interactionId,
    4803              :                                        start,
    4804              :                                        end,
    4805            5 :                                        onFinished = std::move(onFinished)] {
    4806            5 :                 if (auto shared = w.lock())
    4807            5 :                     if (auto dt = shared->dataTransfer(conversationId))
    4808           10 :                         dt->transferFile(socket, fileId, interactionId, path, start, end, std::move(onFinished));
    4809            5 :             });
    4810              :         });
    4811           10 : }
    4812              : 
    4813              : void
    4814           13 : JamiAccount::askForFileChannel(const std::string& conversationId,
    4815              :                                const std::string& deviceId,
    4816              :                                const std::string& interactionId,
    4817              :                                const std::string& fileId,
    4818              :                                size_t start,
    4819              :                                size_t end)
    4820              : {
    4821           29 :     auto tryDevice = [=](const auto& did) {
    4822           29 :         std::shared_lock lkCM(connManagerMtx_);
    4823           29 :         if (!connectionManager_)
    4824            0 :             return;
    4825              : 
    4826           29 :         auto channelName = fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fileId);
    4827           29 :         if (start != 0 || end != 0) {
    4828            6 :             channelName += fmt::format("?start={}&end={}", start, end);
    4829              :         }
    4830              :         // We can avoid to negotiate new sessions, as the file notif
    4831              :         // probably came from an online device or last connected device.
    4832           87 :         connectionManager_->connectDevice(
    4833              :             did,
    4834              :             channelName,
    4835           87 :             [w = weak(),
    4836           29 :              conversationId,
    4837           29 :              fileId,
    4838           29 :              interactionId,
    4839              :              start](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
    4840           29 :                 if (!channel)
    4841           17 :                     return;
    4842           12 :                 dht::ThreadPool::io().run([w, conversationId, channel, fileId, interactionId, start] {
    4843           12 :                     auto shared = w.lock();
    4844           12 :                     if (!shared)
    4845            0 :                         return;
    4846           12 :                     auto dt = shared->dataTransfer(conversationId);
    4847           12 :                     if (!dt)
    4848            0 :                         return;
    4849           12 :                     if (interactionId.empty())
    4850            0 :                         dt->onIncomingProfile(channel);
    4851              :                     else
    4852           12 :                         dt->onIncomingFileTransfer(fileId, channel, start);
    4853           12 :                 });
    4854              :             },
    4855              :             false);
    4856           42 :     };
    4857              : 
    4858           13 :     if (!deviceId.empty()) {
    4859              :         // Only ask for device
    4860            1 :         tryDevice(DeviceId(deviceId));
    4861              :     } else {
    4862              :         // Only ask for connected devices. For others we will attempt
    4863              :         // with new peer online
    4864           40 :         for (const auto& m : convModule()->getConversationMembers(conversationId)) {
    4865          140 :             accountManager_->forEachDevice(dht::InfoHash(m.at("uri")),
    4866           56 :                                            [tryDevice](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
    4867           28 :                                                tryDevice(dev->getLongId());
    4868           28 :                                            });
    4869           12 :         }
    4870              :     }
    4871           13 : }
    4872              : 
    4873              : void
    4874           47 : JamiAccount::askForProfile(const std::string& conversationId, const std::string& deviceId, const std::string& memberUri)
    4875              : {
    4876           47 :     std::shared_lock lkCM(connManagerMtx_);
    4877           47 :     if (!connectionManager_)
    4878            0 :         return;
    4879              : 
    4880           47 :     auto channelName = fmt::format("{}{}/profile/{}.vcf", DATA_TRANSFER_SCHEME, conversationId, memberUri);
    4881              :     // We can avoid to negotiate new sessions, as the file notif
    4882              :     // probably came from an online device or last connected device.
    4883          188 :     connectionManager_->connectDevice(
    4884           94 :         DeviceId(deviceId),
    4885              :         channelName,
    4886           94 :         [this, conversationId](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
    4887           47 :             if (!channel)
    4888            1 :                 return;
    4889           46 :             dht::ThreadPool::io().run([w = weak(), conversationId, channel] {
    4890           46 :                 if (auto shared = w.lock())
    4891           46 :                     if (auto dt = shared->dataTransfer(conversationId))
    4892          184 :                         dt->onIncomingProfile(channel);
    4893           46 :             });
    4894              :         },
    4895              :         false);
    4896           47 : }
    4897              : 
    4898              : void
    4899         2489 : JamiAccount::onPeerConnected(const std::string& peerId, bool connected)
    4900              : {
    4901         2489 :     auto isOnline = presenceManager_ && presenceManager_->isOnline(peerId);
    4902         3732 :     auto newState = connected ? PresenceState::CONNECTED
    4903         1243 :                               : (isOnline ? PresenceState::AVAILABLE : PresenceState::DISCONNECTED);
    4904              : 
    4905         2489 :     runOnMainThread([w = weak(), peerId, newState] {
    4906         2489 :         if (auto sthis = w.lock()) {
    4907         2489 :             std::lock_guard lock(sthis->presenceStateMtx_);
    4908         2489 :             auto& state = sthis->presenceState_[peerId];
    4909         2489 :             if (state != newState) {
    4910         2489 :                 state = newState;
    4911         2489 :                 emitSignal<libjami::PresenceSignal::NewBuddyNotification>(sthis->getAccountID(),
    4912         2489 :                                                                           peerId,
    4913              :                                                                           static_cast<int>(newState),
    4914              :                                                                           "");
    4915              :             }
    4916         4978 :         }
    4917         2489 :     });
    4918         2489 : }
    4919              : 
    4920              : void
    4921          746 : JamiAccount::initConnectionManager()
    4922              : {
    4923          746 :     if (!nonSwarmTransferManager_)
    4924          711 :         nonSwarmTransferManager_ = std::make_shared<TransferManager>(accountID_,
    4925          711 :                                                                      config().username,
    4926              :                                                                      "",
    4927         2133 :                                                                      dht::crypto::getDerivedRandomEngine(rand));
    4928          746 :     if (!connectionManager_) {
    4929          721 :         auto connectionManagerConfig = std::make_shared<dhtnet::ConnectionManager::Config>();
    4930          721 :         connectionManagerConfig->ioContext = Manager::instance().ioContext();
    4931          721 :         connectionManagerConfig->dht = dht();
    4932          721 :         connectionManagerConfig->certStore = certStore_;
    4933          721 :         connectionManagerConfig->id = identity();
    4934          721 :         connectionManagerConfig->upnpCtrl = upnpCtrl_;
    4935          721 :         connectionManagerConfig->turnServer = config().turnServer;
    4936          721 :         connectionManagerConfig->upnpEnabled = config().upnpEnabled;
    4937          721 :         connectionManagerConfig->turnServerUserName = config().turnServerUserName;
    4938          721 :         connectionManagerConfig->turnServerPwd = config().turnServerPwd;
    4939          721 :         connectionManagerConfig->turnServerRealm = config().turnServerRealm;
    4940          721 :         connectionManagerConfig->turnEnabled = config().turnEnabled;
    4941          721 :         connectionManagerConfig->cachePath = cachePath_;
    4942          721 :         if (Manager::instance().dhtnetLogLevel > 0) {
    4943            0 :             connectionManagerConfig->logger = logger_;
    4944              :         }
    4945          721 :         connectionManagerConfig->factory = Manager::instance().getIceTransportFactory();
    4946          721 :         connectionManagerConfig->turnCache = turnCache_;
    4947          721 :         connectionManagerConfig->rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
    4948          721 :         connectionManagerConfig->legacyMode = dhtnet::LegacyMode::Disabled;
    4949          721 :         connectionManager_ = std::make_unique<dhtnet::ConnectionManager>(connectionManagerConfig);
    4950         1442 :         channelHandlers_[Uri::Scheme::SWARM] = std::make_unique<SwarmChannelHandler>(shared(),
    4951         1442 :                                                                                      *connectionManager_.get());
    4952         1442 :         channelHandlers_[Uri::Scheme::GIT] = std::make_unique<ConversationChannelHandler>(shared(),
    4953         1442 :                                                                                           *connectionManager_.get());
    4954          721 :         channelHandlers_[Uri::Scheme::YDOC] = std::make_unique<YdocChannelHandler>(shared(), *connectionManager_.get());
    4955          721 :         if (jami::Manager::instance().syncOnRegister) {
    4956         1442 :             channelHandlers_[Uri::Scheme::SYNC] = std::make_unique<SyncChannelHandler>(shared(),
    4957         1442 :                                                                                        *connectionManager_.get());
    4958              :         }
    4959          721 :         channelHandlers_[Uri::Scheme::DATA_TRANSFER]
    4960         1442 :             = std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get());
    4961         1442 :         channelHandlers_[Uri::Scheme::MESSAGE] = std::make_unique<MessageChannelHandler>(
    4962          721 :             *connectionManager_.get(),
    4963          721 :             [this](const auto& cert, std::string& type, const std::string& content) {
    4964        61954 :                 onTextMessage("", cert->issuer->getId().toString(), cert, {{type, content}});
    4965        24786 :             },
    4966         1442 :             [w = weak()](const std::string& peer, bool connected) {
    4967         2490 :                 asio::post(*Manager::instance().ioContext(), [w, peer, connected] {
    4968         2492 :                     if (auto acc = w.lock())
    4969         2492 :                         acc->onPeerConnected(peer, connected);
    4970         2492 :                 });
    4971         2492 :             },
    4972         1442 :             [w = weak()](const std::string& peer, const DeviceId& device) {
    4973         1259 :                 dht::ThreadPool::io().run([w, peer, device] {
    4974         1258 :                     if (auto acc = w.lock())
    4975         1259 :                         acc->onMessageChannelReady(peer, device);
    4976         1257 :                 });
    4977         1980 :             });
    4978          721 :         channelHandlers_[Uri::Scheme::AUTH] = std::make_unique<AuthChannelHandler>(shared(), *connectionManager_.get());
    4979              : 
    4980          721 :         if (!serviceManager_)
    4981          711 :             serviceManager_ = std::make_unique<ServiceManager>(idPath_);
    4982          721 :         channelHandlers_[Uri::Scheme::SVC_DISCOVERY]
    4983         1442 :             = std::make_unique<SvcDiscoveryChannelHandler>(shared(), *connectionManager_.get(), cachePath_);
    4984          721 :         static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get())
    4985          721 :             ->onCacheUpdated([w = weak()](const std::string& peerUri,
    4986              :                                           const DeviceId& deviceId,
    4987              :                                           const std::vector<svc_protocol::SvcInfo>&) {
    4988         1258 :                 auto self = w.lock();
    4989         1258 :                 if (!self)
    4990            0 :                     return;
    4991              :                 // Re-publish the peer's full service list. The device that just
    4992              :                 // answered is flagged available immediately, even if its DHT
    4993              :                 // presence announcement has not yet been observed. A cache
    4994              :                 // update is always published, even when the peer's last service
    4995              :                 // was removed (empty list), so listeners can clear it.
    4996         1258 :                 auto servicesJson = self->buildPeerServicesJson(peerUri, &deviceId);
    4997         2516 :                 emitSignal<libjami::ServiceSignal::PeerServicesReceived>(
    4998              :                     0u,
    4999         1258 :                     self->getAccountID(),
    5000              :                     peerUri,
    5001              :                     static_cast<int>(libjami::ServiceSignal::PeerServicesStatus::OK),
    5002         3765 :                     servicesJson.empty() ? "[]" : servicesJson);
    5003         1258 :             });
    5004          721 :         serviceManager_->setOnChanged([w = weak()]() {
    5005            8 :             auto self = w.lock();
    5006            8 :             if (!self)
    5007            0 :                 return;
    5008            8 :             runOnMainThread([w]() {
    5009            8 :                 auto self = w.lock();
    5010            8 :                 if (!self)
    5011            0 :                     return;
    5012            8 :                 std::shared_lock lk(self->connManagerMtx_);
    5013            8 :                 auto it = self->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
    5014            8 :                 if (it != self->channelHandlers_.end() && it->second)
    5015            8 :                     static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->broadcastServiceUpdate();
    5016            8 :             });
    5017            8 :         });
    5018          721 :         channelHandlers_[Uri::Scheme::SVC_TUNNEL]
    5019         1442 :             = std::make_unique<SvcTunnelChannelHandler>(shared(),
    5020          721 :                                                         *connectionManager_.get(),
    5021         2163 :                                                         Manager::instance().ioContext());
    5022              : 
    5023              : #if TARGET_OS_IOS
    5024              :         connectionManager_->oniOSConnected([&](const std::string& connType, dht::InfoHash peer_h) {
    5025              :             if ((connType == "videoCall" || connType == "audioCall") && jami::Manager::instance().isIOSExtension) {
    5026              :                 bool hasVideo = connType == "videoCall";
    5027              :                 emitSignal<libjami::ConversationSignal::CallConnectionRequest>("", peer_h.toString(), hasVideo);
    5028              :                 return true;
    5029              :             }
    5030              :             return false;
    5031              :         });
    5032              : #endif
    5033          721 :     }
    5034          746 : }
    5035              : 
    5036              : void
    5037         1039 : JamiAccount::updateUpnpController()
    5038              : {
    5039         1039 :     if (upnpCtrl_ and (not config().upnpEnabled or not isUsable()))
    5040          159 :         releaseDhtUpnpMapping();
    5041         1039 :     Account::updateUpnpController();
    5042         1039 :     if (connectionManager_) {
    5043           44 :         auto config = connectionManager_->getConfig();
    5044           44 :         if (config)
    5045           44 :             config->upnpCtrl = upnpCtrl_;
    5046           44 :     }
    5047         1039 : }
    5048              : 
    5049              : } // namespace jami
        

Generated by: LCOV version 2.0-1