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.9 % 2020 1695
Test Date: 2026-06-13 09:18:46 Functions: 73.0 % 382 279

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

Generated by: LCOV version 2.0-1