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: 1675 2000 83.8 %
Date: 2025-12-18 10:07:43 Functions: 268 365 73.4 %

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

Generated by: LCOV version 1.14