LCOV - code coverage report
Current view: top level - src/jamidht - jamiaccount.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 74.0 % 3194 2365
Test Date: 2026-11-09 10:41:14 Functions: 83.7 % 344 288

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

Generated by: LCOV version 2.0-1