LCOV - code coverage report
Current view: top level - src/jamidht - conversation_module.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 83.5 % 2057 1717
Test Date: 2026-07-06 08:25:38 Functions: 72.4 % 391 283

            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              : #include "conversation_module.h"
      19              : 
      20              : #include "account_const.h"
      21              : #include "call.h"
      22              : #include "client/jami_signal.h"
      23              : #include "fileutils.h"
      24              : #include "jamidht/account_manager.h"
      25              : #include "jamidht/commit_message.h"
      26              : #include "jamidht/jamiaccount.h"
      27              : #include "jamidht/presence_manager.h"
      28              : #include "manager.h"
      29              : #ifdef ENABLE_PLUGIN
      30              : #include "plugin/jamipluginmanager.h"
      31              : #endif
      32              : #include "sip/sipcall.h"
      33              : #include "vcard.h"
      34              : #include "json_utils.h"
      35              : 
      36              : #include <opendht/thread_pool.h>
      37              : #include <dhtnet/certstore.h>
      38              : 
      39              : #include <algorithm>
      40              : #include <fstream>
      41              : 
      42              : namespace jami {
      43              : 
      44              : using ConvInfoMap = std::map<std::string, ConvInfo>;
      45              : 
      46              : struct PendingConversationFetch
      47              : {
      48              :     bool ready {false};
      49              :     bool cloning {false};
      50              :     std::string deviceId {};
      51              :     std::map<std::string, std::string> preferences {};
      52              :     std::map<std::string, std::map<std::string, std::string>> status {};
      53              :     std::set<std::string> connectingTo {};
      54              :     std::shared_ptr<dhtnet::ChannelSocket> socket {};
      55              : };
      56              : 
      57              : constexpr std::chrono::seconds MAX_FALLBACK {12 * 3600s};
      58              : // Maximum attempts at cloning a conversation whose repository fails validation
      59              : // before giving up. Validation failures are permanent (the remote history is
      60              : // immutable), so retrying is wasted work; the counter is kept in-memory only,
      61              : // so a restart allows a new bounded round of attempts.
      62              : constexpr unsigned MAX_VALIDATION_FAILURES {3};
      63              : 
      64              : struct SyncedConversation
      65              : {
      66              :     std::mutex mtx;
      67              :     std::unique_ptr<asio::steady_timer> fallbackClone;
      68              :     std::chrono::seconds fallbackTimer {5s};
      69              :     unsigned validationFailures {0};
      70              :     ConvInfo info;
      71              :     std::unique_ptr<PendingConversationFetch> pending;
      72              :     std::shared_ptr<Conversation> conversation;
      73              : 
      74          334 :     bool isUnrecoverable() const { return validationFailures >= MAX_VALIDATION_FAILURES; }
      75              : 
      76          370 :     SyncedConversation(const std::string& convId)
      77          370 :         : info {convId}
      78              :     {
      79          370 :         fallbackClone = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext());
      80          370 :     }
      81           30 :     SyncedConversation(const ConvInfo& info)
      82           30 :         : info {info}
      83              :     {
      84           30 :         fallbackClone = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext());
      85           30 :     }
      86              : 
      87         2233 :     bool startFetch(const std::string& deviceId, bool checkIfConv = false)
      88              :     {
      89              :         // conversation mtx must be locked
      90         2233 :         if (checkIfConv && conversation)
      91            8 :             return false; // Already a conversation
      92         2225 :         if (pending) {
      93          525 :             if (pending->ready)
      94           36 :                 return false; // Already doing stuff
      95              :             // if (pending->deviceId == deviceId)
      96              :             //     return false; // Already fetching
      97          488 :             if (pending->connectingTo.find(deviceId) != pending->connectingTo.end())
      98           90 :                 return false; // Already connecting to this device
      99              :         } else {
     100         1700 :             pending = std::make_unique<PendingConversationFetch>();
     101         1700 :             pending->connectingTo.insert(deviceId);
     102         1700 :             return true;
     103              :         }
     104          399 :         return true;
     105              :     }
     106              : 
     107           49 :     void stopFetch(const std::string& deviceId)
     108              :     {
     109              :         // conversation mtx must be locked
     110           49 :         if (!pending)
     111            0 :             return;
     112           49 :         pending->connectingTo.erase(deviceId);
     113           49 :         if (pending->connectingTo.empty())
     114           47 :             pending.reset();
     115              :     }
     116              : 
     117          131 :     std::vector<std::map<std::string, std::string>> getMembers(bool includeLeft, bool includeBanned) const
     118              :     {
     119              :         // conversation mtx must be locked
     120          131 :         if (conversation)
     121          100 :             return conversation->getMembers(true, includeLeft, includeBanned);
     122              :         // If we're cloning, we can return the initial members
     123           31 :         std::vector<std::map<std::string, std::string>> result;
     124           31 :         result.reserve(info.members.size());
     125           93 :         for (const auto& uri : info.members) {
     126          186 :             result.emplace_back(std::map<std::string, std::string> {{"uri", uri}});
     127              :         }
     128           31 :         return result;
     129           93 :     }
     130              : };
     131              : 
     132              : class ConversationModule::Impl : public std::enable_shared_from_this<Impl>
     133              : {
     134              : public:
     135              :     Impl(std::shared_ptr<JamiAccount>&& account,
     136              :          std::shared_ptr<AccountManager>&& accountManager,
     137              :          NeedsSyncingCb&& needsSyncingCb,
     138              :          SengMsgCb&& sendMsgCb,
     139              :          NeedSocketCb&& onNeedSocket,
     140              :          NeedSocketCb&& onNeedSwarmSocket,
     141              :          OneToOneRecvCb&& oneToOneRecvCb);
     142              : 
     143              :     template<typename S, typename T>
     144          129 :     inline auto withConv(const S& convId, T&& cb) const
     145              :     {
     146          258 :         if (auto conv = getConversation(convId)) {
     147          129 :             std::lock_guard lk(conv->mtx);
     148          129 :             return cb(*conv);
     149          129 :         } else {
     150            0 :             JAMI_WARNING("Conversation {} not found", convId);
     151              :         }
     152            0 :         return decltype(cb(std::declval<SyncedConversation&>()))();
     153              :     }
     154              :     template<typename S, typename T>
     155         2257 :     inline auto withConversation(const S& convId, T&& cb)
     156              :     {
     157         4514 :         if (auto conv = getConversation(convId)) {
     158         2249 :             std::lock_guard lk(conv->mtx);
     159         2250 :             if (conv->conversation)
     160         2188 :                 return cb(*conv->conversation);
     161         2250 :         } else {
     162           28 :             JAMI_WARNING("Conversation {} not found", convId);
     163              :         }
     164           68 :         return decltype(cb(std::declval<Conversation&>()))();
     165              :     }
     166              : 
     167              :     // Retrieving recent commits
     168              :     /**
     169              :      * Clone a conversation (initial) from device
     170              :      * @param deviceId
     171              :      * @param convId
     172              :      */
     173              :     void cloneConversation(const std::string& deviceId, const std::string& peer, const std::string& convId);
     174              :     void cloneConversation(const std::string& deviceId,
     175              :                            const std::string& peer,
     176              :                            const std::shared_ptr<SyncedConversation>& conv);
     177              : 
     178              :     /**
     179              :      * Pull remote device
     180              :      * @param peer              Contact URI
     181              :      * @param deviceId          Contact's device
     182              :      * @param conversationId
     183              :      * @param commitId (optional)
     184              :      */
     185              :     void fetchNewCommits(const std::string& peer,
     186              :                          const std::string& deviceId,
     187              :                          const std::string& conversationId,
     188              :                          const std::string& commitId = "");
     189              :     /**
     190              :      * Handle events to receive new commits
     191              :      */
     192              :     void handlePendingConversation(const std::string& conversationId, const std::string& deviceId);
     193              : 
     194              :     // Requests
     195              :     std::optional<ConversationRequest> getRequest(const std::string& id) const;
     196              : 
     197              :     // Conversations
     198              :     /**
     199              :      * Get members
     200              :      * @param conversationId
     201              :      * @param includeBanned
     202              :      * @return a map of members with their role and details
     203              :      */
     204              :     std::vector<std::map<std::string, std::string>> getConversationMembers(const std::string& conversationId,
     205              :                                                                            bool includeBanned = false) const;
     206              :     void setConversationMembers(const std::string& convId, const std::set<std::string>& members);
     207              : 
     208              :     /**
     209              :      * Remove a repository and all files
     210              :      * @param convId
     211              :      * @param sync      If we send an update to other account's devices
     212              :      * @param force     True if ignore the removing flag
     213              :      */
     214              :     void removeRepository(const std::string& convId, bool sync, bool force = false);
     215              :     void removeRepositoryImpl(SyncedConversation& conv, bool sync, bool force = false);
     216              :     /**
     217              :      * Remove a conversation
     218              :      * @param conversationId
     219              :      */
     220              :     bool removeConversation(const std::string& conversationId, bool forceRemove = false);
     221              :     bool removeConversationImpl(SyncedConversation& conv, bool forceRemove = false);
     222              : 
     223              :     /**
     224              :      * Send a message notification to all members
     225              :      * @param conversation
     226              :      * @param commit
     227              :      * @param sync      If we send an update to other account's devices
     228              :      * @param deviceId  If we need to filter a specific device
     229              :      */
     230              :     void sendMessageNotification(const std::string& conversationId,
     231              :                                  bool sync,
     232              :                                  const std::string& commitId = "",
     233              :                                  const std::string& deviceId = "");
     234              :     void sendMessageNotification(Conversation& conversation,
     235              :                                  bool sync,
     236              :                                  const std::string& commitId = "",
     237              :                                  const std::string& deviceId = "");
     238              : 
     239              :     /**
     240              :      * @return if a convId is a valid conversation (repository cloned & usable)
     241              :      */
     242          606 :     bool isConversation(const std::string& convId) const
     243              :     {
     244          606 :         std::lock_guard lk(conversationsMtx_);
     245          606 :         auto c = conversations_.find(convId);
     246         1212 :         return c != conversations_.end() && c->second;
     247          606 :     }
     248              : 
     249         2697 :     void addConvInfo(const ConvInfo& info)
     250              :     {
     251         2697 :         std::lock_guard lk(convInfosMtx_);
     252         2697 :         convInfos_[info.id] = info;
     253         2697 :         saveConvInfos();
     254         2697 :     }
     255              : 
     256              :     std::string getOneToOneConversation(const std::string& uri) const noexcept;
     257              : 
     258              :     bool updateConvForContact(const std::string& uri, const std::string& oldConv, const std::string& newConv);
     259              : 
     260          129 :     std::shared_ptr<SyncedConversation> getConversation(std::string_view convId) const
     261              :     {
     262          129 :         std::lock_guard lk(conversationsMtx_);
     263          129 :         auto c = conversations_.find(convId);
     264          258 :         return c != conversations_.end() ? c->second : nullptr;
     265          129 :     }
     266        34115 :     std::shared_ptr<SyncedConversation> getConversation(std::string_view convId)
     267              :     {
     268        34115 :         std::lock_guard lk(conversationsMtx_);
     269        34123 :         auto c = conversations_.find(convId);
     270        68240 :         return c != conversations_.end() ? c->second : nullptr;
     271        34112 :     }
     272          438 :     std::shared_ptr<SyncedConversation> startConversation(const std::string& convId)
     273              :     {
     274          438 :         std::lock_guard lk(conversationsMtx_);
     275          438 :         auto& c = conversations_[convId];
     276          438 :         if (!c)
     277          352 :             c = std::make_shared<SyncedConversation>(convId);
     278          876 :         return c;
     279          438 :     }
     280          103 :     std::shared_ptr<SyncedConversation> startConversation(const ConvInfo& info)
     281              :     {
     282          103 :         std::lock_guard lk(conversationsMtx_);
     283          103 :         auto& c = conversations_[info.id];
     284          103 :         if (!c)
     285           17 :             c = std::make_shared<SyncedConversation>(info);
     286          206 :         return c;
     287          103 :     }
     288         4249 :     std::vector<std::shared_ptr<SyncedConversation>> getSyncedConversations() const
     289              :     {
     290         4249 :         std::lock_guard lk(conversationsMtx_);
     291         4249 :         std::vector<std::shared_ptr<SyncedConversation>> result;
     292         4249 :         result.reserve(conversations_.size());
     293         6717 :         for (const auto& [_, c] : conversations_)
     294         2468 :             result.emplace_back(c);
     295         8497 :         return result;
     296         4248 :     }
     297         1598 :     std::vector<std::shared_ptr<Conversation>> getConversations() const
     298              :     {
     299         1598 :         auto conversations = getSyncedConversations();
     300         1598 :         std::vector<std::shared_ptr<Conversation>> result;
     301         1598 :         result.reserve(conversations.size());
     302         2721 :         for (const auto& sc : conversations) {
     303         1124 :             std::lock_guard lk(sc->mtx);
     304         1124 :             if (sc->conversation)
     305          962 :                 result.emplace_back(sc->conversation);
     306         1124 :         }
     307         3196 :         return result;
     308         1598 :     }
     309              : 
     310              :     void createCommit(const std::string& conversationId,
     311              :                       CommitMessage&& message,
     312              :                       bool announce = true,
     313              :                       OnCommitCb&& onCommit = {},
     314              :                       OnDoneCb&& cb = {});
     315              : 
     316              :     void sendMessage(const std::string& conversationId,
     317              :                      std::string message,
     318              :                      const std::string& replyTo = "",
     319              :                      bool announce = true,
     320              :                      OnCommitCb&& onCommit = {},
     321              :                      OnDoneCb&& cb = {});
     322              : 
     323              :     void editMessage(const std::string& conversationId, const std::string& newBody, const std::string& editedId);
     324              : 
     325              :     void bootstrapCb(std::string convId);
     326              : 
     327              :     // The following methods modify what is stored on the disk
     328              :     /**
     329              :      * @note convInfosMtx_ should be locked
     330              :      */
     331         3398 :     void saveConvInfos() const { ConversationModule::saveConvInfos(accountId_, convInfos_); }
     332              :     /**
     333              :      * @note conversationsRequestsMtx_ should be locked
     334              :      */
     335          482 :     void saveConvRequests() const { ConversationModule::saveConvRequests(accountId_, conversationsRequests_); }
     336              :     void declineOtherConversationWith(const std::string& uri);
     337          232 :     bool addConversationRequest(const std::string& id, const ConversationRequest& req)
     338              :     {
     339              :         // conversationsRequestsMtx_ MUST BE LOCKED
     340          232 :         if (isConversation(id))
     341            0 :             return false;
     342          232 :         auto it = conversationsRequests_.find(id);
     343          232 :         if (it != conversationsRequests_.end()) {
     344              :             // We only remove requests (if accepted) or change .declined
     345           31 :             if (req.declined == TimePoint {})
     346           28 :                 return false;
     347            3 :             if (it->second.declined != TimePoint {}) {
     348              :                 // Already declined: nothing changed. Reporting a change here
     349              :                 // would make devices re-propagate declined requests to each
     350              :                 // other forever (sync ping-pong).
     351            2 :                 return false;
     352              :             }
     353          201 :         } else if (req.isOneToOne()) {
     354              :             // Check that we're not adding a second one to one trust request
     355              :             // NOTE: If a new one to one request is received, we can decline the previous one.
     356           68 :             declineOtherConversationWith(req.from);
     357              :         }
     358          808 :         JAMI_DEBUG("[Account {}] [Conversation {}] Adding conversation request from {}", accountId_, id, req.from);
     359          202 :         conversationsRequests_[id] = req;
     360          202 :         saveConvRequests();
     361          202 :         return true;
     362              :     }
     363          270 :     void rmConversationRequest(const std::string& id)
     364              :     {
     365              :         // conversationsRequestsMtx_ MUST BE LOCKED
     366          270 :         auto it = conversationsRequests_.find(id);
     367          270 :         if (it != conversationsRequests_.end()) {
     368          171 :             auto& md = syncingMetadatas_[id];
     369          171 :             md = it->second.metadatas;
     370          171 :             md["syncing"] = "true";
     371          513 :             md["created"] = std::to_string(toSecondsSinceEpoch(it->second.received));
     372              :         }
     373          270 :         saveMetadata();
     374          270 :         conversationsRequests_.erase(id);
     375          270 :         saveConvRequests();
     376          270 :     }
     377              : 
     378              :     std::weak_ptr<JamiAccount> account_;
     379              :     std::shared_ptr<AccountManager> accountManager_;
     380              :     const std::string accountId_ {};
     381              :     NeedsSyncingCb needsSyncingCb_;
     382              :     SengMsgCb sendMsgCb_;
     383              :     NeedSocketCb onNeedSocket_;
     384              :     NeedSocketCb onNeedSwarmSocket_;
     385              :     OneToOneRecvCb oneToOneRecvCb_;
     386              : 
     387              :     std::string deviceId_ {};
     388              :     std::string username_ {};
     389              : 
     390              :     // Requests
     391              :     mutable std::mutex conversationsRequestsMtx_;
     392              :     std::map<std::string, ConversationRequest> conversationsRequests_;
     393              : 
     394              :     // Conversations
     395              :     mutable std::mutex conversationsMtx_ {};
     396              :     std::map<std::string, std::shared_ptr<SyncedConversation>, std::less<>> conversations_;
     397              : 
     398              :     // The following information are stored on the disk
     399              :     mutable std::mutex convInfosMtx_; // Note, should be locked after conversationsMtx_ if needed
     400              :     std::map<std::string, ConvInfo> convInfos_;
     401              : 
     402              :     // When sending a new message, we need to send the notification to some peers of the
     403              :     // conversation However, the conversation may be not bootstrapped, so the list will be empty.
     404              :     // notSyncedNotification_ will store the notifiaction to announce until we have peers to sync
     405              :     // with.
     406              :     std::mutex notSyncedNotificationMtx_;
     407              :     std::map<std::string, std::string> notSyncedNotification_;
     408              : 
     409         3528 :     std::weak_ptr<Impl> weak() { return std::static_pointer_cast<Impl>(shared_from_this()); }
     410              : 
     411              :     std::mutex refreshMtx_;
     412              :     std::map<std::string, uint64_t> refreshMessage;
     413              :     std::atomic_int syncCnt {0};
     414              : 
     415              : #ifdef LIBJAMI_TEST
     416              :     std::function<void(std::string, Conversation::BootstrapStatus)> bootstrapCbTest_;
     417              : #endif
     418              : 
     419              :     uint64_t presenceListenerToken_ {0};
     420              :     void onBuddyOnline(const std::string& uri);
     421              : 
     422          689 :     ~Impl()
     423          689 :     {
     424          689 :         if (auto acc = account_.lock()) {
     425            4 :             if (auto pm = acc->presenceManager()) {
     426            4 :                 if (presenceListenerToken_)
     427            0 :                     pm->removeListener(presenceListenerToken_);
     428              :             }
     429          689 :         }
     430          689 :     }
     431              : 
     432              :     void fixStructures(std::shared_ptr<JamiAccount> account,
     433              :                        const std::vector<std::tuple<std::string, std::string, std::string>>& updateContactConv,
     434              :                        const std::set<std::string>& toRm);
     435              : 
     436              :     void cloneConversationFrom(const std::shared_ptr<SyncedConversation> conv, const std::string& deviceId);
     437              :     void bootstrap(const std::string& convId);
     438              :     void fallbackClone(const asio::error_code& ec, const std::string& conversationId);
     439              : 
     440              :     void cloneConversationFrom(const ConversationRequest& request);
     441              : 
     442              :     void cloneConversationFrom(const std::string& conversationId, const std::string& uri);
     443              : 
     444              :     // While syncing, we do not want to lose metadata (avatar/title and mode)
     445              :     std::map<std::string, std::map<std::string, std::string>> syncingMetadatas_;
     446          467 :     void saveMetadata()
     447              :     {
     448          467 :         auto path = fileutils::get_data_dir() / accountId_;
     449          467 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "syncingMetadatas"));
     450          467 :         std::ofstream file(path / "syncingMetadatas", std::ios::trunc | std::ios::binary);
     451          467 :         msgpack::pack(file, syncingMetadatas_);
     452          467 :     }
     453              : 
     454          689 :     void loadMetadata()
     455              :     {
     456              :         try {
     457              :             // read file
     458          689 :             auto path = fileutils::get_data_dir() / accountId_;
     459          689 :             std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "syncingMetadatas"));
     460         1378 :             auto file = fileutils::loadFile("syncingMetadatas", path);
     461              :             // load values
     462            0 :             msgpack::unpacked result;
     463            0 :             msgpack::unpack(result, (const char*) file.data(), file.size(), 0);
     464            0 :             result.get().convert(syncingMetadatas_);
     465         2067 :         } catch (const std::exception& e) {
     466         2756 :             JAMI_WARNING("[Account {}] [ConversationModule] unable to load syncing metadata: {}", accountId_, e.what());
     467          689 :         }
     468          689 :     }
     469              : 
     470           16 :     void initPresence()
     471              :     {
     472           16 :         if (!presenceListenerToken_)
     473           15 :             if (auto acc = account_.lock())
     474           30 :                 presenceListenerToken_ = acc->presenceManager()->addListener(
     475           30 :                     [w = weak_from_this()](const std::string& uri, bool online) {
     476            4 :                         if (!online)
     477            0 :                             return;
     478            4 :                         if (auto sthis = w.lock())
     479            4 :                             sthis->onBuddyOnline(uri);
     480           15 :                     });
     481           16 :     }
     482              : };
     483              : 
     484          689 : ConversationModule::Impl::Impl(std::shared_ptr<JamiAccount>&& account,
     485              :                                std::shared_ptr<AccountManager>&& accountManager,
     486              :                                NeedsSyncingCb&& needsSyncingCb,
     487              :                                SengMsgCb&& sendMsgCb,
     488              :                                NeedSocketCb&& onNeedSocket,
     489              :                                NeedSocketCb&& onNeedSwarmSocket,
     490          689 :                                OneToOneRecvCb&& oneToOneRecvCb)
     491          689 :     : account_(account)
     492          689 :     , accountManager_(accountManager)
     493          689 :     , accountId_(account->getAccountID())
     494          689 :     , needsSyncingCb_(needsSyncingCb)
     495          689 :     , sendMsgCb_(sendMsgCb)
     496          689 :     , onNeedSocket_(onNeedSocket)
     497          689 :     , onNeedSwarmSocket_(onNeedSwarmSocket)
     498         1378 :     , oneToOneRecvCb_(oneToOneRecvCb)
     499              : {
     500          689 :     if (auto accm = account->accountManager())
     501          689 :         if (const auto* info = accm->getInfo()) {
     502          689 :             deviceId_ = info->deviceId;
     503          689 :             username_ = info->accountId;
     504          689 :         }
     505          689 :     conversationsRequests_ = convRequests(accountId_);
     506          689 :     loadMetadata();
     507          689 : }
     508              : 
     509              : void
     510           54 : ConversationModule::Impl::cloneConversation(const std::string& deviceId,
     511              :                                             const std::string& peerUri,
     512              :                                             const std::string& convId)
     513              : {
     514          216 :     JAMI_DEBUG("[Account {}] [Conversation {}] [device {}] Cloning conversation", accountId_, convId, deviceId);
     515              : 
     516           54 :     auto conv = startConversation(convId);
     517           54 :     std::unique_lock lk(conv->mtx);
     518           54 :     cloneConversation(deviceId, peerUri, conv);
     519           54 : }
     520              : 
     521              : void
     522          118 : ConversationModule::Impl::cloneConversation(const std::string& deviceId,
     523              :                                             const std::string& peerUri,
     524              :                                             const std::shared_ptr<SyncedConversation>& conv)
     525              : {
     526              :     // conv->mtx must be locked
     527          118 :     if (conv->isUnrecoverable()) {
     528            0 :         JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Conversation is marked unrecoverable, "
     529              :                      "ignoring clone request",
     530              :                      accountId_,
     531              :                      conv->info.id,
     532              :                      deviceId);
     533            0 :         return;
     534              :     }
     535          118 :     if (!conv->conversation) {
     536              :         // Note: here we don't return and connect to all members
     537              :         // the first that will successfully connect will be used for
     538              :         // cloning.
     539              :         // This avoid the case when we try to clone from convInfos + sync message
     540              :         // at the same time.
     541          118 :         if (!conv->startFetch(deviceId, true)) {
     542          256 :             JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Already fetching conversation",
     543              :                          accountId_,
     544              :                          conv->info.id,
     545              :                          deviceId);
     546           64 :             addConvInfo(conv->info);
     547           64 :             return;
     548              :         }
     549          162 :         onNeedSocket_(
     550           54 :             conv->info.id,
     551              :             deviceId,
     552          108 :             [w = weak(), conv, deviceId](const auto& channel) {
     553           54 :                 std::lock_guard lk(conv->mtx);
     554           54 :                 if (conv->pending && !conv->pending->ready) {
     555           54 :                     if (channel) {
     556           54 :                         conv->pending->ready = true;
     557           54 :                         conv->pending->deviceId = channel->deviceId().toString();
     558           54 :                         conv->pending->socket = channel;
     559           54 :                         if (!conv->pending->cloning) {
     560           54 :                             conv->pending->cloning = true;
     561          108 :                             dht::ThreadPool::io().run([w, convId = conv->info.id, deviceId = conv->pending->deviceId]() {
     562          108 :                                 if (auto sthis = w.lock())
     563           54 :                                     sthis->handlePendingConversation(convId, deviceId);
     564              :                             });
     565              :                         }
     566           54 :                         return true;
     567              :                     } else {
     568            0 :                         conv->stopFetch(deviceId);
     569              :                     }
     570              :                 }
     571            0 :                 return false;
     572           54 :             },
     573              :             MIME_TYPE_GIT,
     574              :             false);
     575              : 
     576          216 :         JAMI_LOG("[Account {}] [Conversation {}] [device {}] Requesting device", accountId_, conv->info.id, deviceId);
     577           54 :         conv->info.members.emplace(username_);
     578           54 :         conv->info.members.emplace(peerUri);
     579           54 :         addConvInfo(conv->info);
     580              :     } else {
     581            0 :         JAMI_DEBUG("[Account {}] [Conversation {}] Conversation already cloned", accountId_, conv->info.id);
     582              :     }
     583              : }
     584              : 
     585              : void
     586        16401 : ConversationModule::Impl::fetchNewCommits(const std::string& peer,
     587              :                                           const std::string& deviceId,
     588              :                                           const std::string& conversationId,
     589              :                                           const std::string& commitId)
     590              : {
     591        16401 :     auto conv = getConversation(conversationId);
     592              :     {
     593        16402 :         bool needReclone = false;
     594        16402 :         std::unique_lock lkInfos(convInfosMtx_);
     595              : 
     596        16400 :         auto itConvInfo = convInfos_.find(conversationId);
     597        16403 :         if (itConvInfo != convInfos_.end() && itConvInfo->second.isRemoved()) {
     598            3 :             if (!conv)
     599            0 :                 return;
     600              : 
     601            3 :             const bool isOneToOne = (itConvInfo->second.mode == ConversationMode::ONE_TO_ONE);
     602            3 :             auto contactInfo = accountManager_->getContactInfo(peer);
     603            3 :             const bool shouldReadd = isOneToOne && contactInfo && contactInfo->confirmed && contactInfo->isActive()
     604            0 :                                      && !contactInfo->isBanned() && contactInfo->added > itConvInfo->second.removed
     605            6 :                                      && contactInfo->conversationId != conversationId;
     606              : 
     607            3 :             if (shouldReadd) {
     608            0 :                 if (conv) {
     609            0 :                     std::unique_lock lkSynced(conv->mtx);
     610              : 
     611            0 :                     if (!conv->conversation) {
     612            0 :                         conv->info.created = nowMs();
     613            0 :                         conv->info.erased = TimePoint {};
     614            0 :                         convInfos_[conversationId] = conv->info;
     615            0 :                         saveConvInfos();
     616            0 :                         needReclone = true;
     617              :                     }
     618            0 :                 }
     619              : 
     620            0 :                 lkInfos.unlock();
     621              : 
     622            0 :                 if (needReclone && conv) {
     623              :                     {
     624            0 :                         std::unique_lock lkSynced(conv->mtx);
     625            0 :                         cloneConversation(deviceId, peer, conv);
     626            0 :                     }
     627              :                 }
     628              : 
     629            0 :                 return;
     630              :             }
     631              : 
     632           12 :             JAMI_WARNING("[Account {:s}] [Conversation {}] Received a commit, but conversation is removed",
     633              :                          accountId_,
     634              :                          conversationId);
     635            3 :             return;
     636            3 :         }
     637        16402 :     }
     638        16400 :     std::optional<ConversationRequest> oldReq;
     639              :     {
     640        16400 :         std::lock_guard lk(conversationsRequestsMtx_);
     641        16399 :         oldReq = getRequest(conversationId);
     642        16400 :         if (oldReq != std::nullopt && oldReq->declined != TimePoint {}) {
     643            0 :             JAMI_DEBUG("[Account {}] [Conversation {}] Received a request for a conversation already declined.",
     644              :                        accountId_,
     645              :                        conversationId);
     646            0 :             return;
     647              :         }
     648        16400 :     }
     649        65589 :     JAMI_DEBUG("[Account {:s}] [Conversation {}] [device {}] fetching '{:s}'",
     650              :                accountId_,
     651              :                conversationId,
     652              :                deviceId,
     653              :                commitId);
     654              : 
     655        16401 :     const bool shouldRequestInvite = username_ != peer;
     656        16401 :     if (!conv) {
     657          134 :         if (oldReq == std::nullopt && shouldRequestInvite) {
     658              :             // We didn't find a conversation or a request with the given ID.
     659              :             // This suggests that someone tried to send us an invitation but
     660              :             // that we didn't receive it, so we ask for a new one.
     661          524 :             JAMI_WARNING("[Account {}] [Conversation {}] Unable to find conversation, asking for an invite",
     662              :                          accountId_,
     663              :                          conversationId);
     664          393 :             sendMsgCb_(peer, {}, std::map<std::string, std::string> {{MIME_TYPE_INVITE, conversationId}}, 0);
     665              :         }
     666          134 :         return;
     667              :     }
     668        16267 :     std::unique_lock lk(conv->mtx);
     669              : 
     670        16267 :     if (conv->conversation) {
     671              :         // Check if we already have the commit
     672        16194 :         if (not commitId.empty() && conv->conversation->hasCommit(commitId)) {
     673        14342 :             return;
     674              :         }
     675         1907 :         if (conv->conversation->isRemoving()) {
     676            0 :             JAMI_WARNING("[Account {}] [Conversation {}] conversaton is being removed", accountId_, conversationId);
     677            0 :             return;
     678              :         }
     679         1906 :         if (!conv->conversation->isPeerAuthorized(peer, deviceId, true)) {
     680           12 :             JAMI_WARNING("[Account {}] [Conversation {}] device {} is not authorized for {}",
     681              :                          accountId_,
     682              :                          conversationId,
     683              :                          deviceId,
     684              :                          peer);
     685            3 :             return;
     686              :         }
     687              : 
     688              :         // Retrieve current last message
     689         1903 :         auto lastMessageId = conv->conversation->lastCommitId();
     690         1903 :         if (lastMessageId.empty()) {
     691            4 :             JAMI_ERROR("[Account {}] [Conversation {}] No message detected. This is a bug", accountId_, conversationId);
     692            1 :             return;
     693              :         }
     694              : 
     695         1902 :         if (!conv->startFetch(deviceId)) {
     696          216 :             JAMI_WARNING("[Account {}] [Conversation {}] Already fetching", accountId_, conversationId);
     697           54 :             return;
     698              :         }
     699              : 
     700         1847 :         syncCnt.fetch_add(1);
     701         5542 :         onNeedSocket_(
     702              :             conversationId,
     703              :             deviceId,
     704         3696 :             [w = weak(), conv, conversationId, peer = std::move(peer), deviceId, commitId = std::move(commitId)](
     705              :                 const auto& channel) {
     706         1847 :                 auto sthis = w.lock();
     707         1847 :                 auto acc = sthis ? sthis->account_.lock() : nullptr;
     708         1847 :                 std::unique_lock lk(conv->mtx);
     709         1847 :                 auto conversation = conv->conversation;
     710         1847 :                 if (!channel || !acc || !conversation) {
     711           21 :                     conv->stopFetch(deviceId);
     712           21 :                     if (sthis)
     713           21 :                         sthis->syncCnt.fetch_sub(1);
     714           21 :                     return false;
     715              :                 }
     716         1826 :                 conversation->addGitSocket(channel->deviceId(), channel);
     717         1826 :                 lk.unlock();
     718         3652 :                 conversation->sync(
     719         1826 :                     peer,
     720         1826 :                     deviceId,
     721         3652 :                     [w, conv, conversationId = std::move(conversationId), peer, deviceId, commitId](bool ok) {
     722         1826 :                         auto shared = w.lock();
     723         1826 :                         if (!shared)
     724            0 :                             return;
     725         1826 :                         if (!ok) {
     726         1196 :                             JAMI_WARNING("[Account {}] [Conversation {}] Unable to fetch new commit from "
     727              :                                          "{}, other peer may be disconnected",
     728              :                                          shared->accountId_,
     729              :                                          conversationId,
     730              :                                          deviceId);
     731         1196 :                             JAMI_LOG("[Account {}] [Conversation {}] Relaunch sync with {}",
     732              :                                      shared->accountId_,
     733              :                                      conversationId,
     734              :                                      deviceId);
     735              :                         }
     736              : 
     737              :                         {
     738         1826 :                             std::lock_guard lk(conv->mtx);
     739         1826 :                             conv->pending.reset();
     740              :                             // Notify peers that a new commit is there (DRT)
     741         1826 :                             if (not commitId.empty() && ok) {
     742          950 :                                 shared->sendMessageNotification(*conv->conversation, false, commitId, deviceId);
     743              :                             }
     744         1826 :                         }
     745         3652 :                         if (shared->syncCnt.fetch_sub(1) == 1) {
     746         1438 :                             emitSignal<libjami::ConversationSignal::ConversationSyncFinished>(shared->accountId_);
     747              :                         }
     748         1826 :                     },
     749         1826 :                     commitId);
     750         1825 :                 return true;
     751         1846 :             },
     752              :             "",
     753              :             false);
     754         1903 :     } else {
     755           72 :         if (oldReq != std::nullopt)
     756            0 :             return;
     757           72 :         if (conv->pending)
     758           55 :             return;
     759           17 :         bool clone = !conv->info.isRemoved();
     760           17 :         if (clone) {
     761           17 :             cloneConversation(deviceId, peer, conv);
     762           17 :             return;
     763              :         }
     764            0 :         if (!shouldRequestInvite)
     765            0 :             return;
     766            0 :         lk.unlock();
     767            0 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to find conversation, asking for an invite",
     768              :                      accountId_,
     769              :                      conversationId);
     770            0 :         sendMsgCb_(peer, {}, std::map<std::string, std::string> {{MIME_TYPE_INVITE, conversationId}}, 0);
     771              :     }
     772        45489 : }
     773              : 
     774              : void
     775            4 : ConversationModule::Impl::onBuddyOnline(const std::string& uri)
     776              : {
     777            4 :     auto acc = account_.lock();
     778            4 :     if (!acc)
     779            0 :         return;
     780              : 
     781            4 :     auto toUpdate = std::make_shared<std::vector<std::shared_ptr<Conversation>>>();
     782            8 :     for (auto& conv : getConversations()) {
     783            4 :         if (conv->isMember(uri)) {
     784            3 :             toUpdate->emplace_back(std::move(conv));
     785              :         }
     786            4 :     }
     787              : 
     788            4 :     if (auto pm = acc->presenceManager()) {
     789            4 :         auto devices = pm->getDevices(uri);
     790            7 :         for (const auto& conv : *toUpdate)
     791            3 :             conv->addKnownDevices(devices, uri);
     792            4 :     }
     793            4 : }
     794              : 
     795              : // Clone and store conversation
     796              : void
     797          196 : ConversationModule::Impl::handlePendingConversation(const std::string& conversationId, const std::string& deviceId)
     798              : {
     799          196 :     auto acc = account_.lock();
     800          196 :     if (!acc)
     801            0 :         return;
     802          196 :     auto conv = getConversation(conversationId);
     803          196 :     if (!conv)
     804            0 :         return;
     805          196 :     std::unique_lock lk(conv->mtx, std::defer_lock);
     806          379 :     auto erasePending = [&] {
     807          379 :         conv->pending.reset();
     808          379 :         lk.unlock();
     809          575 :     };
     810              :     try {
     811          196 :         auto conversation = std::make_shared<Conversation>(acc, deviceId, conversationId);
     812          183 :         conversation->onMembersChanged([w = weak_from_this(), conversationId](const auto& members) {
     813              :             // Delay in another thread to avoid deadlocks
     814         2827 :             dht::ThreadPool::io().run([w, conversationId, members = std::move(members)] {
     815         2828 :                 if (auto sthis = w.lock())
     816         1414 :                     sthis->setConversationMembers(conversationId, members);
     817              :             });
     818         1414 :         });
     819          183 :         conversation->onMessageStatusChanged([this, conversationId](const auto& status) {
     820          300 :             auto msg = std::make_shared<SyncMsg>();
     821          600 :             msg->ms = {{conversationId, status}};
     822          300 :             needsSyncingCb_(std::move(msg));
     823          600 :         });
     824          183 :         conversation->onNeedSocket(onNeedSwarmSocket_);
     825          183 :         if (!conversation->isMember(username_, true)) {
     826            0 :             JAMI_ERROR("[Account {}] [Conversation {}] Conversation cloned but we do not seem to be a valid member",
     827              :                        accountId_,
     828              :                        conversationId);
     829            0 :             conversation->erase();
     830            0 :             lk.lock();
     831            0 :             erasePending();
     832            0 :             return;
     833              :         }
     834              : 
     835              :         // Make sure that the list of members stored in convInfos_ matches the
     836              :         // one from the conversation's repository.
     837              :         // (https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/1026)
     838          183 :         setConversationMembers(conversationId, conversation->memberUris("", {}));
     839              : 
     840          183 :         lk.lock();
     841          183 :         if (conv->info.mode != conversation->mode()) {
     842           52 :             JAMI_ERROR(
     843              :                 "[Account {}] [Conversation {}] Cloned conversation mode is {}, but {} was expected from invite.",
     844              :                 accountId_,
     845              :                 conversationId,
     846              :                 static_cast<int>(conversation->mode()),
     847              :                 static_cast<int>(conv->info.mode));
     848              :             // TODO: erase conversation after transition period
     849              :             // conversation->erase();
     850              :             // erasePending();
     851              :             // return;
     852           13 :             conv->info.mode = conversation->mode();
     853           13 :             addConvInfo(conv->info);
     854              :         }
     855              : 
     856          183 :         if (conv->pending && conv->pending->socket)
     857          183 :             conversation->addGitSocket(DeviceId(deviceId), std::move(conv->pending->socket));
     858          183 :         auto removeRepo = false;
     859              :         // Note: a removeContact while cloning. In this case, the conversation
     860              :         // must not be announced and removed.
     861          183 :         if (conv->info.isRemoved())
     862            0 :             removeRepo = true;
     863          183 :         std::map<std::string, std::string> preferences;
     864          183 :         std::map<std::string, std::map<std::string, std::string>> status;
     865          183 :         if (conv->pending) {
     866          183 :             preferences = std::move(conv->pending->preferences);
     867          183 :             status = std::move(conv->pending->status);
     868              :         }
     869          183 :         conv->conversation = conversation;
     870          183 :         if (removeRepo) {
     871            0 :             removeRepositoryImpl(*conv, false, true);
     872            0 :             erasePending();
     873            0 :             return;
     874              :         }
     875              : 
     876          183 :         auto commitId = conversation->join();
     877          183 :         if (!commitId.empty())
     878          304 :             sendMessageNotification(*conversation, false, commitId);
     879          183 :         erasePending(); // Will unlock
     880              : 
     881              : #ifdef LIBJAMI_TEST
     882          183 :         conversation->onBootstrapStatus(bootstrapCbTest_);
     883              : #endif
     884          183 :         auto id = conversation->id();
     885          183 :         conversation->bootstrap([w = weak(), id = std::move(id)]() {
     886          304 :             if (auto sthis = w.lock())
     887          304 :                 sthis->bootstrapCb(id);
     888          304 :         });
     889              : 
     890          183 :         if (auto pm = acc->presenceManager()) {
     891         1213 :             for (const auto& member : conversation->memberUris()) {
     892          664 :                 conversation->addKnownDevices(pm->getDevices(member), member);
     893          183 :             }
     894              :         }
     895              : 
     896          183 :         if (!preferences.empty())
     897            1 :             conversation->updatePreferences(preferences);
     898          183 :         if (!status.empty())
     899           14 :             conversation->updateMessageStatus(status);
     900          183 :         syncingMetadatas_.erase(conversationId);
     901          183 :         saveMetadata();
     902              : 
     903              :         // Inform user that the conversation is ready
     904          183 :         emitSignal<libjami::ConversationSignal::ConversationReady>(accountId_, conversationId);
     905          183 :         needsSyncingCb_({});
     906              :         // Download members profile on first sync
     907          183 :         auto isOneOne = conversation->mode() == ConversationMode::ONE_TO_ONE;
     908          183 :         auto askForProfile = isOneOne;
     909          183 :         if (!isOneOne) {
     910              :             // If not 1:1 only download profiles from self (to avoid non checked files)
     911          124 :             auto cert = acc->certStore().getCertificate(deviceId);
     912          124 :             askForProfile = cert && cert->issuer && cert->issuer->getId().toString() == username_;
     913          124 :         }
     914          183 :         if (askForProfile) {
     915          273 :             for (const auto& member : conversation->memberUris(username_)) {
     916           57 :                 acc->askForProfile(conversationId, deviceId, member);
     917           72 :             }
     918              :         }
     919          196 :     } catch (const InvalidRepositoryError&) {
     920              :         // Permanent failure: the remote repository contains malformed commits and
     921              :         // its history is immutable, so every re-clone would fail the same way.
     922              :         // Stop after a few attempts instead of retrying (and waking the peer's
     923              :         // devices through the DHT proxy) indefinitely.
     924            3 :         ++conv->validationFailures;
     925            3 :         if (conv->isUnrecoverable()) {
     926            0 :             JAMI_ERROR("[Account {}] [Conversation {}] Failed validation {} times, marking conversation "
     927              :                        "as unrecoverable: remove it and ask the peer for a new invitation",
     928              :                        accountId_,
     929              :                        conversationId,
     930              :                        conv->validationFailures);
     931            0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(
     932            0 :                 accountId_, conversationId, EUNRECOVERABLE, "Conversation repository failed validation repeatedly");
     933              :         } else {
     934           12 :             JAMI_WARNING(
     935              :                 "[Account {}] [Conversation {}] Remote conversation failed validation ({}/{}). Re-clone in {}s",
     936              :                 accountId_,
     937              :                 conversationId,
     938              :                 conv->validationFailures,
     939              :                 MAX_VALIDATION_FAILURES,
     940              :                 conv->fallbackTimer.count());
     941            3 :             conv->fallbackClone->expires_at(std::chrono::steady_clock::now() + conv->fallbackTimer);
     942            3 :             conv->fallbackTimer *= 2;
     943            3 :             if (conv->fallbackTimer > MAX_FALLBACK)
     944            0 :                 conv->fallbackTimer = MAX_FALLBACK;
     945            6 :             conv->fallbackClone->async_wait(std::bind(&ConversationModule::Impl::fallbackClone,
     946            6 :                                                       shared_from_this(),
     947              :                                                       std::placeholders::_1,
     948              :                                                       conversationId));
     949              :         }
     950           13 :     } catch (const std::exception& e) {
     951           40 :         JAMI_WARNING(
     952              :             "[Account {}] [Conversation {}] Something went wrong when cloning conversation: {}. Re-clone in {}s",
     953              :             accountId_,
     954              :             conversationId,
     955              :             e.what(),
     956              :             conv->fallbackTimer.count());
     957           10 :         conv->fallbackClone->expires_at(std::chrono::steady_clock::now() + conv->fallbackTimer);
     958           10 :         conv->fallbackTimer *= 2;
     959           10 :         if (conv->fallbackTimer > MAX_FALLBACK)
     960            0 :             conv->fallbackTimer = MAX_FALLBACK;
     961           20 :         conv->fallbackClone->async_wait(std::bind(&ConversationModule::Impl::fallbackClone,
     962           20 :                                                   shared_from_this(),
     963              :                                                   std::placeholders::_1,
     964              :                                                   conversationId));
     965           10 :     }
     966          196 :     lk.lock();
     967          196 :     erasePending();
     968          196 : }
     969              : 
     970              : std::optional<ConversationRequest>
     971        16763 : ConversationModule::Impl::getRequest(const std::string& id) const
     972              : {
     973              :     // ConversationsRequestsMtx MUST BE LOCKED
     974        16763 :     auto it = conversationsRequests_.find(id);
     975        16765 :     if (it != conversationsRequests_.end())
     976          191 :         return it->second;
     977        16573 :     return std::nullopt;
     978              : }
     979              : 
     980              : std::string
     981          589 : ConversationModule::Impl::getOneToOneConversation(const std::string& uri) const noexcept
     982              : {
     983          589 :     if (auto details = accountManager_->getContactInfo(uri)) {
     984              :         // If contact is removed there is no conversation
     985              :         // If banned, conversation is still on disk
     986          347 :         if (details->removed != TimePoint {} && details->banned == 0) {
     987              :             // Check if contact is removed
     988           24 :             if (details->removed > details->added)
     989           24 :                 return {};
     990              :         }
     991          323 :         return details->conversationId;
     992          589 :     }
     993          242 :     return {};
     994              : }
     995              : 
     996              : bool
     997            9 : ConversationModule::Impl::updateConvForContact(const std::string& uri,
     998              :                                                const std::string& oldConv,
     999              :                                                const std::string& newConv)
    1000              : {
    1001            9 :     if (newConv != oldConv) {
    1002            9 :         auto conversation = getOneToOneConversation(uri);
    1003            9 :         if (conversation != oldConv) {
    1004            0 :             JAMI_DEBUG("[Account {}] [Conversation {}] Old conversation is not found in details {} - found: {}",
    1005              :                        accountId_,
    1006              :                        newConv,
    1007              :                        oldConv,
    1008              :                        conversation);
    1009            0 :             return false;
    1010              :         }
    1011            9 :         accountManager_->updateContactConversation(uri, newConv);
    1012            9 :         return true;
    1013            9 :     }
    1014            0 :     return false;
    1015              : }
    1016              : 
    1017              : void
    1018           68 : ConversationModule::Impl::declineOtherConversationWith(const std::string& uri)
    1019              : {
    1020              :     // conversationsRequestsMtx_ MUST BE LOCKED
    1021           69 :     for (auto& [id, request] : conversationsRequests_) {
    1022            1 :         if (request.declined != TimePoint {})
    1023            0 :             continue; // Ignore already declined requests
    1024            1 :         if (request.isOneToOne() && request.from == uri) {
    1025            4 :             JAMI_WARNING("[Account {}] [Conversation {}] Decline conversation request from {}", accountId_, id, uri);
    1026            1 :             request.declined = nowMs();
    1027            1 :             syncingMetadatas_.erase(id);
    1028            1 :             saveMetadata();
    1029            1 :             emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(accountId_, id);
    1030              :         }
    1031              :     }
    1032           68 : }
    1033              : 
    1034              : std::vector<std::map<std::string, std::string>>
    1035          120 : ConversationModule::Impl::getConversationMembers(const std::string& conversationId, bool includeBanned) const
    1036              : {
    1037          360 :     return withConv(conversationId, [&](const auto& conv) { return conv.getMembers(true, includeBanned); });
    1038              : }
    1039              : 
    1040              : void
    1041           13 : ConversationModule::Impl::removeRepository(const std::string& conversationId, bool sync, bool force)
    1042              : {
    1043           13 :     auto conv = getConversation(conversationId);
    1044           13 :     if (!conv)
    1045            0 :         return;
    1046           13 :     std::unique_lock lk(conv->mtx);
    1047           13 :     removeRepositoryImpl(*conv, sync, force);
    1048           13 : }
    1049              : 
    1050              : void
    1051           21 : ConversationModule::Impl::removeRepositoryImpl(SyncedConversation& conv, bool sync, bool force)
    1052              : {
    1053           21 :     if (conv.conversation && (force || conv.conversation->isRemoving())) {
    1054              :         // Stop fetch!
    1055           21 :         conv.pending.reset();
    1056              : 
    1057           84 :         JAMI_LOG("[Account {}] [Conversation {}] Remove conversation", accountId_, conv.info.id);
    1058              :         try {
    1059           21 :             if (conv.conversation->mode() == ConversationMode::ONE_TO_ONE) {
    1060           42 :                 for (const auto& member : conv.conversation->getInitialMembers()) {
    1061           28 :                     if (member != username_) {
    1062              :                         // Note: this can happen while re-adding a contact.
    1063              :                         // In this case, check that we are removing the linked conversation.
    1064           14 :                         if (conv.info.id == getOneToOneConversation(member)) {
    1065            0 :                             accountManager_->removeContactConversation(member);
    1066              :                         }
    1067              :                     }
    1068           14 :                 }
    1069              :             }
    1070            0 :         } catch (const std::exception& e) {
    1071            0 :             JAMI_ERROR("{}", e.what());
    1072            0 :         }
    1073           21 :         conv.conversation->erase();
    1074           21 :         conv.conversation.reset();
    1075              : 
    1076           21 :         if (!sync)
    1077            1 :             return;
    1078              : 
    1079           20 :         conv.info.erased = nowMs();
    1080           20 :         needsSyncingCb_({});
    1081           20 :         addConvInfo(conv.info);
    1082              :     }
    1083              : }
    1084              : 
    1085              : bool
    1086            9 : ConversationModule::Impl::removeConversation(const std::string& conversationId, bool forceRemove)
    1087              : {
    1088           18 :     return withConv(conversationId,
    1089           18 :                     [this, forceRemove](auto& conv) { return removeConversationImpl(conv, forceRemove); });
    1090              : }
    1091              : 
    1092              : bool
    1093            9 : ConversationModule::Impl::removeConversationImpl(SyncedConversation& conv, bool forceRemove)
    1094              : {
    1095            9 :     auto members = conv.getMembers(false, false);
    1096            9 :     auto isSyncing = !conv.conversation;
    1097            9 :     auto hasMembers = !isSyncing // If syncing there is no member to inform
    1098            8 :                       && std::find_if(members.begin(),
    1099              :                                       members.end(),
    1100           24 :                                       [&](const auto& member) { return member.at("uri") == username_; })
    1101           16 :                              != members.end() // We must be still a member
    1102           17 :                       && members.size() != 1; // If there is only ourself
    1103            9 :     conv.info.removed = nowMs();
    1104            9 :     if (isSyncing)
    1105            1 :         conv.info.erased = nowMs();
    1106            9 :     if (conv.fallbackClone)
    1107            9 :         conv.fallbackClone->cancel();
    1108              :     // Sync now, because it can take some time to really removes the datas
    1109            9 :     needsSyncingCb_({});
    1110            9 :     addConvInfo(conv.info);
    1111            9 :     emitSignal<libjami::ConversationSignal::ConversationRemoved>(accountId_, conv.info.id);
    1112            9 :     if (isSyncing)
    1113            1 :         return true;
    1114              : 
    1115            8 :     if (forceRemove && conv.conversation->mode() == ConversationMode::ONE_TO_ONE) {
    1116              :         // skip waiting for sync
    1117            1 :         removeRepositoryImpl(conv, true);
    1118            1 :         return true;
    1119              :     }
    1120              : 
    1121            7 :     auto commitId = conv.conversation->leave();
    1122            7 :     if (hasMembers) {
    1123            8 :         JAMI_LOG("Wait that someone sync that user left conversation {}", conv.info.id);
    1124              :         // Commit that we left
    1125            2 :         if (!commitId.empty()) {
    1126              :             // Do not sync as it's synched by convInfos
    1127            4 :             sendMessageNotification(*conv.conversation, false, commitId);
    1128              :         } else {
    1129            0 :             JAMI_ERROR("Failed to send message to conversation {}", conv.info.id);
    1130              :         }
    1131              :         // In this case, we wait that another peer sync the conversation
    1132              :         // to definitely remove it from the device. This is to inform the
    1133              :         // peer that we left the conversation and never want to receive
    1134              :         // any messages
    1135            2 :         return true;
    1136              :     }
    1137              : 
    1138              :     // Else we are the last member, so we can remove
    1139            5 :     removeRepositoryImpl(conv, true);
    1140            5 :     return true;
    1141            9 : }
    1142              : 
    1143              : void
    1144          531 : ConversationModule::Impl::sendMessageNotification(const std::string& conversationId,
    1145              :                                                   bool sync,
    1146              :                                                   const std::string& commitId,
    1147              :                                                   const std::string& deviceId)
    1148              : {
    1149          531 :     if (auto conv = getConversation(conversationId)) {
    1150          531 :         std::lock_guard lk(conv->mtx);
    1151          531 :         if (conv->conversation)
    1152          529 :             sendMessageNotification(*conv->conversation, sync, commitId, deviceId);
    1153         1062 :     }
    1154          531 : }
    1155              : 
    1156              : void
    1157         1769 : ConversationModule::Impl::sendMessageNotification(Conversation& conversation,
    1158              :                                                   bool sync,
    1159              :                                                   const std::string& commitId,
    1160              :                                                   const std::string& deviceId)
    1161              : {
    1162         1769 :     auto acc = account_.lock();
    1163         1769 :     if (!acc)
    1164            0 :         return;
    1165         1769 :     auto commit = commitId == "" ? conversation.lastCommitId() : commitId;
    1166         1769 :     Json::Value message;
    1167         1769 :     message["id"] = conversation.id();
    1168         1769 :     message["commit"] = commit;
    1169         1769 :     message["deviceId"] = deviceId_;
    1170         1769 :     const auto text = json::toString(message);
    1171              : 
    1172              :     // Send message notification will announce the new commit in 3 steps.
    1173         5307 :     const auto messageMap = std::map<std::string, std::string> {{MIME_TYPE_GIT, text}};
    1174              : 
    1175              :     // First, because our account can have several devices, announce to other devices
    1176         1769 :     if (sync) {
    1177              :         // Announce to our devices
    1178          665 :         std::lock_guard lk(refreshMtx_);
    1179          665 :         auto& refresh = refreshMessage[username_];
    1180          665 :         refresh = sendMsgCb_(username_, {}, messageMap, refresh);
    1181          665 :     }
    1182              : 
    1183              :     // Then, we announce to 2 random members in the conversation that aren't in the DRT
    1184              :     // This allow new devices without the ability to sync to their other devices to sync with us.
    1185              :     // Or they can also use an old backup.
    1186         1769 :     std::vector<std::string> nonConnectedMembers;
    1187         1769 :     std::vector<NodeId> devices;
    1188              :     {
    1189         1769 :         std::lock_guard lk(notSyncedNotificationMtx_);
    1190         1769 :         devices = conversation.peersToSyncWith();
    1191         3534 :         auto members = conversation.memberUris(username_, {MemberRole::BANNED});
    1192         1769 :         std::vector<std::string> connectedMembers;
    1193              :         // print all members
    1194        17959 :         for (const auto& device : devices) {
    1195        16179 :             auto cert = acc->certStore().getCertificate(device.toString());
    1196        16177 :             if (cert && cert->issuer)
    1197        16178 :                 connectedMembers.emplace_back(cert->issuer->getId().toString());
    1198        16163 :         }
    1199         1768 :         std::sort(std::begin(connectedMembers), std::end(connectedMembers));
    1200         1769 :         std::set_difference(members.begin(),
    1201              :                             members.end(),
    1202              :                             connectedMembers.begin(),
    1203              :                             connectedMembers.end(),
    1204              :                             std::inserter(nonConnectedMembers, nonConnectedMembers.begin()));
    1205         1769 :         std::shuffle(nonConnectedMembers.begin(), nonConnectedMembers.end(), acc->rand);
    1206         1769 :         if (nonConnectedMembers.size() > 2)
    1207          114 :             nonConnectedMembers.resize(2);
    1208         1769 :         if (!conversation.isBootstrapped()) {
    1209         2248 :             JAMI_DEBUG("[Conversation {}] Not yet bootstrapped, save notification", conversation.id());
    1210              :             // Because we can get some git channels but not bootstrapped, we should keep this
    1211              :             // to refresh when bootstrapped.
    1212          562 :             notSyncedNotification_[conversation.id()] = commit;
    1213              :         }
    1214         1769 :     }
    1215              : 
    1216         1769 :     std::lock_guard lk(refreshMtx_);
    1217         2940 :     for (const auto& member : nonConnectedMembers) {
    1218         1172 :         auto& refresh = refreshMessage[member];
    1219         1172 :         refresh = sendMsgCb_(member, {}, messageMap, refresh);
    1220              :     }
    1221              : 
    1222              :     // Finally we send to devices that the DRT choose.
    1223        17942 :     for (const auto& device : devices) {
    1224        16182 :         auto deviceIdStr = device.toString();
    1225        16175 :         auto memberUri = conversation.uriFromDevice(deviceIdStr);
    1226        16163 :         if (memberUri.empty() || deviceIdStr == deviceId)
    1227          946 :             continue;
    1228        15216 :         auto& refresh = refreshMessage[deviceIdStr];
    1229        15230 :         refresh = sendMsgCb_(memberUri, device, messageMap, refresh);
    1230        17120 :     }
    1231         3536 : }
    1232              : 
    1233              : void
    1234           90 : ConversationModule::Impl::sendMessage(const std::string& conversationId,
    1235              :                                       std::string message,
    1236              :                                       const std::string& replyTo,
    1237              :                                       bool announce,
    1238              :                                       OnCommitCb&& onCommit,
    1239              :                                       OnDoneCb&& cb)
    1240              : {
    1241           90 :     auto msg = CommitMessage::text(std::move(message), replyTo);
    1242           90 :     createCommit(conversationId, std::move(msg), announce, std::move(onCommit), std::move(cb));
    1243           90 : }
    1244              : 
    1245              : void
    1246          116 : ConversationModule::Impl::createCommit(
    1247              :     const std::string& conversationId, CommitMessage&& message, bool announce, OnCommitCb&& onCommit, OnDoneCb&& cb)
    1248              : {
    1249          116 :     if (auto conv = getConversation(conversationId)) {
    1250          116 :         std::lock_guard lk(conv->mtx);
    1251          116 :         if (conv->conversation)
    1252          232 :             conv->conversation->createCommit(std::move(message),
    1253          116 :                                              std::move(onCommit),
    1254          348 :                                              [this,
    1255              :                                               conversationId,
    1256              :                                               announce,
    1257          116 :                                               cb = std::move(cb)](bool ok, const std::string& commitId) {
    1258          114 :                                                  if (cb)
    1259            5 :                                                      cb(ok, commitId);
    1260          114 :                                                  if (!announce)
    1261            0 :                                                      return;
    1262          114 :                                                  if (ok)
    1263          339 :                                                      sendMessageNotification(conversationId, true, commitId);
    1264              :                                                  else
    1265            4 :                                                      JAMI_ERROR("Failed to send message to conversation {}",
    1266              :                                                                 conversationId);
    1267              :                                              });
    1268          232 :     }
    1269          116 : }
    1270              : 
    1271              : void
    1272            8 : ConversationModule::Impl::editMessage(const std::string& conversationId,
    1273              :                                       const std::string& newBody,
    1274              :                                       const std::string& editedId)
    1275              : {
    1276              :     // Check that editedId is a valid commit, from ourself and plain/text
    1277            8 :     auto validCommit = false;
    1278            8 :     std::string type, fileId;
    1279            8 :     if (auto conv = getConversation(conversationId)) {
    1280            8 :         std::lock_guard lk(conv->mtx);
    1281            8 :         if (conv->conversation) {
    1282            8 :             auto commit = conv->conversation->getCommit(editedId);
    1283            8 :             if (commit != std::nullopt) {
    1284            7 :                 type = commit->commitMsg.type;
    1285            7 :                 if (type == CommitType::DATA_TRANSFER) {
    1286            2 :                     fileId = getFileId(editedId, commit->commitMsg.tid, commit->commitMsg.displayName);
    1287              :                 }
    1288           14 :                 validCommit = commit->authorId == username_
    1289            7 :                               && (type == CommitType::TEXT || type == CommitType::DATA_TRANSFER);
    1290              :             }
    1291            8 :         }
    1292           16 :     }
    1293            8 :     if (!validCommit) {
    1294            8 :         JAMI_ERROR("Unable to edit commit {:s}", editedId);
    1295            2 :         return;
    1296              :     }
    1297              :     // Commit message edition
    1298            6 :     CommitMessage message;
    1299            6 :     if (type == CommitType::DATA_TRANSFER) {
    1300              :         // Remove file!
    1301            2 :         auto path = fileutils::get_data_dir() / accountId_ / "conversation_data" / conversationId / fileId;
    1302            2 :         dhtnet::fileutils::remove(path, true);
    1303            2 :         message = CommitMessage::fileDeleted(editedId);
    1304            2 :     } else {
    1305            4 :         message = CommitMessage::edit(newBody, editedId);
    1306              :     }
    1307            6 :     createCommit(conversationId, std::move(message));
    1308           10 : }
    1309              : 
    1310              : void
    1311          373 : ConversationModule::Impl::bootstrapCb(std::string convId)
    1312              : {
    1313          373 :     std::string commitId;
    1314              :     {
    1315          373 :         std::lock_guard lk(notSyncedNotificationMtx_);
    1316          374 :         auto it = notSyncedNotification_.find(convId);
    1317          374 :         if (it != notSyncedNotification_.end()) {
    1318          138 :             commitId = std::move(it->second);
    1319          138 :             notSyncedNotification_.erase(it);
    1320              :         }
    1321          374 :     }
    1322         1496 :     JAMI_DEBUG("[Account {}] [Conversation {}] Resend last message notification", accountId_, convId);
    1323          373 :     dht::ThreadPool::io().run([w = weak(), convId, commitId = std::move(commitId)] {
    1324          374 :         if (auto sthis = w.lock())
    1325         1122 :             sthis->sendMessageNotification(convId, true, commitId);
    1326          374 :     });
    1327          374 : }
    1328              : 
    1329              : void
    1330          701 : ConversationModule::Impl::fixStructures(
    1331              :     std::shared_ptr<JamiAccount> acc,
    1332              :     const std::vector<std::tuple<std::string, std::string, std::string>>& updateContactConv,
    1333              :     const std::set<std::string>& toRm)
    1334              : {
    1335          702 :     for (const auto& [uri, oldConv, newConv] : updateContactConv) {
    1336            1 :         updateConvForContact(uri, oldConv, newConv);
    1337              :     }
    1338              :     ////////////////////////////////////////////////////////////////
    1339              :     // Note: This is only to homogenize trust and convRequests
    1340          701 :     std::vector<std::string> invalidPendingRequests;
    1341              :     {
    1342          701 :         auto requests = acc->getTrustRequests();
    1343          701 :         std::lock_guard lk(conversationsRequestsMtx_);
    1344          702 :         for (const auto& request : requests) {
    1345            2 :             auto itConvId = request.find(libjami::Account::TrustRequest::CONVERSATIONID);
    1346            1 :             auto itConvFrom = request.find(libjami::Account::TrustRequest::FROM);
    1347            1 :             if (itConvId != request.end() && itConvFrom != request.end()) {
    1348              :                 // Check if requests exists or is declined.
    1349            1 :                 auto itReq = conversationsRequests_.find(itConvId->second);
    1350            1 :                 auto declined = itReq == conversationsRequests_.end() || itReq->second.declined != TimePoint {};
    1351            1 :                 if (declined) {
    1352            4 :                     JAMI_WARNING("Invalid trust request found: {:s}", itConvId->second);
    1353            1 :                     invalidPendingRequests.emplace_back(itConvFrom->second);
    1354              :                 }
    1355              :             }
    1356              :         }
    1357          701 :         auto requestRemoved = false;
    1358          703 :         for (auto it = conversationsRequests_.begin(); it != conversationsRequests_.end();) {
    1359            2 :             if (it->second.from == username_) {
    1360            0 :                 JAMI_WARNING("Detected request from ourself, this makes no sense. Remove {}", it->first);
    1361            0 :                 it = conversationsRequests_.erase(it);
    1362              :             } else {
    1363            2 :                 ++it;
    1364              :             }
    1365              :         }
    1366          701 :         if (requestRemoved) {
    1367            0 :             saveConvRequests();
    1368              :         }
    1369          701 :     }
    1370          702 :     for (const auto& invalidPendingRequest : invalidPendingRequests)
    1371            1 :         acc->discardTrustRequest(invalidPendingRequest);
    1372              : 
    1373              :     ////////////////////////////////////////////////////////////////
    1374          702 :     for (const auto& conv : toRm) {
    1375            4 :         JAMI_ERROR("[Account {}] Remove conversation ({})", accountId_, conv);
    1376            1 :         removeConversation(conv, true);
    1377              :     }
    1378         2804 :     JAMI_DEBUG("[Account {}] Conversations loaded!", accountId_);
    1379          701 : }
    1380              : 
    1381              : void
    1382          213 : ConversationModule::Impl::cloneConversationFrom(const std::shared_ptr<SyncedConversation> conv,
    1383              :                                                 const std::string& deviceId)
    1384              : {
    1385          213 :     std::lock_guard lk(conv->mtx);
    1386          213 :     const auto& conversationId = conv->info.id;
    1387          213 :     if (conv->isUnrecoverable()) {
    1388            0 :         JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Conversation is marked unrecoverable, "
    1389              :                      "ignoring clone request",
    1390              :                      accountId_,
    1391              :                      conversationId,
    1392              :                      deviceId);
    1393            0 :         return;
    1394              :     }
    1395          213 :     if (!conv->startFetch(deviceId, true)) {
    1396           64 :         JAMI_WARNING("[Account {}] [Conversation {}] Already fetching", accountId_, conversationId);
    1397           16 :         return;
    1398              :     }
    1399              : 
    1400          591 :     onNeedSocket_(
    1401              :         conversationId,
    1402              :         deviceId,
    1403          394 :         [wthis = weak_from_this(), conv, conversationId, deviceId](const auto& channel) {
    1404          197 :             std::lock_guard lk(conv->mtx);
    1405          197 :             if (conv->pending && !conv->pending->ready) {
    1406          170 :                 if (channel) {
    1407          142 :                     conv->pending->ready = true;
    1408          142 :                     conv->pending->deviceId = channel->deviceId().toString();
    1409          142 :                     conv->pending->socket = channel;
    1410          142 :                     if (!conv->pending->cloning) {
    1411          142 :                         conv->pending->cloning = true;
    1412          284 :                         dht::ThreadPool::io().run([wthis, conversationId, deviceId = conv->pending->deviceId]() {
    1413          284 :                             if (auto sthis = wthis.lock())
    1414          142 :                                 sthis->handlePendingConversation(conversationId, deviceId);
    1415              :                         });
    1416              :                     }
    1417          142 :                     return true;
    1418           56 :                 } else if (auto sthis = wthis.lock()) {
    1419           28 :                     conv->stopFetch(deviceId);
    1420          112 :                     JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Clone failed. Re-clone in {}s",
    1421              :                                  sthis->accountId_,
    1422              :                                  conversationId,
    1423              :                                  deviceId,
    1424              :                                  conv->fallbackTimer.count());
    1425           28 :                     conv->fallbackClone->expires_at(std::chrono::steady_clock::now() + conv->fallbackTimer);
    1426           28 :                     conv->fallbackTimer *= 2;
    1427           28 :                     if (conv->fallbackTimer > MAX_FALLBACK)
    1428            0 :                         conv->fallbackTimer = MAX_FALLBACK;
    1429           56 :                     conv->fallbackClone->async_wait(std::bind(&ConversationModule::Impl::fallbackClone,
    1430              :                                                               sthis,
    1431              :                                                               std::placeholders::_1,
    1432           28 :                                                               conversationId));
    1433              :                 }
    1434              :             }
    1435           55 :             return false;
    1436          197 :         },
    1437              :         MIME_TYPE_GIT,
    1438              :         false);
    1439          213 : }
    1440              : 
    1441              : void
    1442           39 : ConversationModule::Impl::fallbackClone(const asio::error_code& ec, const std::string& conversationId)
    1443              : {
    1444           39 :     if (ec == asio::error::operation_aborted)
    1445           12 :         return;
    1446           37 :     auto conv = getConversation(conversationId);
    1447           37 :     if (!conv || conv->conversation)
    1448           10 :         return;
    1449           27 :     auto members = getConversationMembers(conversationId);
    1450           81 :     for (const auto& member : members)
    1451          162 :         if (member.at("uri") != username_)
    1452           54 :             cloneConversationFrom(conversationId, member.at("uri"));
    1453           37 : }
    1454              : 
    1455              : void
    1456          859 : ConversationModule::Impl::bootstrap(const std::string& convId)
    1457              : {
    1458          859 :     std::vector<std::shared_ptr<SyncedConversation>> toClone;
    1459          859 :     std::vector<std::shared_ptr<Conversation>> conversations;
    1460          859 :     if (convId.empty()) {
    1461          696 :         std::vector<std::shared_ptr<SyncedConversation>> convs;
    1462              :         {
    1463          696 :             std::lock_guard lk(convInfosMtx_);
    1464          733 :             for (const auto& [conversationId, convInfo] : convInfos_) {
    1465           37 :                 if (auto conv = getConversation(conversationId))
    1466           37 :                     convs.emplace_back(std::move(conv));
    1467              :             }
    1468          696 :         }
    1469          733 :         for (auto& conv : convs) {
    1470           37 :             std::lock_guard lk(conv->mtx);
    1471           37 :             if (!conv->conversation && !conv->info.isRemoved()) {
    1472              :                 // we need to ask to clone requests when bootstrapping all conversations
    1473              :                 // otherwise it can stay syncing
    1474            2 :                 toClone.emplace_back(std::move(conv));
    1475           35 :             } else if (conv->conversation) {
    1476           34 :                 conversations.emplace_back(conv->conversation);
    1477              :             }
    1478           37 :         }
    1479          859 :     } else if (auto conv = getConversation(convId)) {
    1480          107 :         std::lock_guard lk(conv->mtx);
    1481          107 :         if (conv->conversation)
    1482          106 :             conversations.emplace_back(conv->conversation);
    1483          270 :     }
    1484              : 
    1485          999 :     for (const auto& conversation : conversations) {
    1486              : #ifdef LIBJAMI_TEST
    1487          140 :         conversation->onBootstrapStatus(bootstrapCbTest_);
    1488              : #endif
    1489          140 :         conversation->bootstrap([w = weak(), id = conversation->id()] {
    1490           29 :             if (auto sthis = w.lock())
    1491           29 :                 sthis->bootstrapCb(id);
    1492           29 :         });
    1493              : 
    1494          140 :         if (auto acc = account_.lock()) {
    1495          140 :             if (auto pm = acc->presenceManager()) {
    1496          580 :                 for (const auto& member : conversation->memberUris()) {
    1497          160 :                     conversation->addKnownDevices(pm->getDevices(member), member);
    1498          140 :                 }
    1499              :             }
    1500          140 :         }
    1501              :     }
    1502          861 :     for (const auto& conv : toClone) {
    1503            6 :         for (const auto& member : conv->getMembers(false, false)) {
    1504           12 :             if (member.at("uri") != username_)
    1505            4 :                 cloneConversationFrom(conv->info.id, member.at("uri"));
    1506            2 :         }
    1507              :     }
    1508          859 : }
    1509              : 
    1510              : void
    1511          172 : ConversationModule::Impl::cloneConversationFrom(const ConversationRequest& request)
    1512              : {
    1513          172 :     auto memberHash = dht::InfoHash(request.from);
    1514          172 :     if (!memberHash) {
    1515            0 :         JAMI_WARNING("Invalid member detected: {}", request.from);
    1516            0 :         return;
    1517              :     }
    1518          172 :     auto conv = startConversation(request.conversationId);
    1519          172 :     std::lock_guard lk(conv->mtx);
    1520          172 :     if (conv->info.created == TimePoint {}) {
    1521          169 :         conv->info = {request.conversationId};
    1522          169 :         conv->info.created = request.received;
    1523          169 :         conv->info.members.emplace(username_);
    1524          169 :         conv->info.members.emplace(request.from);
    1525          169 :         conv->info.mode = request.mode();
    1526          169 :         addConvInfo(conv->info);
    1527              :     }
    1528          172 :     accountManager_->forEachDevice(memberHash, [w = weak(), conv](const auto& pk) {
    1529          173 :         auto sthis = w.lock();
    1530          173 :         auto deviceId = pk->getLongId().toString();
    1531          173 :         if (!sthis or deviceId == sthis->deviceId_)
    1532            0 :             return;
    1533          173 :         sthis->cloneConversationFrom(conv, deviceId);
    1534          173 :     });
    1535          172 : }
    1536              : 
    1537              : void
    1538           29 : ConversationModule::Impl::cloneConversationFrom(const std::string& conversationId, const std::string& uri)
    1539              : {
    1540           29 :     auto memberHash = dht::InfoHash(uri);
    1541           29 :     if (!memberHash) {
    1542            0 :         JAMI_WARNING("Invalid member detected: {}", uri);
    1543            0 :         return;
    1544              :     }
    1545           29 :     auto conv = startConversation(conversationId);
    1546           58 :     accountManager_->forEachDevice(memberHash,
    1547           58 :                                    [w = weak(), conv, conversationId](
    1548              :                                        const std::shared_ptr<dht::crypto::PublicKey>& pk) {
    1549           14 :                                        auto sthis = w.lock();
    1550           14 :                                        auto deviceId = pk->getLongId().toString();
    1551           14 :                                        if (!sthis or deviceId == sthis->deviceId_)
    1552            0 :                                            return;
    1553           14 :                                        sthis->cloneConversationFrom(conv, deviceId);
    1554           14 :                                    });
    1555           29 : }
    1556              : 
    1557              : ////////////////////////////////////////////////////////////////
    1558              : 
    1559              : void
    1560          482 : ConversationModule::saveConvRequests(const std::string& accountId,
    1561              :                                      const std::map<std::string, ConversationRequest>& conversationsRequests)
    1562              : {
    1563          482 :     auto path = fileutils::get_data_dir() / accountId;
    1564          481 :     saveConvRequestsToPath(path, conversationsRequests);
    1565          482 : }
    1566              : 
    1567              : void
    1568         1276 : ConversationModule::saveConvRequestsToPath(const std::filesystem::path& path,
    1569              :                                            const std::map<std::string, ConversationRequest>& conversationsRequests)
    1570              : {
    1571         1276 :     auto p = path / "convRequests";
    1572         1276 :     std::lock_guard lock(dhtnet::fileutils::getFileLock(p));
    1573         1277 :     std::ofstream file(p, std::ios::trunc | std::ios::binary);
    1574         1277 :     msgpack::pack(file, conversationsRequests);
    1575         1277 : }
    1576              : 
    1577              : void
    1578         3398 : ConversationModule::saveConvInfos(const std::string& accountId, const ConvInfoMap& conversations)
    1579              : {
    1580         3398 :     auto path = fileutils::get_data_dir() / accountId;
    1581         3398 :     saveConvInfosToPath(path, conversations);
    1582         3397 : }
    1583              : 
    1584              : void
    1585         4193 : ConversationModule::saveConvInfosToPath(const std::filesystem::path& path, const ConvInfoMap& conversations)
    1586              : {
    1587         4193 :     std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convInfo"));
    1588         4193 :     std::ofstream file(path / "convInfo", std::ios::trunc | std::ios::binary);
    1589         4191 :     msgpack::pack(file, conversations);
    1590         4193 : }
    1591              : 
    1592              : ////////////////////////////////////////////////////////////////
    1593              : 
    1594          689 : ConversationModule::ConversationModule(std::shared_ptr<JamiAccount> account,
    1595              :                                        std::shared_ptr<AccountManager> accountManager,
    1596              :                                        NeedsSyncingCb&& needsSyncingCb,
    1597              :                                        SengMsgCb&& sendMsgCb,
    1598              :                                        NeedSocketCb&& onNeedSocket,
    1599              :                                        NeedSocketCb&& onNeedSwarmSocket,
    1600              :                                        OneToOneRecvCb&& oneToOneRecvCb,
    1601          689 :                                        bool autoLoadConversations)
    1602          689 :     : pimpl_ {std::make_unique<Impl>(std::move(account),
    1603          689 :                                      std::move(accountManager),
    1604          689 :                                      std::move(needsSyncingCb),
    1605          689 :                                      std::move(sendMsgCb),
    1606          689 :                                      std::move(onNeedSocket),
    1607          689 :                                      std::move(onNeedSwarmSocket),
    1608          689 :                                      std::move(oneToOneRecvCb))}
    1609              : {
    1610          689 :     if (autoLoadConversations) {
    1611          689 :         loadConversations();
    1612              :     }
    1613          689 : }
    1614              : 
    1615              : void
    1616           16 : ConversationModule::setAccountManager(std::shared_ptr<AccountManager> accountManager)
    1617              : {
    1618           16 :     std::unique_lock lk(pimpl_->conversationsMtx_);
    1619           16 :     pimpl_->accountManager_ = accountManager;
    1620           16 : }
    1621              : 
    1622              : #ifdef LIBJAMI_TEST
    1623              : void
    1624            2 : ConversationModule::onBootstrapStatus(const std::function<void(std::string, Conversation::BootstrapStatus)>& cb)
    1625              : {
    1626            2 :     pimpl_->bootstrapCbTest_ = cb;
    1627            3 :     for (auto& c : pimpl_->getConversations())
    1628            3 :         c->onBootstrapStatus(pimpl_->bootstrapCbTest_);
    1629            2 : }
    1630              : #endif
    1631              : 
    1632              : void
    1633          701 : ConversationModule::loadConversations()
    1634              : {
    1635          701 :     auto acc = pimpl_->account_.lock();
    1636          701 :     if (!acc)
    1637            0 :         return;
    1638         2804 :     JAMI_LOG("[Account {}] Start loading conversations…", pimpl_->accountId_);
    1639          701 :     auto conversationPath = fileutils::get_data_dir() / pimpl_->accountId_ / "conversations";
    1640              : 
    1641          701 :     std::unique_lock lk(pimpl_->conversationsMtx_);
    1642          701 :     auto contacts = pimpl_->accountManager_->getContacts(
    1643          701 :         true); // Avoid to lock configurationMtx while conv Mtx is locked
    1644          701 :     std::unique_lock ilk(pimpl_->convInfosMtx_);
    1645          701 :     pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
    1646          701 :     pimpl_->conversations_.clear();
    1647              : 
    1648              :     struct Ctx
    1649              :     {
    1650              :         std::mutex cvMtx;
    1651              :         std::condition_variable cv;
    1652              :         std::mutex toRmMtx;
    1653              :         std::set<std::string> toRm;
    1654              :         std::mutex convMtx;
    1655              :         size_t convNb;
    1656              :         std::map<dht::InfoHash, Contact> contacts;
    1657              :         std::vector<std::tuple<std::string, std::string, std::string>> updateContactConv;
    1658              :     };
    1659              :     struct PendingConvCounter
    1660              :     {
    1661              :         std::shared_ptr<Ctx> ctx;
    1662           18 :         PendingConvCounter(std::shared_ptr<Ctx> c)
    1663           18 :             : ctx(std::move(c))
    1664              :         {
    1665           18 :             std::lock_guard lk {ctx->cvMtx};
    1666           18 :             ++ctx->convNb;
    1667           18 :         }
    1668           18 :         ~PendingConvCounter()
    1669              :         {
    1670           18 :             std::lock_guard lk {ctx->cvMtx};
    1671           18 :             --ctx->convNb;
    1672           18 :             ctx->cv.notify_all();
    1673           18 :         }
    1674              :     };
    1675          701 :     auto ctx = std::make_shared<Ctx>();
    1676          701 :     ctx->convNb = 0;
    1677          701 :     ctx->contacts = std::move(contacts);
    1678              : 
    1679          701 :     std::error_code ec;
    1680          737 :     for (const auto& convIt : std::filesystem::directory_iterator(conversationPath, ec)) {
    1681              :         // ignore if not regular file or hidden
    1682           18 :         auto name = convIt.path().filename().string();
    1683           18 :         if (!convIt.is_directory() || name[0] == '.')
    1684            0 :             continue;
    1685           36 :         dht::ThreadPool::io().run(
    1686           36 :             [this, ctx, repository = std::move(name), acc, _ = std::make_shared<PendingConvCounter>(ctx)] {
    1687              :                 try {
    1688           18 :                     auto sconv = std::make_shared<SyncedConversation>(repository);
    1689           18 :                     auto conv = std::make_shared<Conversation>(acc, repository);
    1690           18 :                     conv->onMessageStatusChanged([this, repository](const auto& status) {
    1691            5 :                         auto msg = std::make_shared<SyncMsg>();
    1692           10 :                         msg->ms = {{repository, status}};
    1693            5 :                         pimpl_->needsSyncingCb_(std::move(msg));
    1694           10 :                     });
    1695           18 :                     conv->onMembersChanged([w = pimpl_->weak_from_this(), repository](const auto& members) {
    1696              :                         // Delay in another thread to avoid deadlocks
    1697            8 :                         dht::ThreadPool::io().run([w, repository, members = std::move(members)] {
    1698            8 :                             if (auto sthis = w.lock())
    1699            4 :                                 sthis->setConversationMembers(repository, members);
    1700              :                         });
    1701            4 :                     });
    1702           18 :                     conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
    1703           18 :                     auto members = conv->memberUris(acc->getUsername(), {});
    1704              :                     // NOTE: The following if is here to protect against any incorrect state
    1705              :                     // that can be introduced
    1706           18 :                     if (conv->mode() == ConversationMode::ONE_TO_ONE && members.size() == 1) {
    1707              :                         // If we got a 1:1 conversation, but not in the contact details, it's rather a
    1708              :                         // duplicate or a weird state
    1709            5 :                         auto otherUri = *members.begin();
    1710            5 :                         auto itContact = ctx->contacts.find(dht::InfoHash(otherUri));
    1711            5 :                         if (itContact == ctx->contacts.end()) {
    1712            0 :                             JAMI_WARNING("Contact {} not found", otherUri);
    1713            0 :                             return;
    1714              :                         }
    1715            5 :                         const std::string& convFromDetails = itContact->second.conversationId;
    1716            5 :                         auto isRemoved = !itContact->second.isActive();
    1717            5 :                         if (convFromDetails != repository) {
    1718            3 :                             if (convFromDetails.empty()) {
    1719            2 :                                 if (isRemoved) {
    1720              :                                     // If details is empty, contact is removed and not banned.
    1721            4 :                                     JAMI_ERROR("Conversation {} detected for {} and should be removed",
    1722              :                                                repository,
    1723              :                                                otherUri);
    1724            1 :                                     std::lock_guard lkMtx {ctx->toRmMtx};
    1725            1 :                                     ctx->toRm.insert(repository);
    1726            1 :                                 } else {
    1727            4 :                                     JAMI_ERROR("No conversation detected for {} but one exists ({}). Update details",
    1728              :                                                otherUri,
    1729              :                                                repository);
    1730            1 :                                     std::lock_guard lkMtx {ctx->toRmMtx};
    1731            2 :                                     ctx->updateContactConv.emplace_back(
    1732            2 :                                         std::make_tuple(otherUri, convFromDetails, repository));
    1733            1 :                                 }
    1734              :                             }
    1735              :                         }
    1736            5 :                     }
    1737              :                     {
    1738           18 :                         std::lock_guard lkMtx {ctx->convMtx};
    1739           18 :                         auto convInfo = pimpl_->convInfos_.find(repository);
    1740           18 :                         if (convInfo == pimpl_->convInfos_.end()) {
    1741            8 :                             JAMI_ERROR("Missing conv info for {}. This is a bug!", repository);
    1742            2 :                             sconv->info.created = nowMs();
    1743            6 :                             sconv->info.lastDisplayed = conv->infos()[ConversationMapKeys::LAST_DISPLAYED];
    1744              :                         } else {
    1745           16 :                             sconv->info = convInfo->second;
    1746           16 :                             if (convInfo->second.isRemoved()) {
    1747              :                                 // A conversation was removed, but repository still exists
    1748            1 :                                 conv->setRemovingFlag();
    1749            1 :                                 std::lock_guard lkMtx {ctx->toRmMtx};
    1750            1 :                                 ctx->toRm.insert(repository);
    1751            1 :                             }
    1752              :                         }
    1753              :                         // Even if we found the conversation in convInfos_, unable to assume that the
    1754              :                         // list of members stored in `convInfo` is correct
    1755              :                         // (https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/1025). For this
    1756              :                         // reason, we always use the list we got from the conversation repository to set
    1757              :                         // the value of `sconv->info.members`.
    1758           18 :                         members.emplace(acc->getUsername());
    1759           18 :                         sconv->info.members = std::move(members);
    1760              :                         // convInfosMtx_ is already locked
    1761           18 :                         pimpl_->convInfos_[repository] = sconv->info;
    1762           18 :                     }
    1763           18 :                     auto commits = conv->commitsEndedCalls();
    1764              : 
    1765           18 :                     if (!commits.empty()) {
    1766              :                         // Note: here, this means that some calls were actives while the
    1767              :                         // daemon finished (can be a crash).
    1768              :                         // Notify other in the conversation that the call is finished
    1769            0 :                         pimpl_->sendMessageNotification(*conv, true, *commits.rbegin());
    1770              :                     }
    1771           18 :                     sconv->conversation = conv;
    1772           18 :                     std::lock_guard lkMtx {ctx->convMtx};
    1773           18 :                     pimpl_->conversations_.emplace(repository, std::move(sconv));
    1774           18 :                 } catch (const std::logic_error& e) {
    1775            0 :                     JAMI_WARNING("[Account {}] Conversations not loaded: {}", pimpl_->accountId_, e.what());
    1776            0 :                 }
    1777              :             });
    1778          719 :     }
    1779          701 :     if (ec) {
    1780         2736 :         JAMI_ERROR("Failed to read conversations directory {}: {}", conversationPath, ec.message());
    1781              :     }
    1782              : 
    1783          701 :     std::unique_lock lkCv(ctx->cvMtx);
    1784         1420 :     ctx->cv.wait(lkCv, [&] { return ctx->convNb == 0; });
    1785              : 
    1786              :     // Prune any invalid conversations without members and
    1787              :     // set the removed flag if needed
    1788          701 :     std::set<std::string> removed;
    1789          732 :     for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
    1790           31 :         const auto& info = itInfo->second;
    1791           31 :         if (info.members.empty()) {
    1792            0 :             itInfo = pimpl_->convInfos_.erase(itInfo);
    1793            0 :             continue;
    1794              :         }
    1795           31 :         if (info.isRemoved())
    1796            2 :             removed.insert(info.id);
    1797           31 :         auto itConv = pimpl_->conversations_.find(info.id);
    1798           31 :         if (itConv == pimpl_->conversations_.end()) {
    1799              :             // convInfos_ can contain a conversation that is not yet cloned
    1800              :             // so we need to add it there.
    1801           13 :             itConv = pimpl_->conversations_.emplace(info.id, std::make_shared<SyncedConversation>(info)).first;
    1802              :         }
    1803           31 :         if (itConv != pimpl_->conversations_.end() && itConv->second && itConv->second->conversation && info.isRemoved())
    1804            1 :             itConv->second->conversation->setRemovingFlag();
    1805           31 :         if (!info.isRemoved() && itConv == pimpl_->conversations_.end()) {
    1806              :             // In this case, the conversation is not synced and we only know ourself
    1807            0 :             if (info.members.size() == 1 && *info.members.begin() == acc->getUsername()) {
    1808            0 :                 JAMI_WARNING("[Account {:s}] Conversation {:s} seems not present/synced.", pimpl_->accountId_, info.id);
    1809            0 :                 emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, info.id);
    1810            0 :                 itInfo = pimpl_->convInfos_.erase(itInfo);
    1811            0 :                 continue;
    1812            0 :             }
    1813              :         }
    1814           31 :         ++itInfo;
    1815              :     }
    1816              :     // On oldest version, removeConversation didn't update "appdata/contacts"
    1817              :     // causing a potential incorrect state between "appdata/contacts" and "appdata/convInfos"
    1818          701 :     if (!removed.empty())
    1819            2 :         acc->unlinkConversations(removed);
    1820              : 
    1821          708 :     for (const auto& [contactId, contact] : ctx->contacts) {
    1822            7 :         if (contact.conversationId.empty())
    1823            7 :             continue;
    1824              : 
    1825            5 :         if (pimpl_->convInfos_.find(contact.conversationId) != pimpl_->convInfos_.end())
    1826            5 :             continue;
    1827              : 
    1828            0 :         ConvInfo newInfo;
    1829            0 :         newInfo.id = contact.conversationId;
    1830            0 :         newInfo.created = nowMs();
    1831            0 :         newInfo.members.emplace(pimpl_->username_);
    1832            0 :         newInfo.members.emplace(contactId.toString());
    1833            0 :         pimpl_->conversations_.emplace(contact.conversationId, std::make_shared<SyncedConversation>(newInfo));
    1834            0 :         pimpl_->convInfos_.emplace(contact.conversationId, std::move(newInfo));
    1835            0 :     }
    1836              : 
    1837          701 :     pimpl_->saveConvInfos();
    1838              : 
    1839          701 :     ilk.unlock();
    1840          701 :     lk.unlock();
    1841              : 
    1842         1402 :     dht::ThreadPool::io().run(
    1843         1402 :         [w = pimpl_->weak(), acc, updateContactConv = std::move(ctx->updateContactConv), toRm = std::move(ctx->toRm)]() {
    1844              :             // Will lock account manager
    1845          701 :             if (auto shared = w.lock())
    1846          701 :                 shared->fixStructures(acc, updateContactConv, toRm);
    1847          701 :         });
    1848          701 : }
    1849              : 
    1850              : void
    1851            0 : ConversationModule::loadSingleConversation(const std::string& convId)
    1852              : {
    1853            0 :     auto acc = pimpl_->account_.lock();
    1854            0 :     if (!acc)
    1855            0 :         return;
    1856            0 :     JAMI_LOG("[Account {}] Start loading conversation {}", pimpl_->accountId_, convId);
    1857              : 
    1858            0 :     std::unique_lock lk(pimpl_->conversationsMtx_);
    1859            0 :     std::unique_lock ilk(pimpl_->convInfosMtx_);
    1860              :     // Load convInfos to retrieve requests that have been accepted but not yet synchronized.
    1861            0 :     pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
    1862            0 :     pimpl_->conversations_.clear();
    1863              : 
    1864              :     try {
    1865            0 :         auto sconv = std::make_shared<SyncedConversation>(convId);
    1866              : 
    1867            0 :         auto conv = std::make_shared<Conversation>(acc, convId);
    1868              : 
    1869            0 :         conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
    1870              : 
    1871            0 :         sconv->conversation = conv;
    1872            0 :         pimpl_->conversations_.emplace(convId, std::move(sconv));
    1873            0 :     } catch (const std::logic_error& e) {
    1874            0 :         JAMI_WARNING("[Account {}] Conversations not loaded: {}", pimpl_->accountId_, e.what());
    1875            0 :     }
    1876              : 
    1877              :     // Add all other conversations as dummy conversations to indicate their existence so
    1878              :     // isConversation could detect conversations correctly.
    1879            0 :     auto conversationsRepositoryIds = dhtnet::fileutils::readDirectory(fileutils::get_data_dir() / pimpl_->accountId_
    1880            0 :                                                                        / "conversations");
    1881            0 :     for (auto repositoryId : conversationsRepositoryIds) {
    1882            0 :         if (repositoryId != convId) {
    1883            0 :             auto conv = std::make_shared<SyncedConversation>(repositoryId);
    1884            0 :             pimpl_->conversations_.emplace(repositoryId, conv);
    1885            0 :         }
    1886            0 :     }
    1887              : 
    1888              :     // Add conversations from convInfos_ so isConversation could detect conversations correctly.
    1889              :     // This includes conversations that have been accepted but are not yet synchronized.
    1890            0 :     for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
    1891            0 :         const auto& info = itInfo->second;
    1892            0 :         if (info.members.empty()) {
    1893            0 :             itInfo = pimpl_->convInfos_.erase(itInfo);
    1894            0 :             continue;
    1895              :         }
    1896            0 :         auto itConv = pimpl_->conversations_.find(info.id);
    1897            0 :         if (itConv == pimpl_->conversations_.end()) {
    1898              :             // convInfos_ can contain a conversation that is not yet cloned
    1899              :             // so we need to add it there.
    1900            0 :             pimpl_->conversations_.emplace(info.id, std::make_shared<SyncedConversation>(info));
    1901              :         }
    1902            0 :         ++itInfo;
    1903              :     }
    1904              : 
    1905            0 :     ilk.unlock();
    1906            0 :     lk.unlock();
    1907            0 : }
    1908              : 
    1909              : void
    1910          859 : ConversationModule::bootstrap(const std::string& convId)
    1911              : {
    1912          859 :     pimpl_->bootstrap(convId);
    1913          859 : }
    1914              : 
    1915              : void
    1916            0 : ConversationModule::monitor()
    1917              : {
    1918            0 :     for (auto& conv : pimpl_->getConversations())
    1919            0 :         conv->monitor();
    1920            0 : }
    1921              : 
    1922              : void
    1923          716 : ConversationModule::clearPendingFetch()
    1924              : {
    1925              :     // Note: This is a workaround. convModule() is kept if account is disabled/re-enabled.
    1926              :     // iOS uses setAccountActive() a lot, and if for some reason the previous pending fetch
    1927              :     // is not erased (callback not called), it will block the new messages as it will not
    1928              :     // sync. The best way to debug this is to get logs from the last ICE connection for
    1929              :     // syncing the conversation. It may have been killed in some un-expected way avoiding to
    1930              :     // call the callbacks. This should never happen, but if it's the case, this will allow
    1931              :     // new messages to be synced correctly.
    1932          743 :     for (auto& conv : pimpl_->getSyncedConversations()) {
    1933           27 :         std::lock_guard lk(conv->mtx);
    1934           27 :         if (conv && conv->pending) {
    1935            0 :             JAMI_ERROR("This is a bug, seems to still fetch to some device on initializing");
    1936            0 :             conv->pending.reset();
    1937              :         }
    1938          743 :     }
    1939          716 : }
    1940              : 
    1941              : void
    1942            0 : ConversationModule::reloadRequests()
    1943              : {
    1944            0 :     pimpl_->conversationsRequests_ = convRequests(pimpl_->accountId_);
    1945            0 : }
    1946              : 
    1947              : std::vector<std::string>
    1948           12 : ConversationModule::getConversations() const
    1949              : {
    1950           12 :     std::vector<std::string> result;
    1951           12 :     std::lock_guard lk(pimpl_->convInfosMtx_);
    1952           12 :     result.reserve(pimpl_->convInfos_.size());
    1953           25 :     for (const auto& [key, conv] : pimpl_->convInfos_) {
    1954           13 :         if (conv.isRemoved())
    1955            3 :             continue;
    1956           10 :         result.emplace_back(key);
    1957              :     }
    1958           24 :     return result;
    1959           12 : }
    1960              : 
    1961              : std::string
    1962          566 : ConversationModule::getOneToOneConversation(const std::string& uri) const noexcept
    1963              : {
    1964          566 :     return pimpl_->getOneToOneConversation(uri);
    1965              : }
    1966              : 
    1967              : bool
    1968            8 : ConversationModule::updateConvForContact(const std::string& uri, const std::string& oldConv, const std::string& newConv)
    1969              : {
    1970            8 :     return pimpl_->updateConvForContact(uri, oldConv, newConv);
    1971              : }
    1972              : 
    1973              : std::vector<std::map<std::string, std::string>>
    1974           12 : ConversationModule::getConversationRequests() const
    1975              : {
    1976           12 :     std::vector<std::map<std::string, std::string>> requests;
    1977           12 :     std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    1978           12 :     requests.reserve(pimpl_->conversationsRequests_.size());
    1979           24 :     for (const auto& [id, request] : pimpl_->conversationsRequests_) {
    1980           12 :         if (request.declined != TimePoint {})
    1981            6 :             continue; // Do not add declined requests
    1982            6 :         requests.emplace_back(request.toMap());
    1983              :     }
    1984           24 :     return requests;
    1985           12 : }
    1986              : 
    1987              : void
    1988           95 : ConversationModule::onTrustRequest(const std::string& uri,
    1989              :                                    const std::string& conversationId,
    1990              :                                    const std::vector<uint8_t>& payload,
    1991              :                                    TimePoint received)
    1992              : {
    1993           95 :     std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
    1994           95 :     ConversationRequest req;
    1995           95 :     req.from = uri;
    1996           95 :     req.conversationId = conversationId;
    1997           95 :     req.received = nowMs();
    1998          190 :     req.metadatas = ConversationRepository::infosFromVCard(
    1999          285 :         vCard::utils::toMap(std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size())));
    2000           95 :     auto reqMap = req.toMap();
    2001              : 
    2002           95 :     auto contactInfo = pimpl_->accountManager_->getContactInfo(uri);
    2003           95 :     if (contactInfo && contactInfo->confirmed && !contactInfo->isBanned() && contactInfo->isActive()) {
    2004           28 :         JAMI_LOG("[Account {}] Contact {} is confirmed, cloning {}", pimpl_->accountId_, uri, conversationId);
    2005            7 :         lk.unlock();
    2006            7 :         updateConvForContact(uri, contactInfo->conversationId, conversationId);
    2007            7 :         pimpl_->cloneConversationFrom(req);
    2008            7 :         return;
    2009              :     }
    2010              : 
    2011           88 :     if (pimpl_->addConversationRequest(conversationId, std::move(req))) {
    2012           65 :         lk.unlock();
    2013           65 :         emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(pimpl_->accountId_,
    2014              :                                                                        conversationId,
    2015              :                                                                        uri,
    2016              :                                                                        payload,
    2017              :                                                                        toSecondsSinceEpoch(received));
    2018           65 :         emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, conversationId, reqMap);
    2019           65 :         pimpl_->needsSyncingCb_({});
    2020              :     } else {
    2021           92 :         JAMI_DEBUG("[Account {}] Received a request for a conversation already existing. Ignore", pimpl_->accountId_);
    2022              :     }
    2023          116 : }
    2024              : 
    2025              : void
    2026          257 : ConversationModule::onConversationRequest(const std::string& from, const Json::Value& value)
    2027              : {
    2028          257 :     ConversationRequest req(value);
    2029          257 :     auto isOneToOne = req.isOneToOne();
    2030          257 :     std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
    2031         1028 :     JAMI_DEBUG("[Account {}] Receive a new conversation request for conversation {} from {}",
    2032              :                pimpl_->accountId_,
    2033              :                req.conversationId,
    2034              :                from);
    2035          257 :     auto convId = req.conversationId;
    2036              : 
    2037              :     // Already accepted request, do nothing
    2038          257 :     if (pimpl_->isConversation(convId))
    2039          102 :         return;
    2040          155 :     auto oldReq = pimpl_->getRequest(convId);
    2041          155 :     if (oldReq != std::nullopt) {
    2042           92 :         JAMI_DEBUG("[Account {}] Received a request for a conversation already existing. "
    2043              :                    "Ignore. Declined: {}",
    2044              :                    pimpl_->accountId_,
    2045              :                    oldReq->declined != TimePoint {});
    2046           23 :         return;
    2047              :     }
    2048          132 :     req.received = nowMs();
    2049          132 :     req.from = from;
    2050              : 
    2051          132 :     if (isOneToOne) {
    2052            1 :         auto contactInfo = pimpl_->accountManager_->getContactInfo(from);
    2053            1 :         if (contactInfo && contactInfo->confirmed && !contactInfo->isBanned() && contactInfo->isActive()) {
    2054            0 :             JAMI_LOG("[Account {}] Contact {} is confirmed, cloning {}", pimpl_->accountId_, from, convId);
    2055            0 :             lk.unlock();
    2056            0 :             updateConvForContact(from, contactInfo->conversationId, convId);
    2057            0 :             pimpl_->cloneConversationFrom(req);
    2058            0 :             return;
    2059              :         }
    2060            1 :     }
    2061              : 
    2062          132 :     auto reqMap = req.toMap();
    2063          132 :     if (pimpl_->addConversationRequest(convId, std::move(req))) {
    2064          132 :         lk.unlock();
    2065              :         // Note: no need to sync here because other connected devices should receive
    2066              :         // the same conversation request. Will sync when the conversation will be added
    2067          132 :         if (isOneToOne)
    2068            1 :             pimpl_->oneToOneRecvCb_(convId, from);
    2069          132 :         emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, convId, reqMap);
    2070              :     }
    2071          530 : }
    2072              : 
    2073              : std::string
    2074            3 : ConversationModule::peerFromConversationRequest(const std::string& convId) const
    2075              : {
    2076            3 :     std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2077            3 :     auto it = pimpl_->conversationsRequests_.find(convId);
    2078            3 :     if (it != pimpl_->conversationsRequests_.end()) {
    2079            3 :         return it->second.from;
    2080              :     }
    2081            0 :     return {};
    2082            3 : }
    2083              : 
    2084              : void
    2085          130 : ConversationModule::onNeedConversationRequest(const std::string& from, const std::string& conversationId)
    2086              : {
    2087          130 :     pimpl_->withConversation(conversationId, [&](auto& conversation) {
    2088          130 :         if (!conversation.isMember(from, true)) {
    2089            0 :             JAMI_WARNING("{} is asking a new invite for {}, but not a member", from, conversationId);
    2090            0 :             return;
    2091              :         }
    2092          520 :         JAMI_LOG("{} is asking a new invite for {}", from, conversationId);
    2093          130 :         pimpl_->sendMsgCb_(from, {}, conversation.generateInvitation(), 0);
    2094              :     });
    2095          130 : }
    2096              : 
    2097              : void
    2098          211 : ConversationModule::acceptConversationRequest(const std::string& conversationId, const std::string& deviceId)
    2099              : {
    2100              :     // For all conversation members, try to open a git channel with this conversation ID
    2101          211 :     std::unique_lock lkCr(pimpl_->conversationsRequestsMtx_);
    2102          211 :     auto request = pimpl_->getRequest(conversationId);
    2103          211 :     if (request == std::nullopt) {
    2104           46 :         lkCr.unlock();
    2105           46 :         if (auto conv = pimpl_->getConversation(conversationId)) {
    2106           27 :             std::unique_lock lk(conv->mtx);
    2107           27 :             if (!conv->conversation) {
    2108           26 :                 lk.unlock();
    2109           26 :                 pimpl_->cloneConversationFrom(conv, deviceId);
    2110              :             }
    2111           73 :         }
    2112          184 :         JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Request not found.",
    2113              :                      pimpl_->accountId_,
    2114              :                      conversationId,
    2115              :                      deviceId);
    2116           46 :         return;
    2117              :     }
    2118          165 :     pimpl_->rmConversationRequest(conversationId);
    2119          165 :     lkCr.unlock();
    2120          165 :     pimpl_->accountManager_->acceptTrustRequest(request->from, true);
    2121          165 :     pimpl_->cloneConversationFrom(*request);
    2122          257 : }
    2123              : 
    2124              : void
    2125            6 : ConversationModule::declineConversationRequest(const std::string& conversationId)
    2126              : {
    2127            6 :     std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2128            6 :     auto it = pimpl_->conversationsRequests_.find(conversationId);
    2129            6 :     if (it != pimpl_->conversationsRequests_.end()) {
    2130            6 :         it->second.declined = nowMs();
    2131            6 :         pimpl_->saveConvRequests();
    2132              :     }
    2133            6 :     pimpl_->syncingMetadatas_.erase(conversationId);
    2134            6 :     pimpl_->saveMetadata();
    2135            6 :     emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, conversationId);
    2136            6 :     pimpl_->needsSyncingCb_({});
    2137            6 : }
    2138              : 
    2139              : std::string
    2140          183 : ConversationModule::startConversation(ConversationMode mode, const dht::InfoHash& otherMember)
    2141              : {
    2142          183 :     auto acc = pimpl_->account_.lock();
    2143          183 :     if (!acc)
    2144            0 :         return {};
    2145              :     // Create the conversation object
    2146          183 :     std::shared_ptr<Conversation> conversation;
    2147              :     try {
    2148          183 :         conversation = std::make_shared<Conversation>(acc, mode, otherMember.toString());
    2149          183 :         auto conversationId = conversation->id();
    2150          183 :         conversation->onMessageStatusChanged([this, conversationId](const auto& status) {
    2151          768 :             auto msg = std::make_shared<SyncMsg>();
    2152         1536 :             msg->ms = {{conversationId, status}};
    2153          768 :             pimpl_->needsSyncingCb_(std::move(msg));
    2154         1536 :         });
    2155          183 :         conversation->onMembersChanged([w = pimpl_->weak_from_this(), conversationId](const auto& members) {
    2156              :             // Delay in another thread to avoid deadlocks
    2157         1138 :             dht::ThreadPool::io().run([w, conversationId, members = std::move(members)] {
    2158         1138 :                 if (auto sthis = w.lock())
    2159          569 :                     sthis->setConversationMembers(conversationId, members);
    2160              :             });
    2161          569 :         });
    2162          183 :         conversation->onNeedSocket(pimpl_->onNeedSwarmSocket_);
    2163              : #ifdef LIBJAMI_TEST
    2164          183 :         conversation->onBootstrapStatus(pimpl_->bootstrapCbTest_);
    2165              : #endif
    2166          183 :         conversation->bootstrap([w = pimpl_->weak_from_this(), conversationId]() {
    2167           41 :             if (auto sthis = w.lock())
    2168           41 :                 sthis->bootstrapCb(conversationId);
    2169           41 :         });
    2170          183 :         if (auto pm = acc->presenceManager()) {
    2171          732 :             for (const auto& member : conversation->memberUris()) {
    2172          183 :                 conversation->addKnownDevices(pm->getDevices(member), member);
    2173          183 :             }
    2174              :         }
    2175          183 :     } catch (const std::exception& e) {
    2176            0 :         JAMI_ERROR("[Account {}] Error while generating a conversation {}", pimpl_->accountId_, e.what());
    2177            0 :         return {};
    2178            0 :     }
    2179          183 :     auto convId = conversation->id();
    2180          183 :     auto conv = pimpl_->startConversation(convId);
    2181          183 :     std::unique_lock lk(conv->mtx);
    2182          183 :     conv->info.created = nowMs();
    2183          183 :     conv->info.members.emplace(pimpl_->username_);
    2184          183 :     if (otherMember)
    2185           69 :         conv->info.members.emplace(otherMember.toString());
    2186          183 :     conv->conversation = conversation;
    2187          183 :     addConvInfo(conv->info);
    2188          183 :     lk.unlock();
    2189              : 
    2190          183 :     pimpl_->needsSyncingCb_({});
    2191          183 :     emitSignal<libjami::ConversationSignal::ConversationReady>(pimpl_->accountId_, convId);
    2192          183 :     return convId;
    2193          183 : }
    2194              : 
    2195              : void
    2196            0 : ConversationModule::cloneConversationFrom(const std::string& conversationId, const std::string& uri)
    2197              : {
    2198            0 :     pimpl_->cloneConversationFrom(conversationId, uri);
    2199            0 : }
    2200              : 
    2201              : // Message send/load
    2202              : void
    2203           90 : ConversationModule::sendMessage(const std::string& conversationId,
    2204              :                                 std::string message,
    2205              :                                 const std::string& replyTo,
    2206              :                                 bool announce,
    2207              :                                 OnCommitCb&& onCommit,
    2208              :                                 OnDoneCb&& cb)
    2209              : {
    2210           90 :     pimpl_->sendMessage(conversationId, std::move(message), replyTo, announce, std::move(onCommit), std::move(cb));
    2211           90 : }
    2212              : 
    2213              : void
    2214           14 : ConversationModule::createCommit(
    2215              :     const std::string& conversationId, CommitMessage&& message, bool announce, OnCommitCb&& onCommit, OnDoneCb&& cb)
    2216              : {
    2217           14 :     pimpl_->createCommit(conversationId, std::move(message), announce, std::move(onCommit), std::move(cb));
    2218           14 : }
    2219              : 
    2220              : void
    2221            8 : ConversationModule::editMessage(const std::string& conversationId,
    2222              :                                 const std::string& newBody,
    2223              :                                 const std::string& editedId)
    2224              : {
    2225            8 :     pimpl_->editMessage(conversationId, newBody, editedId);
    2226            8 : }
    2227              : 
    2228              : void
    2229            3 : ConversationModule::reactToMessage(const std::string& conversationId,
    2230              :                                    const std::string& newBody,
    2231              :                                    const std::string& reactToId)
    2232              : {
    2233            3 :     auto message = CommitMessage::reaction(newBody, reactToId);
    2234            3 :     pimpl_->createCommit(conversationId, std::move(message));
    2235            3 : }
    2236              : 
    2237              : void
    2238          100 : ConversationModule::addCallHistoryMessage(const std::string& uri, uint64_t duration_ms, const std::string& reason)
    2239              : {
    2240          100 :     auto finalUri = uri.substr(0, uri.find("@ring.dht"));
    2241          100 :     finalUri = finalUri.substr(0, uri.find("@jami.dht"));
    2242          100 :     auto convId = getOneToOneConversation(finalUri);
    2243          100 :     if (!convId.empty()) {
    2244            3 :         auto message = CommitMessage::outgoingCallEnd(finalUri, duration_ms, reason);
    2245            3 :         pimpl_->createCommit(convId, std::move(message));
    2246            3 :     }
    2247          100 : }
    2248              : 
    2249              : bool
    2250           18 : ConversationModule::onMessageDisplayed(const std::string& peer,
    2251              :                                        const std::string& conversationId,
    2252              :                                        const std::string& interactionId)
    2253              : {
    2254           18 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2255           18 :         std::unique_lock lk(conv->mtx);
    2256           18 :         if (auto conversation = conv->conversation) {
    2257           18 :             lk.unlock();
    2258           18 :             return conversation->setMessageDisplayed(peer, interactionId);
    2259           18 :         }
    2260           36 :     }
    2261            0 :     return false;
    2262              : }
    2263              : 
    2264              : std::map<std::string, std::map<std::string, std::map<std::string, std::string>>>
    2265          181 : ConversationModule::convMessageStatus() const
    2266              : {
    2267          181 :     std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> messageStatus;
    2268          245 :     for (const auto& conv : pimpl_->getConversations()) {
    2269           64 :         auto d = conv->messageStatus();
    2270           64 :         if (!d.empty())
    2271           32 :             messageStatus[conv->id()] = std::move(d);
    2272          245 :     }
    2273          181 :     return messageStatus;
    2274            0 : }
    2275              : 
    2276              : void
    2277            0 : ConversationModule::clearCache(const std::string& conversationId)
    2278              : {
    2279            0 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2280            0 :         std::lock_guard lk(conv->mtx);
    2281            0 :         if (conv->conversation) {
    2282            0 :             conv->conversation->clearCache();
    2283              :         }
    2284            0 :     }
    2285            0 : }
    2286              : 
    2287              : uint32_t
    2288            2 : ConversationModule::loadConversation(const std::string& conversationId, const std::string& fromMessage, size_t n)
    2289              : {
    2290            2 :     auto acc = pimpl_->account_.lock();
    2291            2 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2292            2 :         std::lock_guard lk(conv->mtx);
    2293            2 :         if (conv->conversation) {
    2294            2 :             const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
    2295            2 :             LogOptions options;
    2296            2 :             options.from = fromMessage;
    2297            2 :             options.nbOfCommits = n;
    2298            2 :             auto convWeak = std::weak_ptr<Conversation>(conv->conversation);
    2299            4 :             conv->conversation->loadMessages(
    2300            4 :                 [accountId = pimpl_->accountId_, conversationId, id, convWeak](auto&& messages) {
    2301            2 :                     emitSignal<libjami::ConversationSignal::SwarmLoaded>(id, accountId, conversationId, messages);
    2302              : #ifdef ENABLE_PLUGIN
    2303            4 :                     if (const auto convPtr = convWeak.lock()) {
    2304            2 :                         convPtr->loadMissingBodyOverwrites();
    2305              :                     }
    2306              : #endif
    2307            2 :                 },
    2308              :                 options);
    2309            2 :             return id;
    2310            2 :         }
    2311            4 :     }
    2312            0 :     return 0;
    2313            2 : }
    2314              : 
    2315              : uint32_t
    2316            0 : ConversationModule::loadSwarmUntil(const std::string& conversationId,
    2317              :                                    const std::string& fromMessage,
    2318              :                                    const std::string& toMessage)
    2319              : {
    2320            0 :     auto acc = pimpl_->account_.lock();
    2321            0 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2322            0 :         std::lock_guard lk(conv->mtx);
    2323            0 :         if (conv->conversation) {
    2324            0 :             const uint32_t id = std::uniform_int_distribution<uint32_t> {}(acc->rand);
    2325            0 :             LogOptions options;
    2326            0 :             options.from = fromMessage;
    2327            0 :             options.to = toMessage;
    2328            0 :             options.includeTo = true;
    2329            0 :             auto convWeak = std::weak_ptr<Conversation>(conv->conversation);
    2330            0 :             conv->conversation->loadMessages(
    2331            0 :                 [accountId = pimpl_->accountId_, conversationId, id, convWeak](auto&& messages) {
    2332            0 :                     emitSignal<libjami::ConversationSignal::SwarmLoaded>(id, accountId, conversationId, messages);
    2333              : #ifdef ENABLE_PLUGIN
    2334            0 :                     if (const auto convPtr = convWeak.lock()) {
    2335            0 :                         convPtr->loadMissingBodyOverwrites();
    2336              :                     }
    2337              : #endif
    2338            0 :                 },
    2339              :                 options);
    2340            0 :             return id;
    2341            0 :         }
    2342            0 :     }
    2343            0 :     return 0;
    2344            0 : }
    2345              : 
    2346              : std::shared_ptr<TransferManager>
    2347           88 : ConversationModule::dataTransfer(const std::string& conversationId) const
    2348              : {
    2349          262 :     return pimpl_->withConversation(conversationId, [](auto& conversation) { return conversation.dataTransfer(); });
    2350              : }
    2351              : 
    2352              : bool
    2353           14 : ConversationModule::onFileChannelRequest(const std::string& conversationId,
    2354              :                                          const std::string& member,
    2355              :                                          const std::string& fileId,
    2356              :                                          bool verifyShaSum) const
    2357              : {
    2358           14 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2359           14 :         std::filesystem::path path;
    2360           14 :         std::string sha3sum;
    2361           14 :         std::unique_lock lk(conv->mtx);
    2362           14 :         if (!conv->conversation)
    2363            0 :             return false;
    2364           14 :         if (!conv->conversation->onFileChannelRequest(member, fileId, path, sha3sum))
    2365            0 :             return false;
    2366              : 
    2367              :         // Release the lock here to prevent the sha3 calculation from blocking other threads.
    2368           14 :         lk.unlock();
    2369           14 :         if (!std::filesystem::is_regular_file(path)) {
    2370            4 :             JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for non existing file {}",
    2371              :                          pimpl_->accountId_,
    2372              :                          conversationId,
    2373              :                          member,
    2374              :                          fileId);
    2375            1 :             return false;
    2376              :         }
    2377              :         // Check that our file is correct before sending
    2378           13 :         if (verifyShaSum && sha3sum != fileutils::sha3File(path)) {
    2379            4 :             JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for file {:s}, but our version is not "
    2380              :                          "complete or corrupted",
    2381              :                          pimpl_->accountId_,
    2382              :                          conversationId,
    2383              :                          member,
    2384              :                          fileId);
    2385            1 :             return false;
    2386              :         }
    2387           12 :         return true;
    2388           28 :     }
    2389            0 :     return false;
    2390              : }
    2391              : 
    2392              : bool
    2393           13 : ConversationModule::downloadFile(const std::string& conversationId,
    2394              :                                  const std::string& interactionId,
    2395              :                                  const std::string& fileId,
    2396              :                                  const std::string& path)
    2397              : {
    2398           13 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2399           13 :         std::lock_guard lk(conv->mtx);
    2400           13 :         if (conv->conversation)
    2401           65 :             return conv->conversation->downloadFile(interactionId, fileId, path, "", "");
    2402           26 :     }
    2403            0 :     return false;
    2404              : }
    2405              : 
    2406              : void
    2407         1224 : ConversationModule::syncConversations(const std::string& peer, const std::string& deviceId)
    2408              : {
    2409              :     // Sync conversations where peer is member
    2410         1224 :     std::set<std::string> toFetch;
    2411         1224 :     std::set<std::string> toClone;
    2412         2133 :     for (const auto& conv : pimpl_->getSyncedConversations()) {
    2413          909 :         std::lock_guard lk(conv->mtx);
    2414          909 :         if (conv->conversation) {
    2415          829 :             if (!conv->conversation->isRemoving() && conv->conversation->isMember(peer, false)) {
    2416          607 :                 toFetch.emplace(conv->info.id);
    2417              :             }
    2418           80 :         } else if (!conv->info.isRemoved()
    2419          218 :                    && std::find(conv->info.members.begin(), conv->info.members.end(), peer)
    2420          218 :                           != conv->info.members.end()) {
    2421              :             // In this case the conversation was never cloned (can be after an import)
    2422           54 :             toClone.emplace(conv->info.id);
    2423              :         }
    2424         2133 :     }
    2425         1831 :     for (const auto& cid : toFetch)
    2426         1821 :         pimpl_->fetchNewCommits(peer, deviceId, cid);
    2427         1278 :     for (const auto& cid : toClone)
    2428           54 :         pimpl_->cloneConversation(deviceId, peer, cid);
    2429         2448 :     if (pimpl_->syncCnt.load() == 0)
    2430          598 :         emitSignal<libjami::ConversationSignal::ConversationSyncFinished>(pimpl_->accountId_);
    2431         1224 : }
    2432              : 
    2433              : void
    2434          157 : ConversationModule::onSyncData(const SyncMsg& msg, const std::string& peerId, const std::string& deviceId)
    2435              : {
    2436          157 :     std::vector<std::string> toClone;
    2437              :     // Set when the conversation list or requests actually change, so the change
    2438              :     // can be re-propagated to our other devices (relay). Metadata-only updates
    2439              :     // (preferences, message status) deliberately do not set it.
    2440          157 :     bool listChanged = false;
    2441          260 :     for (const auto& [key, convInfo] : msg.c) {
    2442          103 :         const auto& convId = convInfo.id;
    2443              :         {
    2444          103 :             std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2445          103 :             pimpl_->rmConversationRequest(convId);
    2446          103 :         }
    2447              : 
    2448              :         // Whether this conversation is new to our list. Used to decide if the
    2449              :         // change must be relayed: a conversation we already know but have not
    2450              :         // finished cloning yet must not be treated as a change, otherwise it
    2451              :         // would re-trigger syncing on every received sync.
    2452          103 :         bool isNewConv = not pimpl_->isConversation(convId);
    2453          103 :         auto conv = pimpl_->startConversation(convInfo);
    2454          103 :         std::unique_lock lk(conv->mtx);
    2455              :         // Skip outdated info
    2456          103 :         if (std::max(convInfo.created, convInfo.removed) < std::max(conv->info.created, conv->info.removed))
    2457            6 :             continue;
    2458           97 :         if (not convInfo.isRemoved()) {
    2459              :             // If multi devices, it can detect a conversation that was already
    2460              :             // removed, so just check if the convinfo contains a removed conv
    2461           92 :             if (conv->info.removed != TimePoint {}) {
    2462            0 :                 if (conv->info.removed >= convInfo.created) {
    2463              :                     // Only reclone if re-added, else the peer is not synced yet (could be
    2464              :                     // offline before)
    2465            0 :                     continue;
    2466              :                 }
    2467            0 :                 JAMI_DEBUG("Re-add previously removed conversation {:s}", convId);
    2468              :             }
    2469           92 :             conv->info = convInfo;
    2470           92 :             if (!conv->conversation) {
    2471           47 :                 if (isNewConv)
    2472           17 :                     listChanged = true;
    2473           47 :                 if (deviceId != "") {
    2474           47 :                     pimpl_->cloneConversation(deviceId, peerId, conv);
    2475              :                 } else {
    2476              :                     // In this case, information is from JAMS
    2477              :                     // JAMS does not store the conversation itself, so we
    2478              :                     // must use information to clone the conversation
    2479            0 :                     addConvInfo(convInfo);
    2480            0 :                     toClone.emplace_back(convId);
    2481              :                 }
    2482              :             }
    2483              :         } else {
    2484            5 :             if (conv->conversation && !conv->conversation->isRemoving()) {
    2485            1 :                 emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, convId);
    2486            1 :                 conv->conversation->setRemovingFlag();
    2487              :             }
    2488            5 :             auto update = false;
    2489            5 :             if (conv->info.removed == TimePoint {}) {
    2490            1 :                 update = true;
    2491            1 :                 listChanged = true;
    2492            1 :                 conv->info.removed = nowMs();
    2493            1 :                 emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, convId);
    2494              :             }
    2495            5 :             if (convInfo.erased != TimePoint {} && conv->info.erased == TimePoint {}) {
    2496            1 :                 listChanged = true;
    2497            1 :                 conv->info.erased = nowMs();
    2498            1 :                 pimpl_->addConvInfo(conv->info);
    2499            1 :                 pimpl_->removeRepositoryImpl(*conv, false);
    2500            4 :             } else if (update) {
    2501            0 :                 pimpl_->addConvInfo(conv->info);
    2502              :             }
    2503              :         }
    2504          109 :     }
    2505              : 
    2506          157 :     for (const auto& cid : toClone) {
    2507            0 :         auto members = getConversationMembers(cid);
    2508            0 :         for (const auto& member : members) {
    2509            0 :             if (member.at("uri") != pimpl_->username_)
    2510            0 :                 cloneConversationFrom(cid, member.at("uri"));
    2511              :         }
    2512            0 :     }
    2513              : 
    2514          171 :     for (const auto& [convId, req] : msg.cr) {
    2515           14 :         if (req.from == pimpl_->username_) {
    2516            0 :             JAMI_WARNING("Detected request from ourself, ignore {}.", convId);
    2517            0 :             continue;
    2518            0 :         }
    2519           14 :         std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
    2520           14 :         if (pimpl_->isConversation(convId)) {
    2521              :             // Already handled request
    2522            2 :             pimpl_->rmConversationRequest(convId);
    2523            2 :             continue;
    2524              :         }
    2525              : 
    2526              :         // New request
    2527           12 :         if (!pimpl_->addConversationRequest(convId, req))
    2528            7 :             continue;
    2529              :         // A request was added or transitioned to declined: the request list
    2530              :         // changed and must be re-propagated to our other devices.
    2531            5 :         listChanged = true;
    2532              : 
    2533            5 :         if (req.declined != TimePoint {}) {
    2534              :             // Request declined
    2535           12 :             JAMI_LOG("[Account {:s}] Declined request detected for conversation {:s} (device {:s})",
    2536              :                      pimpl_->accountId_,
    2537              :                      convId,
    2538              :                      deviceId);
    2539            3 :             pimpl_->syncingMetadatas_.erase(convId);
    2540            3 :             pimpl_->saveMetadata();
    2541            3 :             lk.unlock();
    2542            3 :             emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, convId);
    2543            3 :             continue;
    2544            3 :         }
    2545            2 :         lk.unlock();
    2546              : 
    2547            8 :         JAMI_LOG("[Account {:s}] New request detected for conversation {:s} (device {:s})",
    2548              :                  pimpl_->accountId_,
    2549              :                  convId,
    2550              :                  deviceId);
    2551              : 
    2552            2 :         emitSignal<libjami::ConversationSignal::ConversationRequestReceived>(pimpl_->accountId_, convId, req.toMap());
    2553           14 :     }
    2554              : 
    2555              :     // Updates preferences for conversations
    2556          161 :     for (const auto& [convId, p] : msg.p) {
    2557            4 :         if (auto conv = pimpl_->getConversation(convId)) {
    2558            4 :             std::unique_lock lk(conv->mtx);
    2559            4 :             if (conv->conversation) {
    2560            2 :                 auto conversation = conv->conversation;
    2561            2 :                 lk.unlock();
    2562            2 :                 conversation->updatePreferences(p);
    2563            4 :             } else if (conv->pending) {
    2564            2 :                 conv->pending->preferences = p;
    2565              :             }
    2566            8 :         }
    2567              :     }
    2568              : 
    2569              :     // Updates displayed for conversations
    2570          212 :     for (const auto& [convId, ms] : msg.ms) {
    2571           55 :         if (auto conv = pimpl_->getConversation(convId)) {
    2572           53 :             std::unique_lock lk(conv->mtx);
    2573           53 :             if (conv->conversation) {
    2574           33 :                 auto conversation = conv->conversation;
    2575           33 :                 lk.unlock();
    2576           33 :                 conversation->updateMessageStatus(ms);
    2577           53 :             } else if (conv->pending) {
    2578           20 :                 conv->pending->status = ms;
    2579              :             }
    2580          108 :         }
    2581              :     }
    2582              : 
    2583              :     // If the conversation list or requests changed as a result of this sync,
    2584              :     // re-propagate to our other devices (relay) so a change learned from one
    2585              :     // device reaches the others. This only applies to peer-to-peer syncs:
    2586              :     // when the data comes from the JAMS server (empty deviceId), the server is
    2587              :     // the synchronization hub and already holds the change, so re-propagating
    2588              :     // would push it straight back and cause an infinite sync loop.
    2589          157 :     if (listChanged && !deviceId.empty())
    2590           23 :         pimpl_->needsSyncingCb_({});
    2591          157 : }
    2592              : 
    2593              : bool
    2594            2 : ConversationModule::needsSyncingWith(const std::string& memberUri) const
    2595              : {
    2596              :     // Check if a conversation needs to fetch remote or to be cloned
    2597            2 :     std::lock_guard lk(pimpl_->conversationsMtx_);
    2598            2 :     for (const auto& [key, ci] : pimpl_->conversations_) {
    2599            1 :         std::lock_guard lk(ci->mtx);
    2600            1 :         if (ci->conversation) {
    2601            0 :             if (ci->conversation->isRemoving() && ci->conversation->isMember(memberUri, false))
    2602            0 :                 return true;
    2603            1 :         } else if (ci->info.removed == TimePoint {}
    2604            1 :                    && std::find(ci->info.members.begin(), ci->info.members.end(), memberUri) != ci->info.members.end()) {
    2605              :             // In this case the conversation was never cloned (can be after an import)
    2606            1 :             return true;
    2607              :         }
    2608            1 :     }
    2609            1 :     return false;
    2610            2 : }
    2611              : 
    2612              : void
    2613         1104 : ConversationModule::setFetched(const std::string& conversationId,
    2614              :                                const std::string& deviceId,
    2615              :                                const std::string& commitId)
    2616              : {
    2617         1104 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2618         1104 :         std::lock_guard lk(conv->mtx);
    2619         1104 :         if (conv->conversation) {
    2620         1104 :             bool remove = conv->conversation->isRemoving();
    2621         1104 :             conv->conversation->hasFetched(deviceId, commitId);
    2622         1104 :             if (remove)
    2623            1 :                 pimpl_->removeRepositoryImpl(*conv, true);
    2624              :         }
    2625         2208 :     }
    2626         1104 : }
    2627              : 
    2628              : void
    2629        15794 : ConversationModule::fetchNewCommits(const std::string& peer,
    2630              :                                     const std::string& deviceId,
    2631              :                                     const std::string& conversationId,
    2632              :                                     const std::string& commitId)
    2633              : {
    2634        15794 :     pimpl_->fetchNewCommits(peer, deviceId, conversationId, commitId);
    2635        15788 : }
    2636              : 
    2637              : void
    2638          136 : ConversationModule::addConversationMember(const std::string& conversationId,
    2639              :                                           const dht::InfoHash& contactUri,
    2640              :                                           bool sendRequest)
    2641              : {
    2642          136 :     auto conv = pimpl_->getConversation(conversationId);
    2643          136 :     if (not conv || not conv->conversation) {
    2644            0 :         JAMI_ERROR("Conversation {:s} does not exist", conversationId);
    2645            0 :         return;
    2646              :     }
    2647          136 :     std::unique_lock lk(conv->mtx);
    2648              : 
    2649          136 :     auto contactUriStr = contactUri.toString();
    2650          136 :     if (conv->conversation->isMember(contactUriStr, true)) {
    2651            0 :         JAMI_DEBUG("{:s} is already a member of {:s}, resend invite", contactUriStr, conversationId);
    2652              :         // Note: This should not be necessary, but if for whatever reason the other side didn't
    2653              :         // join we should not forbid new invites
    2654            0 :         auto invite = conv->conversation->generateInvitation();
    2655            0 :         lk.unlock();
    2656            0 :         pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
    2657            0 :         return;
    2658            0 :     }
    2659              : 
    2660          272 :     conv->conversation->addMember(contactUriStr,
    2661          272 :                                   [this, conv, conversationId, sendRequest, contactUriStr](bool ok,
    2662              :                                                                                            const std::string& commitId) {
    2663          136 :                                       if (ok) {
    2664          134 :                                           std::unique_lock lk(conv->mtx);
    2665          268 :                                           pimpl_->sendMessageNotification(*conv->conversation,
    2666              :                                                                           true,
    2667              :                                                                           commitId); // For the other members
    2668          134 :                                           if (sendRequest) {
    2669          130 :                                               auto invite = conv->conversation->generateInvitation();
    2670          130 :                                               lk.unlock();
    2671          130 :                                               pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
    2672          130 :                                           }
    2673          134 :                                       }
    2674          136 :                                   });
    2675          136 : }
    2676              : 
    2677              : void
    2678           14 : ConversationModule::removeConversationMember(const std::string& conversationId,
    2679              :                                              const dht::InfoHash& contactUri,
    2680              :                                              bool isDevice)
    2681              : {
    2682           14 :     auto contactUriStr = contactUri.toString();
    2683           14 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2684           14 :         std::lock_guard lk(conv->mtx);
    2685           14 :         if (conv->conversation)
    2686           14 :             return conv->conversation
    2687           14 :                 ->removeMember(contactUriStr, isDevice, [this, conversationId](bool ok, const std::string& commitId) {
    2688           14 :                     if (ok) {
    2689           36 :                         pimpl_->sendMessageNotification(conversationId, true, commitId);
    2690              :                     }
    2691           28 :                 });
    2692           28 :     }
    2693           14 : }
    2694              : 
    2695              : std::vector<std::map<std::string, std::string>>
    2696           93 : ConversationModule::getConversationMembers(const std::string& conversationId, bool includeBanned) const
    2697              : {
    2698           93 :     return pimpl_->getConversationMembers(conversationId, includeBanned);
    2699              : }
    2700              : 
    2701              : uint32_t
    2702            4 : ConversationModule::countInteractions(const std::string& convId,
    2703              :                                       const std::string& toId,
    2704              :                                       const std::string& fromId,
    2705              :                                       const std::string& authorUri) const
    2706              : {
    2707            4 :     if (auto conv = pimpl_->getConversation(convId)) {
    2708            4 :         std::lock_guard lk(conv->mtx);
    2709            4 :         if (conv->conversation)
    2710            4 :             return conv->conversation->countInteractions(toId, fromId, authorUri);
    2711            8 :     }
    2712            0 :     return 0;
    2713              : }
    2714              : 
    2715              : void
    2716            4 : ConversationModule::search(uint32_t req, const std::string& convId, const Filter& filter) const
    2717              : {
    2718            4 :     if (convId.empty()) {
    2719            0 :         auto convs = pimpl_->getConversations();
    2720            0 :         if (convs.empty()) {
    2721            0 :             emitSignal<libjami::ConversationSignal::MessagesFound>(req,
    2722            0 :                                                                    pimpl_->accountId_,
    2723            0 :                                                                    std::string {},
    2724            0 :                                                                    std::vector<std::map<std::string, std::string>> {});
    2725            0 :             return;
    2726              :         }
    2727            0 :         auto finishedFlag = std::make_shared<std::atomic_int>(convs.size());
    2728            0 :         for (const auto& conv : convs) {
    2729            0 :             conv->search(req, filter, finishedFlag);
    2730              :         }
    2731            4 :     } else if (auto conv = pimpl_->getConversation(convId)) {
    2732            4 :         std::lock_guard lk(conv->mtx);
    2733            4 :         if (conv->conversation)
    2734            4 :             conv->conversation->search(req, filter, std::make_shared<std::atomic_int>(1));
    2735            8 :     }
    2736              : }
    2737              : 
    2738              : void
    2739            9 : ConversationModule::updateConversationInfos(const std::string& conversationId,
    2740              :                                             const std::map<std::string, std::string>& infos,
    2741              :                                             bool sync)
    2742              : {
    2743            9 :     auto conv = pimpl_->getConversation(conversationId);
    2744            9 :     if (not conv or not conv->conversation) {
    2745            0 :         JAMI_ERROR("Conversation {:s} does not exist", conversationId);
    2746            0 :         return;
    2747              :     }
    2748            9 :     std::lock_guard lk(conv->mtx);
    2749            9 :     conv->conversation->updateInfos(infos, [this, conversationId, sync](bool ok, const std::string& commitId) {
    2750            9 :         if (ok && sync) {
    2751           24 :             pimpl_->sendMessageNotification(conversationId, true, commitId);
    2752            1 :         } else if (sync)
    2753            4 :             JAMI_WARNING("Unable to update info on {:s}", conversationId);
    2754            9 :     });
    2755            9 : }
    2756              : 
    2757              : std::map<std::string, std::string>
    2758            5 : ConversationModule::conversationInfos(const std::string& conversationId) const
    2759              : {
    2760              :     {
    2761            5 :         std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2762            5 :         auto itReq = pimpl_->conversationsRequests_.find(conversationId);
    2763            5 :         if (itReq != pimpl_->conversationsRequests_.end())
    2764            1 :             return itReq->second.metadatas;
    2765            5 :     }
    2766            4 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2767            4 :         std::lock_guard lk(conv->mtx);
    2768            4 :         std::map<std::string, std::string> md;
    2769              :         {
    2770            4 :             std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2771            4 :             auto syncingMetadatasIt = pimpl_->syncingMetadatas_.find(conversationId);
    2772            4 :             if (syncingMetadatasIt != pimpl_->syncingMetadatas_.end()) {
    2773            2 :                 if (conv->conversation) {
    2774            0 :                     pimpl_->syncingMetadatas_.erase(syncingMetadatasIt);
    2775            0 :                     pimpl_->saveMetadata();
    2776              :                 } else {
    2777            2 :                     md = syncingMetadatasIt->second;
    2778              :                 }
    2779              :             }
    2780            4 :         }
    2781            4 :         if (conv->conversation)
    2782            2 :             return conv->conversation->infos();
    2783              :         else
    2784            2 :             return md;
    2785            8 :     }
    2786            0 :     JAMI_ERROR("Conversation {:s} does not exist", conversationId);
    2787            0 :     return {};
    2788              : }
    2789              : 
    2790              : void
    2791            5 : ConversationModule::setConversationPreferences(const std::string& conversationId,
    2792              :                                                const std::map<std::string, std::string>& prefs)
    2793              : {
    2794            5 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2795            5 :         std::unique_lock lk(conv->mtx);
    2796            5 :         if (not conv->conversation) {
    2797            0 :             JAMI_ERROR("Conversation {:s} does not exist", conversationId);
    2798            0 :             return;
    2799              :         }
    2800            5 :         auto conversation = conv->conversation;
    2801            5 :         lk.unlock();
    2802            5 :         conversation->updatePreferences(prefs);
    2803            5 :         auto msg = std::make_shared<SyncMsg>();
    2804           15 :         msg->p = {{conversationId, conversation->preferences(true)}};
    2805            5 :         pimpl_->needsSyncingCb_(std::move(msg));
    2806           10 :     }
    2807           10 : }
    2808              : 
    2809              : std::map<std::string, std::string>
    2810           14 : ConversationModule::getConversationPreferences(const std::string& conversationId, bool includeCreated) const
    2811              : {
    2812           14 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2813           14 :         std::lock_guard lk(conv->mtx);
    2814           14 :         if (conv->conversation)
    2815           13 :             return conv->conversation->preferences(includeCreated);
    2816           28 :     }
    2817            1 :     return {};
    2818              : }
    2819              : 
    2820              : std::map<std::string, std::map<std::string, std::string>>
    2821          181 : ConversationModule::convPreferences() const
    2822              : {
    2823          181 :     std::map<std::string, std::map<std::string, std::string>> p;
    2824          245 :     for (const auto& conv : pimpl_->getConversations()) {
    2825           64 :         auto prefs = conv->preferences(true);
    2826           64 :         if (!prefs.empty())
    2827            3 :             p[conv->id()] = std::move(prefs);
    2828          245 :     }
    2829          181 :     return p;
    2830            0 : }
    2831              : 
    2832              : std::vector<uint8_t>
    2833            0 : ConversationModule::conversationVCard(const std::string& conversationId) const
    2834              : {
    2835            0 :     if (auto conv = pimpl_->getConversation(conversationId)) {
    2836            0 :         std::lock_guard lk(conv->mtx);
    2837            0 :         if (conv->conversation)
    2838            0 :             return conv->conversation->vCard();
    2839            0 :     }
    2840            0 :     JAMI_ERROR("Conversation {:s} does not exist", conversationId);
    2841            0 :     return {};
    2842              : }
    2843              : 
    2844              : bool
    2845            0 : ConversationModule::isMemberBanned(const std::string& convId, const std::string& uri) const
    2846              : {
    2847              :     dhtnet::tls::TrustStore::PermissionStatus status;
    2848              :     {
    2849            0 :         std::lock_guard lk(pimpl_->conversationsMtx_);
    2850            0 :         status = pimpl_->accountManager_->getCertificateStatus(uri);
    2851            0 :     }
    2852            0 :     if (auto conv = pimpl_->getConversation(convId)) {
    2853            0 :         std::lock_guard lk(conv->mtx);
    2854            0 :         if (!conv->conversation)
    2855            0 :             return true;
    2856            0 :         if (conv->conversation->mode() != ConversationMode::ONE_TO_ONE)
    2857            0 :             return conv->conversation->isMemberBanned(uri);
    2858              :         // If 1:1 we check the certificate status
    2859            0 :         return status == dhtnet::tls::TrustStore::PermissionStatus::BANNED;
    2860            0 :     }
    2861            0 :     return true;
    2862              : }
    2863              : 
    2864              : bool
    2865         1035 : ConversationModule::isDeviceBanned(const std::string& convId, const std::string& deviceId) const
    2866              : {
    2867              :     dhtnet::tls::TrustStore::PermissionStatus status;
    2868              :     {
    2869         1035 :         std::lock_guard lk(pimpl_->conversationsMtx_);
    2870         1035 :         status = pimpl_->accountManager_->getCertificateStatus(deviceId);
    2871         1035 :     }
    2872         1035 :     if (auto conv = pimpl_->getConversation(convId)) {
    2873         1035 :         std::lock_guard lk(conv->mtx);
    2874         1035 :         if (!conv->conversation)
    2875            4 :             return true;
    2876         1031 :         if (conv->conversation->mode() != ConversationMode::ONE_TO_ONE)
    2877          887 :             return conv->conversation->isDeviceBanned(deviceId);
    2878          144 :         return status == dhtnet::tls::TrustStore::PermissionStatus::BANNED;
    2879         2070 :     }
    2880            0 :     return true;
    2881              : }
    2882              : 
    2883              : bool
    2884         4589 : ConversationModule::isPeerAuthorized(const std::string& convId,
    2885              :                                      const std::string& uri,
    2886              :                                      const std::string& deviceId,
    2887              :                                      bool includeInvited) const
    2888              : {
    2889              :     dhtnet::tls::TrustStore::PermissionStatus memberStatus;
    2890              :     dhtnet::tls::TrustStore::PermissionStatus deviceStatus;
    2891              :     {
    2892         4589 :         std::lock_guard lk(pimpl_->conversationsMtx_);
    2893         4590 :         memberStatus = pimpl_->accountManager_->getCertificateStatus(uri);
    2894         4590 :         deviceStatus = pimpl_->accountManager_->getCertificateStatus(deviceId);
    2895         4590 :     }
    2896         4590 :     if (memberStatus == dhtnet::tls::TrustStore::PermissionStatus::BANNED
    2897         4590 :         || deviceStatus == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
    2898            0 :         return false;
    2899              :     }
    2900         4590 :     if (auto conv = pimpl_->getConversation(convId)) {
    2901         4572 :         std::lock_guard lk(conv->mtx);
    2902         4573 :         if (conv->conversation)
    2903         4266 :             return conv->conversation->isPeerAuthorized(uri, deviceId, includeInvited);
    2904          307 :         if (!includeInvited)
    2905            0 :             return false;
    2906          307 :         return !conv->info.isRemoved() && conv->info.members.find(uri) != conv->info.members.end();
    2907         9162 :     }
    2908              : 
    2909           17 :     return false;
    2910              : }
    2911              : 
    2912              : void
    2913           23 : ConversationModule::removeContact(const std::string& uri, bool banned)
    2914              : {
    2915              :     // Remove linked conversation's requests
    2916              :     {
    2917           23 :         std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
    2918           23 :         auto update = false;
    2919           27 :         for (auto it = pimpl_->conversationsRequests_.begin(); it != pimpl_->conversationsRequests_.end(); ++it) {
    2920            4 :             if (it->second.from == uri && it->second.declined == TimePoint {}) {
    2921           16 :                 JAMI_DEBUG("Declining conversation request {:s} from {:s}", it->first, uri);
    2922            4 :                 pimpl_->syncingMetadatas_.erase(it->first);
    2923            4 :                 pimpl_->saveMetadata();
    2924            4 :                 emitSignal<libjami::ConversationSignal::ConversationRequestDeclined>(pimpl_->accountId_, it->first);
    2925            4 :                 update = true;
    2926            4 :                 it->second.declined = nowMs();
    2927              :             }
    2928              :         }
    2929           23 :         if (update) {
    2930            4 :             pimpl_->saveConvRequests();
    2931            4 :             pimpl_->needsSyncingCb_({});
    2932              :         }
    2933           23 :     }
    2934           23 :     if (banned) {
    2935            7 :         auto conversationId = getOneToOneConversation(uri);
    2936            7 :         pimpl_->withConversation(conversationId, [&](auto& conv) { conv.shutdownConnections(); });
    2937            7 :         return; // Keep the conversation in banned model but stop connections
    2938            7 :     }
    2939              : 
    2940              :     // Removed contacts should not be linked to any conversation
    2941           32 :     pimpl_->accountManager_->updateContactConversation(uri, "");
    2942              : 
    2943              :     // Remove all one-to-one conversations with the removed contact
    2944           16 :     auto isSelf = uri == pimpl_->username_;
    2945           16 :     std::vector<std::string> toRm;
    2946           15 :     auto removeConvInfo = [&](const auto& conv, const auto& members) {
    2947            1 :         if ((isSelf && members.size() == 1)
    2948           16 :             || (!isSelf && std::find(members.begin(), members.end(), uri) != members.end())) {
    2949              :             // Mark the conversation as removed if it wasn't already
    2950           14 :             if (!conv->info.isRemoved()) {
    2951           14 :                 conv->info.removed = nowMs();
    2952           14 :                 emitSignal<libjami::ConversationSignal::ConversationRemoved>(pimpl_->accountId_, conv->info.id);
    2953           14 :                 pimpl_->addConvInfo(conv->info);
    2954           14 :                 return true;
    2955              :             }
    2956              :         }
    2957            1 :         return false;
    2958           16 :     };
    2959              :     {
    2960           16 :         std::lock_guard lk(pimpl_->conversationsMtx_);
    2961           31 :         for (auto& [convId, conv] : pimpl_->conversations_) {
    2962           15 :             std::lock_guard lk(conv->mtx);
    2963           15 :             if (conv->conversation) {
    2964              :                 try {
    2965              :                     // Note it's important to check getUsername(), else
    2966              :                     // removing self can remove all conversations
    2967           14 :                     if (conv->conversation->mode() == ConversationMode::ONE_TO_ONE) {
    2968           14 :                         auto initMembers = conv->conversation->getInitialMembers();
    2969           14 :                         if (removeConvInfo(conv, initMembers))
    2970           13 :                             toRm.emplace_back(convId);
    2971           14 :                     }
    2972            0 :                 } catch (const std::exception& e) {
    2973            0 :                     JAMI_WARNING("{}", e.what());
    2974            0 :                 }
    2975              :             } else {
    2976            1 :                 removeConvInfo(conv, conv->info.members);
    2977              :             }
    2978           15 :         }
    2979           16 :     }
    2980           29 :     for (const auto& id : toRm)
    2981           13 :         pimpl_->removeRepository(id, true, true);
    2982           16 : }
    2983              : 
    2984              : bool
    2985            9 : ConversationModule::removeConversation(const std::string& conversationId)
    2986              : {
    2987            9 :     auto conversation = pimpl_->getConversation(conversationId);
    2988            9 :     std::string existingConvId;
    2989              : 
    2990            9 :     if (!conversation) {
    2991            4 :         JAMI_LOG("Conversation {} not found", conversationId);
    2992            1 :         return false;
    2993              :     }
    2994              : 
    2995            8 :     std::shared_ptr<Conversation> conv;
    2996              :     {
    2997            8 :         std::lock_guard lk(conversation->mtx);
    2998            8 :         conv = conversation->conversation;
    2999            8 :     }
    3000              : 
    3001            2 :     auto sendNotification = [&](const std::string& convId) {
    3002            2 :         if (auto convObj = pimpl_->getConversation(convId)) {
    3003            2 :             std::lock_guard lk(convObj->mtx);
    3004            2 :             if (convObj->conversation) {
    3005            2 :                 auto commitId = convObj->conversation->lastCommitId();
    3006            2 :                 if (!commitId.empty()) {
    3007            6 :                     pimpl_->sendMessageNotification(*convObj->conversation, true, commitId);
    3008              :                 }
    3009            2 :             }
    3010            4 :         }
    3011            2 :     };
    3012              : 
    3013            2 :     auto handleNewConversation = [&](const std::string& uri) {
    3014            2 :         std::string newConvId = startConversation(ConversationMode::ONE_TO_ONE, dht::InfoHash(uri));
    3015            2 :         pimpl_->accountManager_->updateContactConversation(uri, newConvId, true);
    3016            2 :         sendNotification(newConvId);
    3017            2 :         return newConvId;
    3018            0 :     };
    3019              : 
    3020            8 :     if (!conv) {
    3021            1 :         auto contacts = pimpl_->accountManager_->getContacts(false);
    3022            2 :         for (const auto& contact : contacts) {
    3023            1 :             if (contact.second.conversationId == conversationId) {
    3024            1 :                 const std::string& uri = contact.first.toString();
    3025            1 :                 handleNewConversation(uri);
    3026            1 :             }
    3027              :         }
    3028              : 
    3029            1 :         return pimpl_->removeConversation(conversationId);
    3030            1 :     }
    3031              : 
    3032            7 :     if (conv->mode() == ConversationMode::ONE_TO_ONE) {
    3033            1 :         auto members = conv->getMembers(true, false, false);
    3034            1 :         if (members.empty()) {
    3035            0 :             return false;
    3036              :         }
    3037              : 
    3038            3 :         for (const auto& m : members) {
    3039            2 :             const auto& uri = m.at("uri");
    3040              : 
    3041            2 :             if (members.size() == 1 && uri == pimpl_->username_) {
    3042            0 :                 if (conv->getInitialMembers().size() == 1 && conv->getInitialMembers()[0] == pimpl_->username_) {
    3043              :                     // Self conversation, create new conversation and remove the old one
    3044            0 :                     handleNewConversation(uri);
    3045              :                 } else {
    3046            0 :                     existingConvId = findMatchingOneToOneConversation(conversationId, conv->memberUris("", {}));
    3047            0 :                     if (existingConvId.empty()) {
    3048              :                         // If left with only ended conversation of peer
    3049            0 :                         for (const auto& otherMember : conv->getInitialMembers()) {
    3050            0 :                             if (otherMember != pimpl_->username_) {
    3051            0 :                                 handleNewConversation(otherMember);
    3052              :                             }
    3053            0 :                         }
    3054              :                     }
    3055              :                 }
    3056            0 :                 break;
    3057              :             }
    3058              : 
    3059            2 :             if (uri == pimpl_->username_)
    3060            1 :                 continue;
    3061              : 
    3062            1 :             existingConvId = findMatchingOneToOneConversation(conversationId, conv->memberUris("", {}));
    3063            1 :             if (!existingConvId.empty()) {
    3064              :                 // Found an existing conversation, just update the contact
    3065            0 :                 pimpl_->accountManager_->updateContactConversation(uri, existingConvId, true);
    3066            0 :                 sendNotification(existingConvId);
    3067              :             } else {
    3068              :                 // No existing conversation found, create a new one
    3069            1 :                 handleNewConversation(uri);
    3070              :             }
    3071              :         }
    3072            1 :     }
    3073              : 
    3074            7 :     return pimpl_->removeConversation(conversationId);
    3075            9 : }
    3076              : 
    3077              : std::string
    3078            1 : ConversationModule::findMatchingOneToOneConversation(const std::string& excludedConversationId,
    3079              :                                                      const std::set<std::string>& targetUris) const
    3080              : {
    3081            1 :     std::lock_guard lk(pimpl_->conversationsMtx_);
    3082            2 :     for (const auto& [otherConvId, otherConvPtr] : pimpl_->conversations_) {
    3083            1 :         if (otherConvId == excludedConversationId)
    3084            1 :             continue;
    3085              : 
    3086            0 :         std::lock_guard lk(otherConvPtr->mtx);
    3087            0 :         if (!otherConvPtr->conversation || otherConvPtr->conversation->mode() != ConversationMode::ONE_TO_ONE)
    3088            0 :             continue;
    3089              : 
    3090            0 :         const auto& info = otherConvPtr->info;
    3091            0 :         if (info.removed != TimePoint {} && info.isRemoved())
    3092            0 :             continue;
    3093              : 
    3094            0 :         auto otherUris = otherConvPtr->conversation->memberUris();
    3095              : 
    3096            0 :         if (otherUris == targetUris)
    3097            0 :             return otherConvId;
    3098            0 :     }
    3099              : 
    3100            1 :     return {};
    3101            1 : }
    3102              : 
    3103              : bool
    3104           63 : ConversationModule::isHosting(const std::string& conversationId, const std::string& confId) const
    3105              : {
    3106           63 :     if (conversationId.empty()) {
    3107           53 :         std::lock_guard lk(pimpl_->conversationsMtx_);
    3108           53 :         return std::find_if(pimpl_->conversations_.cbegin(),
    3109           53 :                             pimpl_->conversations_.cend(),
    3110           10 :                             [&](const auto& conv) {
    3111           10 :                                 return conv.second->conversation && conv.second->conversation->isHosting(confId);
    3112              :                             })
    3113          106 :                != pimpl_->conversations_.cend();
    3114           63 :     } else if (auto conv = pimpl_->getConversation(conversationId)) {
    3115           10 :         if (conv->conversation) {
    3116           10 :             return conv->conversation->isHosting(confId);
    3117              :         }
    3118           10 :     }
    3119            0 :     return false;
    3120              : }
    3121              : 
    3122              : std::vector<std::map<std::string, std::string>>
    3123           16 : ConversationModule::getActiveCalls(const std::string& conversationId) const
    3124              : {
    3125           16 :     return pimpl_->withConversation(conversationId,
    3126           48 :                                     [](const auto& conversation) { return conversation.currentCalls(); });
    3127              : }
    3128              : 
    3129              : std::shared_ptr<SIPCall>
    3130           22 : ConversationModule::call(const std::string& url,
    3131              :                          const std::vector<libjami::MediaMap>& mediaList,
    3132              :                          std::function<void(const std::string&, const DeviceId&, const std::shared_ptr<SIPCall>&)>&& cb)
    3133              : {
    3134          154 :     std::string conversationId = "", confId = "", uri = "", deviceId = "";
    3135           22 :     if (url.find('/') == std::string::npos) {
    3136           13 :         conversationId = url;
    3137              :     } else {
    3138            9 :         auto parameters = jami::split_string(url, '/');
    3139            9 :         if (parameters.size() != 4) {
    3140            0 :             JAMI_ERROR("Incorrect url {:s}", url);
    3141            0 :             return {};
    3142              :         }
    3143            9 :         conversationId = parameters[0];
    3144            9 :         uri = parameters[1];
    3145            9 :         deviceId = parameters[2];
    3146            9 :         confId = parameters[3];
    3147            9 :     }
    3148              : 
    3149           22 :     auto conv = pimpl_->getConversation(conversationId);
    3150           22 :     if (!conv)
    3151            0 :         return {};
    3152           22 :     std::unique_lock lk(conv->mtx);
    3153           22 :     if (!conv->conversation) {
    3154            0 :         JAMI_ERROR("Conversation {:s} not found", conversationId);
    3155            0 :         return {};
    3156              :     }
    3157              : 
    3158              :     // Check if we want to join a specific conference
    3159              :     // So, if confId is specified or if there is some activeCalls
    3160              :     // or if we are the default host.
    3161           22 :     auto activeCalls = conv->conversation->currentCalls();
    3162           22 :     auto infos = conv->conversation->infos();
    3163           44 :     auto itRdvAccount = infos.find("rdvAccount");
    3164           22 :     auto itRdvDevice = infos.find("rdvDevice");
    3165           22 :     auto sendCallRequest = false;
    3166           22 :     if (!confId.empty()) {
    3167            9 :         sendCallRequest = true;
    3168           36 :         JAMI_DEBUG("Calling self, join conference");
    3169           13 :     } else if (!activeCalls.empty()) {
    3170              :         // Else, we try to join active calls
    3171            0 :         sendCallRequest = true;
    3172            0 :         auto& ac = *activeCalls.rbegin();
    3173            0 :         confId = ac.at("id");
    3174            0 :         uri = ac.at("uri");
    3175            0 :         deviceId = ac.at("device");
    3176           13 :     } else if (itRdvAccount != infos.end() && itRdvDevice != infos.end() && !itRdvAccount->second.empty()) {
    3177              :         // Else, creates "to" (accountId/deviceId/conversationId/confId) and ask remote host
    3178            3 :         sendCallRequest = true;
    3179            3 :         uri = itRdvAccount->second;
    3180            3 :         deviceId = itRdvDevice->second;
    3181            3 :         confId = "0";
    3182           12 :         JAMI_DEBUG("Remote host detected. Calling {:s} on device {:s}", uri, deviceId);
    3183              :     }
    3184           22 :     lk.unlock();
    3185              : 
    3186           22 :     auto account = pimpl_->account_.lock();
    3187           22 :     std::vector<libjami::MediaMap> mediaMap = mediaList.empty() ? MediaAttribute::mediaAttributesToMediaMaps(
    3188           60 :                                                                       pimpl_->account_.lock()->createDefaultMediaList(
    3189           41 :                                                                           pimpl_->account_.lock()->isVideoEnabled()))
    3190           41 :                                                                 : mediaList;
    3191              : 
    3192           22 :     if (!sendCallRequest || (uri == pimpl_->username_ && deviceId == pimpl_->deviceId_)) {
    3193           11 :         confId = confId == "0" ? Manager::instance().callFactory.getNewCallID() : confId;
    3194              :         // TODO attach host with media list
    3195           11 :         hostConference(conversationId, confId, "", mediaMap);
    3196           11 :         return {};
    3197              :     }
    3198              : 
    3199              :     // Else we need to create a call
    3200           11 :     auto& manager = Manager::instance();
    3201           11 :     std::shared_ptr<SIPCall> call = manager.callFactory.newSipCall(account, Call::CallType::OUTGOING, mediaMap);
    3202              : 
    3203           11 :     if (not call)
    3204            0 :         return {};
    3205              : 
    3206           11 :     auto callUri = fmt::format("{}/{}/{}/{}", conversationId, uri, deviceId, confId);
    3207           44 :     account->getIceOptions([call,
    3208           11 :                             accountId = account->getAccountID(),
    3209              :                             callUri,
    3210           11 :                             uri = std::move(uri),
    3211              :                             conversationId,
    3212              :                             deviceId,
    3213           11 :                             cb = std::move(cb)](auto&& opts) {
    3214           11 :         if (call->isIceEnabled()) {
    3215           11 :             if (not call->createIceMediaTransport(false)
    3216           22 :                 or not call->initIceMediaTransport(true, std::forward<dhtnet::IceTransportOptions>(opts))) {
    3217            0 :                 return;
    3218              :             }
    3219              :         }
    3220           44 :         JAMI_DEBUG("New outgoing call with {}", uri);
    3221           11 :         call->setPeerNumber(uri);
    3222           11 :         call->setPeerUri("swarm:" + uri);
    3223              : 
    3224           44 :         JAMI_DEBUG("Calling: {:s}", callUri);
    3225           11 :         call->setState(Call::ConnectionState::TRYING);
    3226           11 :         call->setPeerNumber(callUri);
    3227           11 :         call->setPeerUri("rdv:" + callUri);
    3228           22 :         call->addStateListener(
    3229           22 :             [accountId, conversationId](Call::CallState call_state, Call::ConnectionState cnx_state, int) {
    3230           63 :                 if (cnx_state == Call::ConnectionState::DISCONNECTED && call_state == Call::CallState::MERROR) {
    3231            2 :                     emitSignal<libjami::ConfigurationSignal::NeedsHost>(accountId, conversationId);
    3232            2 :                     return true;
    3233              :                 }
    3234           61 :                 return true;
    3235              :             });
    3236           11 :         cb(callUri, DeviceId(deviceId), call);
    3237              :     });
    3238              : 
    3239           11 :     return call;
    3240           22 : }
    3241              : 
    3242              : void
    3243           14 : ConversationModule::hostConference(const std::string& conversationId,
    3244              :                                    const std::string& confId,
    3245              :                                    const std::string& callId,
    3246              :                                    const std::vector<libjami::MediaMap>& mediaList)
    3247              : {
    3248           14 :     auto acc = pimpl_->account_.lock();
    3249           14 :     if (!acc)
    3250            0 :         return;
    3251           14 :     auto conf = acc->getConference(confId);
    3252           14 :     auto createConf = !conf;
    3253           14 :     std::shared_ptr<SIPCall> call;
    3254           14 :     if (!callId.empty()) {
    3255            3 :         call = std::dynamic_pointer_cast<SIPCall>(acc->getCall(callId));
    3256            3 :         if (!call) {
    3257            0 :             JAMI_WARNING("No call with id {} found", callId);
    3258            0 :             return;
    3259              :         }
    3260              :     }
    3261           14 :     if (createConf) {
    3262           14 :         conf = std::make_shared<Conference>(acc, confId);
    3263           14 :         acc->attach(conf);
    3264              :     }
    3265              : 
    3266           14 :     if (!callId.empty())
    3267            3 :         conf->addSubCall(callId);
    3268              : 
    3269           14 :     if (callId.empty())
    3270           11 :         conf->attachHost(mediaList);
    3271              : 
    3272           14 :     if (createConf) {
    3273           14 :         emitSignal<libjami::CallSignal::ConferenceCreated>(acc->getAccountID(), conversationId, conf->getConfId());
    3274              :     } else {
    3275            0 :         conf->reportMediaNegotiationStatus();
    3276            0 :         emitSignal<libjami::CallSignal::ConferenceChanged>(acc->getAccountID(), conf->getConfId(), conf->getStateStr());
    3277            0 :         return;
    3278              :     }
    3279              : 
    3280           14 :     auto conv = pimpl_->getConversation(conversationId);
    3281           14 :     if (!conv)
    3282            0 :         return;
    3283           14 :     std::unique_lock lk(conv->mtx);
    3284           14 :     if (!conv->conversation) {
    3285            0 :         JAMI_ERROR("Conversation {} not found", conversationId);
    3286            0 :         return;
    3287              :     }
    3288              :     // Add commit to conversation
    3289           14 :     auto message = CommitMessage::conferenceHostingStart(conf->getConfId(), pimpl_->deviceId_, pimpl_->username_);
    3290           28 :     conv->conversation->hostConference(std::move(message),
    3291           28 :                                        [w = pimpl_->weak(), conversationId](bool ok, const std::string& commitId) {
    3292           14 :                                            if (ok) {
    3293           14 :                                                if (auto shared = w.lock())
    3294           42 :                                                    shared->sendMessageNotification(conversationId, true, commitId);
    3295              :                                            } else {
    3296            0 :                                                JAMI_ERROR("Failed to send message to conversation {}", conversationId);
    3297              :                                            }
    3298           14 :                                        });
    3299              : 
    3300              :     // When conf finished = remove host & commit
    3301              :     // Master call, so when it's stopped, the conference will be stopped (as we use the hold
    3302              :     // state for detaching the call)
    3303           28 :     conf->onShutdown([w = pimpl_->weak(),
    3304           14 :                       accountUri = pimpl_->username_,
    3305           14 :                       confId = conf->getConfId(),
    3306              :                       conversationId,
    3307              :                       conv](int duration) {
    3308           14 :         auto shared = w.lock();
    3309           14 :         if (shared) {
    3310           10 :             auto message = CommitMessage::conferenceHostingEnd(confId, shared->deviceId_, accountUri, duration);
    3311              : 
    3312           10 :             std::lock_guard lk(conv->mtx);
    3313           10 :             if (!conv->conversation) {
    3314            0 :                 JAMI_ERROR("Conversation {} not found", conversationId);
    3315            0 :                 return;
    3316              :             }
    3317           10 :             conv->conversation
    3318           10 :                 ->removeActiveConference(std::move(message), [w, conversationId](bool ok, const std::string& commitId) {
    3319           10 :                     if (ok) {
    3320           10 :                         if (auto shared = w.lock()) {
    3321           30 :                             shared->sendMessageNotification(conversationId, true, commitId);
    3322           10 :                         }
    3323              :                     } else {
    3324            0 :                         JAMI_ERROR("Failed to send message to conversation {}", conversationId);
    3325              :                     }
    3326           10 :                 });
    3327           10 :         }
    3328           14 :     });
    3329           14 : }
    3330              : 
    3331              : std::map<std::string, ConvInfo>
    3332          882 : ConversationModule::convInfos(const std::string& accountId)
    3333              : {
    3334          882 :     return convInfosFromPath(fileutils::get_data_dir() / accountId);
    3335              : }
    3336              : 
    3337              : std::map<std::string, ConvInfo>
    3338          925 : ConversationModule::convInfosFromPath(const std::filesystem::path& path)
    3339              : {
    3340          925 :     std::map<std::string, ConvInfo> convInfos;
    3341              :     try {
    3342              :         // read file
    3343          925 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convInfo"));
    3344          925 :         auto file = fileutils::loadFile("convInfo", path);
    3345              :         // load values
    3346          925 :         msgpack::unpacked result;
    3347          925 :         msgpack::unpack(result, (const char*) file.data(), file.size());
    3348          923 :         result.get().convert(convInfos);
    3349          931 :     } catch (const std::exception& e) {
    3350            8 :         JAMI_WARNING("[convInfo] error loading convInfo: {}", e.what());
    3351            2 :     }
    3352          925 :     return convInfos;
    3353            0 : }
    3354              : 
    3355              : std::map<std::string, ConversationRequest>
    3356          870 : ConversationModule::convRequests(const std::string& accountId)
    3357              : {
    3358          870 :     return convRequestsFromPath(fileutils::get_data_dir() / accountId);
    3359              : }
    3360              : 
    3361              : std::map<std::string, ConversationRequest>
    3362          913 : ConversationModule::convRequestsFromPath(const std::filesystem::path& path)
    3363              : {
    3364          913 :     std::map<std::string, ConversationRequest> convRequests;
    3365              :     try {
    3366              :         // read file
    3367          913 :         std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convRequests"));
    3368          913 :         auto file = fileutils::loadFile("convRequests", path);
    3369              :         // load values
    3370          913 :         msgpack::unpacked result;
    3371          913 :         msgpack::unpack(result, (const char*) file.data(), file.size(), 0);
    3372          913 :         result.get().convert(convRequests);
    3373          913 :     } catch (const std::exception& e) {
    3374            0 :         JAMI_WARNING("[convInfo] error loading convInfo: {}", e.what());
    3375            0 :     }
    3376          913 :     return convRequests;
    3377            0 : }
    3378              : 
    3379              : void
    3380          183 : ConversationModule::addConvInfo(const ConvInfo& info)
    3381              : {
    3382          183 :     pimpl_->addConvInfo(info);
    3383          183 : }
    3384              : 
    3385              : void
    3386         2170 : ConversationModule::Impl::setConversationMembers(const std::string& convId, const std::set<std::string>& members)
    3387              : {
    3388         2170 :     if (auto conv = getConversation(convId)) {
    3389         2170 :         std::lock_guard lk(conv->mtx);
    3390         2170 :         conv->info.members = members;
    3391         2170 :         addConvInfo(conv->info);
    3392         4340 :     }
    3393         2170 : }
    3394              : 
    3395              : std::shared_ptr<Conversation>
    3396            1 : ConversationModule::getConversation(const std::string& convId)
    3397              : {
    3398            1 :     if (auto conv = pimpl_->getConversation(convId)) {
    3399            1 :         std::lock_guard lk(conv->mtx);
    3400            1 :         return conv->conversation;
    3401            2 :     }
    3402            0 :     return nullptr;
    3403              : }
    3404              : 
    3405              : std::shared_ptr<dhtnet::ChannelSocket>
    3406         5056 : ConversationModule::gitSocket(std::string_view deviceId, std::string_view convId) const
    3407              : {
    3408         5056 :     if (auto conv = pimpl_->getConversation(convId)) {
    3409         5056 :         std::lock_guard lk(conv->mtx);
    3410         5057 :         if (conv->conversation)
    3411         4582 :             return conv->conversation->gitSocket(DeviceId(deviceId));
    3412          475 :         else if (conv->pending)
    3413          473 :             return conv->pending->socket;
    3414        10114 :     }
    3415            2 :     return nullptr;
    3416              : }
    3417              : 
    3418              : void
    3419            0 : ConversationModule::addGitSocket(std::string_view deviceId,
    3420              :                                  std::string_view convId,
    3421              :                                  const std::shared_ptr<dhtnet::ChannelSocket>& channel)
    3422              : {
    3423            0 :     if (auto conv = pimpl_->getConversation(convId)) {
    3424            0 :         std::lock_guard lk(conv->mtx);
    3425            0 :         conv->conversation->addGitSocket(DeviceId(deviceId), channel);
    3426            0 :     } else
    3427            0 :         JAMI_WARNING("addGitSocket: Unable to find conversation {:s}", convId);
    3428            0 : }
    3429              : 
    3430              : void
    3431          987 : ConversationModule::removeGitSocket(std::string_view deviceId, std::string_view convId)
    3432              : {
    3433         1939 :     pimpl_->withConversation(convId, [&](auto& conv) { conv.removeGitSocket(DeviceId(deviceId)); });
    3434          989 : }
    3435              : 
    3436              : void
    3437          711 : ConversationModule::shutdownConnections()
    3438              : {
    3439         1119 :     for (const auto& c : pimpl_->getSyncedConversations()) {
    3440          408 :         std::lock_guard lkc(c->mtx);
    3441          408 :         if (c->conversation)
    3442          372 :             c->conversation->shutdownConnections();
    3443          408 :         if (c->pending)
    3444           23 :             c->pending->socket = {};
    3445         1119 :     }
    3446          711 : }
    3447              : void
    3448         1026 : ConversationModule::addSwarmChannel(const std::string& conversationId, std::shared_ptr<dhtnet::ChannelSocket> channel)
    3449              : {
    3450         2030 :     pimpl_->withConversation(conversationId, [&](auto& conv) { conv.addSwarmChannel(std::move(channel)); });
    3451         1027 : }
    3452              : 
    3453              : void
    3454         1230 : ConversationModule::addKnownDevice(const std::string& peerUri, const DeviceId& deviceId)
    3455              : {
    3456         2059 :     for (const auto& conv : pimpl_->getConversations())
    3457          829 :         if (conv->isMember(peerUri))
    3458         1837 :             conv->connectNode(deviceId);
    3459         1230 : }
    3460              : 
    3461              : void
    3462            0 : ConversationModule::connectivityChanged()
    3463              : {
    3464            0 :     for (const auto& conv : pimpl_->getConversations())
    3465            0 :         conv->connectivityChanged();
    3466            0 : }
    3467              : 
    3468              : std::shared_ptr<Typers>
    3469            9 : ConversationModule::getTypers(const std::string& convId)
    3470              : {
    3471            9 :     if (auto c = pimpl_->getConversation(convId)) {
    3472            9 :         std::lock_guard lk(c->mtx);
    3473            9 :         if (c->conversation)
    3474            9 :             return c->conversation->typers();
    3475           18 :     }
    3476            0 :     return nullptr;
    3477              : }
    3478              : 
    3479              : void
    3480           16 : ConversationModule::initPresence()
    3481              : {
    3482           16 :     pimpl_->initPresence();
    3483           16 : }
    3484              : 
    3485              : } // namespace jami
        

Generated by: LCOV version 2.0-1