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: 1620 2075 78.1 %
Date: 2025-09-15 07:46:53 Functions: 242 358 67.6 %

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

Generated by: LCOV version 1.14