LCOV - code coverage report
Current view: top level - src/jamidht - conversation_module.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 84.2 % 2391 2013
Test Date: 2026-09-13 09:08:58 Functions: 94.8 % 212 201

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

Generated by: LCOV version 2.0-1