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: 1696 2015 84.2 %
Date: 2026-04-22 10:25:21 Functions: 273 367 74.4 %

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

Generated by: LCOV version 1.14