LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversation_module.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1681 2008 83.7 %
Date: 2026-02-28 10:41:24 Functions: 266 363 73.3 %

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

Generated by: LCOV version 1.14