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: 1704 2084 81.8 %
Date: 2025-10-16 08:11:43 Functions: 259 360 71.9 %

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

Generated by: LCOV version 1.14