LCOV - code coverage report
Current view: top level - src/jamidht - conversation_module.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 83.1 % 2073 1722
Test Date: 2026-07-29 09:02:12 Functions: 93.8 % 192 180

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

Generated by: LCOV version 2.0-1