LCOV - code coverage report
Current view: top level - src/jamidht - conversation.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 80.4 % 2070 1664
Test Date: 2026-08-23 08:52:56 Functions: 82.7 % 196 162

            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.h"
      19              : 
      20              : #include "account_const.h"
      21              : #include "jamiaccount.h"
      22              : #include "jamidht/collaborative_editing.h"
      23              : #include "client/jami_signal.h"
      24              : #include "swarm/swarm_manager.h"
      25              : #include "conversationrepository.h"
      26              : #include "timestamp.h"
      27              : 
      28              : #ifdef ENABLE_PLUGIN
      29              : #include "manager.h"
      30              : #include "plugin/jamipluginmanager.h"
      31              : #include "plugin/streamdata.h"
      32              : #endif
      33              : #include "jami/conversation_interface.h"
      34              : #include "jami/configurationmanager_interface.h"
      35              : 
      36              : #include "fileutils.h"
      37              : #include "json_utils.h"
      38              : #include "logger.h"
      39              : #include "presence_manager.h"
      40              : #include "string_utils.h"
      41              : 
      42              : #include <opendht/thread_pool.h>
      43              : #include <opendht/infohash.h>
      44              : #include <fmt/compile.h>
      45              : #include <asio/error_code.hpp>
      46              : 
      47              : #include <algorithm>
      48              : #include <memory>
      49              : #include <charconv>
      50              : #include <set>
      51              : #include <string_view>
      52              : #include <tuple>
      53              : #include <optional>
      54              : #include <utility>
      55              : #include <deque>
      56              : #include <chrono>
      57              : #include <ctime>
      58              : #include <functional>
      59              : #include <atomic>
      60              : #include <ranges>
      61              : #include <regex>
      62              : 
      63              : namespace jami {
      64              : 
      65              : static const char* const LAST_MODIFIED = "lastModified";
      66              : 
      67              : namespace {
      68              : 
      69              : // Read a timestamp from JSON, preferring the millisecond key and falling
      70              : // back to the legacy seconds key (written by older devices).
      71              : TimePoint
      72          643 : timePointFromJson(const Json::Value& json, const char* msKey, const char* secondsKey)
      73              : {
      74          643 :     if (json.isMember(msKey))
      75          311 :         return timePointFromMilliseconds(json[msKey].asLargestInt());
      76          332 :     return timePointFromSeconds(json[secondsKey].asLargestInt());
      77              : }
      78              : 
      79              : // Resolve a timestamp from msgpack values, preferring milliseconds.
      80              : TimePoint
      81          889 : resolveTimePoint(const std::optional<int64_t>& ms, int64_t seconds)
      82              : {
      83          889 :     return ms ? timePointFromMilliseconds(*ms) : timePointFromSeconds(seconds);
      84              : }
      85              : 
      86              : } // namespace
      87              : 
      88           17 : ConvInfo::ConvInfo(const Json::Value& json)
      89              : {
      90           17 :     id = json[ConversationMapKeys::ID].asString();
      91           17 :     created = timePointFromJson(json, ConversationMapKeys::CREATED_MS, ConversationMapKeys::CREATED);
      92           17 :     removed = timePointFromJson(json, ConversationMapKeys::REMOVED_MS, ConversationMapKeys::REMOVED);
      93           17 :     erased = timePointFromJson(json, ConversationMapKeys::ERASED_MS, ConversationMapKeys::ERASED);
      94           42 :     for (const auto& v : json[ConversationMapKeys::MEMBERS]) {
      95           25 :         members.emplace(v["uri"].asString());
      96              :     }
      97           17 :     lastDisplayed = json[ConversationMapKeys::LAST_DISPLAYED].asString();
      98           17 :     if (json.isMember(ConversationMapKeys::MODE))
      99           15 :         mode = static_cast<ConversationMode>(json[ConversationMapKeys::MODE].asInt());
     100           17 :     if (json.isMember(ConversationMapKeys::INVITED)) {
     101            3 :         const auto& invitedJson = json[ConversationMapKeys::INVITED];
     102            7 :         for (const auto& uri : invitedJson.getMemberNames())
     103            7 :             invited[uri] = timePointFromMilliseconds(invitedJson[uri].asLargestInt());
     104              :     }
     105           17 : }
     106              : 
     107              : Json::Value
     108           31 : ConvInfo::toJson() const
     109              : {
     110           31 :     Json::Value json;
     111           31 :     json[ConversationMapKeys::ID] = id;
     112           31 :     json[ConversationMapKeys::CREATED] = Json::Int64(toSecondsSinceEpoch(created));
     113           31 :     json[ConversationMapKeys::CREATED_MS] = Json::Int64(toMillisecondsSinceEpoch(created));
     114           31 :     if (removed != TimePoint {}) {
     115            2 :         json[ConversationMapKeys::REMOVED] = Json::Int64(toSecondsSinceEpoch(removed));
     116            2 :         json[ConversationMapKeys::REMOVED_MS] = Json::Int64(toMillisecondsSinceEpoch(removed));
     117              :     }
     118           31 :     if (erased != TimePoint {}) {
     119            1 :         json[ConversationMapKeys::ERASED] = Json::Int64(toSecondsSinceEpoch(erased));
     120            1 :         json[ConversationMapKeys::ERASED_MS] = Json::Int64(toMillisecondsSinceEpoch(erased));
     121              :     }
     122           81 :     for (const auto& m : members) {
     123           50 :         Json::Value member;
     124           50 :         member["uri"] = m;
     125           50 :         json[ConversationMapKeys::MEMBERS].append(member);
     126           50 :     }
     127           31 :     json[ConversationMapKeys::LAST_DISPLAYED] = lastDisplayed;
     128           31 :     json[ConversationMapKeys::MODE] = static_cast<int>(mode);
     129           31 :     if (!invited.empty()) {
     130            7 :         Json::Value invitedJson;
     131           16 :         for (const auto& [uri, t] : invited)
     132            9 :             invitedJson[uri] = Json::Int64(toMillisecondsSinceEpoch(t));
     133            7 :         json[ConversationMapKeys::INVITED] = std::move(invitedJson);
     134            7 :     }
     135           31 :     return json;
     136            0 : }
     137              : 
     138              : void
     139          282 : ConvInfo::msgpack_unpack(const msgpack::object& o)
     140              : {
     141          282 :     if (o.type != msgpack::type::MAP)
     142            0 :         throw msgpack::type_error();
     143          282 :     int64_t createdSec = 0, removedSec = 0, erasedSec = 0;
     144          282 :     std::optional<int64_t> createdMs, removedMs, erasedMs;
     145          282 :     std::map<std::string, int64_t> invitedMs;
     146         3371 :     for (uint32_t i = 0; i < o.via.map.size; ++i) {
     147         3090 :         const auto& kv = o.via.map.ptr[i];
     148         3090 :         if (kv.key.type != msgpack::type::STR)
     149            0 :             continue;
     150         3090 :         std::string_view key(kv.key.via.str.ptr, kv.key.via.str.size);
     151         3091 :         if (key == ConversationMapKeys::ID)
     152          282 :             kv.val.convert(id);
     153         2806 :         else if (key == ConversationMapKeys::CREATED)
     154          281 :             kv.val.convert(createdSec);
     155         2528 :         else if (key == ConversationMapKeys::REMOVED)
     156          280 :             kv.val.convert(removedSec);
     157         2243 :         else if (key == ConversationMapKeys::ERASED)
     158          282 :             kv.val.convert(erasedSec);
     159         1966 :         else if (key == ConversationMapKeys::CREATED_MS)
     160          281 :             createdMs = kv.val.as<int64_t>();
     161         1685 :         else if (key == ConversationMapKeys::REMOVED_MS)
     162          281 :             removedMs = kv.val.as<int64_t>();
     163         1406 :         else if (key == ConversationMapKeys::ERASED_MS)
     164          281 :             erasedMs = kv.val.as<int64_t>();
     165         1125 :         else if (key == ConversationMapKeys::MEMBERS)
     166          281 :             kv.val.convert(members);
     167          844 :         else if (key == ConversationMapKeys::LAST_DISPLAYED)
     168          281 :             kv.val.convert(lastDisplayed);
     169          562 :         else if (key == ConversationMapKeys::MODE)
     170          282 :             kv.val.convert(mode);
     171          281 :         else if (key == ConversationMapKeys::INVITED)
     172          281 :             kv.val.convert(invitedMs);
     173              :     }
     174          281 :     created = resolveTimePoint(createdMs, createdSec);
     175          282 :     removed = resolveTimePoint(removedMs, removedSec);
     176          282 :     erased = resolveTimePoint(erasedMs, erasedSec);
     177          282 :     invited.clear();
     178          319 :     for (const auto& [uri, ms] : invitedMs)
     179           38 :         invited[uri] = timePointFromMilliseconds(ms);
     180          282 : }
     181              : 
     182              : void
     183            0 : ConvInfo::msgpack_object(msgpack::object* o, msgpack::zone& z) const
     184              : {
     185            0 :     int64_t createdSec = toSecondsSinceEpoch(created);
     186            0 :     int64_t removedSec = toSecondsSinceEpoch(removed);
     187            0 :     int64_t erasedSec = toSecondsSinceEpoch(erased);
     188            0 :     int64_t createdMs = toMillisecondsSinceEpoch(created);
     189            0 :     int64_t removedMs = toMillisecondsSinceEpoch(removed);
     190            0 :     int64_t erasedMs = toMillisecondsSinceEpoch(erased);
     191            0 :     std::map<std::string, int64_t> invitedMs;
     192            0 :     for (const auto& [uri, t] : invited)
     193            0 :         invitedMs[uri] = toMillisecondsSinceEpoch(t);
     194            0 :     msgpack::type::make_define_map(ConversationMapKeys::ID,
     195            0 :                                    id,
     196              :                                    ConversationMapKeys::CREATED,
     197              :                                    createdSec,
     198              :                                    ConversationMapKeys::REMOVED,
     199              :                                    removedSec,
     200              :                                    ConversationMapKeys::ERASED,
     201              :                                    erasedSec,
     202              :                                    ConversationMapKeys::MEMBERS,
     203            0 :                                    members,
     204              :                                    ConversationMapKeys::LAST_DISPLAYED,
     205            0 :                                    lastDisplayed,
     206              :                                    ConversationMapKeys::MODE,
     207            0 :                                    mode,
     208              :                                    ConversationMapKeys::CREATED_MS,
     209              :                                    createdMs,
     210              :                                    ConversationMapKeys::REMOVED_MS,
     211              :                                    removedMs,
     212              :                                    ConversationMapKeys::ERASED_MS,
     213              :                                    erasedMs,
     214              :                                    ConversationMapKeys::INVITED,
     215              :                                    invitedMs)
     216            0 :         .msgpack_object(o, z);
     217            0 : }
     218              : 
     219              : // ConversationRequest
     220          296 : ConversationRequest::ConversationRequest(const Json::Value& json)
     221              : {
     222          296 :     received = timePointFromJson(json, ConversationMapKeys::RECEIVED_MS, ConversationMapKeys::RECEIVED);
     223          296 :     declined = timePointFromJson(json, ConversationMapKeys::DECLINED_MS, ConversationMapKeys::DECLINED);
     224          296 :     from = json[ConversationMapKeys::FROM].asString();
     225          296 :     conversationId = json[ConversationMapKeys::CONVERSATIONID].asString();
     226          296 :     auto& md = json[ConversationMapKeys::METADATAS];
     227          593 :     for (const auto& member : md.getMemberNames()) {
     228          297 :         metadatas.emplace(member, md[member].asString());
     229          296 :     }
     230          296 : }
     231              : 
     232              : Json::Value
     233            2 : ConversationRequest::toJson() const
     234              : {
     235            2 :     Json::Value json;
     236            2 :     json[ConversationMapKeys::CONVERSATIONID] = conversationId;
     237            2 :     json[ConversationMapKeys::FROM] = from;
     238            2 :     json[ConversationMapKeys::RECEIVED] = Json::Int64(toSecondsSinceEpoch(received));
     239            2 :     json[ConversationMapKeys::RECEIVED_MS] = Json::Int64(toMillisecondsSinceEpoch(received));
     240            2 :     if (declined != TimePoint {}) {
     241            0 :         json[ConversationMapKeys::DECLINED] = Json::Int64(toSecondsSinceEpoch(declined));
     242            0 :         json[ConversationMapKeys::DECLINED_MS] = Json::Int64(toMillisecondsSinceEpoch(declined));
     243              :     }
     244            4 :     for (const auto& [key, value] : metadatas) {
     245            2 :         json[ConversationMapKeys::METADATAS][key] = value;
     246              :     }
     247            2 :     return json;
     248            0 : }
     249              : 
     250              : std::map<std::string, std::string>
     251          249 : ConversationRequest::toMap() const
     252              : {
     253          249 :     auto result = metadatas;
     254          498 :     result[ConversationMapKeys::ID] = conversationId;
     255          498 :     result[ConversationMapKeys::FROM] = from;
     256          249 :     if (declined != TimePoint {})
     257            3 :         result[ConversationMapKeys::DECLINED] = std::to_string(toSecondsSinceEpoch(declined));
     258          747 :     result[ConversationMapKeys::RECEIVED] = std::to_string(toSecondsSinceEpoch(received));
     259          249 :     return result;
     260            0 : }
     261              : 
     262              : void
     263           22 : ConversationRequest::msgpack_unpack(const msgpack::object& o)
     264              : {
     265           22 :     if (o.type != msgpack::type::MAP)
     266            0 :         throw msgpack::type_error();
     267           22 :     int64_t receivedSec = 0, declinedSec = 0;
     268           22 :     std::optional<int64_t> receivedMs, declinedMs;
     269          174 :     for (uint32_t i = 0; i < o.via.map.size; ++i) {
     270          152 :         const auto& kv = o.via.map.ptr[i];
     271          152 :         if (kv.key.type != msgpack::type::STR)
     272            0 :             continue;
     273          152 :         std::string_view key(kv.key.via.str.ptr, kv.key.via.str.size);
     274          152 :         if (key == ConversationMapKeys::FROM)
     275           22 :             kv.val.convert(from);
     276          130 :         else if (key == ConversationMapKeys::CONVERSATIONID)
     277           22 :             kv.val.convert(conversationId);
     278          108 :         else if (key == ConversationMapKeys::METADATAS)
     279           22 :             kv.val.convert(metadatas);
     280           86 :         else if (key == ConversationMapKeys::RECEIVED)
     281           22 :             kv.val.convert(receivedSec);
     282           64 :         else if (key == ConversationMapKeys::DECLINED)
     283           22 :             kv.val.convert(declinedSec);
     284           42 :         else if (key == ConversationMapKeys::RECEIVED_MS)
     285           21 :             receivedMs = kv.val.as<int64_t>();
     286           21 :         else if (key == ConversationMapKeys::DECLINED_MS)
     287           21 :             declinedMs = kv.val.as<int64_t>();
     288              :     }
     289           22 :     received = resolveTimePoint(receivedMs, receivedSec);
     290           22 :     declined = resolveTimePoint(declinedMs, declinedSec);
     291           22 : }
     292              : 
     293              : void
     294            0 : ConversationRequest::msgpack_object(msgpack::object* o, msgpack::zone& z) const
     295              : {
     296            0 :     int64_t receivedSec = toSecondsSinceEpoch(received);
     297            0 :     int64_t declinedSec = toSecondsSinceEpoch(declined);
     298            0 :     int64_t receivedMs = toMillisecondsSinceEpoch(received);
     299            0 :     int64_t declinedMs = toMillisecondsSinceEpoch(declined);
     300            0 :     msgpack::type::make_define_map(ConversationMapKeys::FROM,
     301            0 :                                    from,
     302              :                                    ConversationMapKeys::CONVERSATIONID,
     303            0 :                                    conversationId,
     304              :                                    ConversationMapKeys::METADATAS,
     305            0 :                                    metadatas,
     306              :                                    ConversationMapKeys::RECEIVED,
     307              :                                    receivedSec,
     308              :                                    ConversationMapKeys::DECLINED,
     309              :                                    declinedSec,
     310              :                                    ConversationMapKeys::RECEIVED_MS,
     311              :                                    receivedMs,
     312              :                                    ConversationMapKeys::DECLINED_MS,
     313              :                                    declinedMs)
     314            0 :         .msgpack_object(o, z);
     315            0 : }
     316              : 
     317              : using MessageList = std::list<std::shared_ptr<libjami::SwarmMessage>>;
     318              : 
     319              : struct History
     320              : {
     321              :     // While loading the history, we need to avoid:
     322              :     // - reloading history (can just be ignored)
     323              :     // - adding new commits (should wait for history to be loaded)
     324              :     std::mutex mutex {};
     325              :     std::condition_variable cv {};
     326              :     bool loading {false};
     327              :     MessageList messageList {};
     328              :     std::map<std::string, std::shared_ptr<libjami::SwarmMessage>> quickAccess {};
     329              :     std::map<std::string, std::list<std::shared_ptr<libjami::SwarmMessage>>> pendingEditions {};
     330              :     std::map<std::string, std::list<std::map<std::string, std::string>>> pendingReactions {};
     331              : };
     332              : 
     333              : class Conversation::Impl
     334              : {
     335              : private:
     336          447 :     Impl(std::unique_ptr<ConversationRepository>&& repository,
     337              :          const std::shared_ptr<JamiAccount>& account,
     338              :          std::vector<ConversationCommit>&& commits = {})
     339          447 :         : repository_(repository ? std::move(repository) : throw std::logic_error("Invalid repository"))
     340          429 :         , account_(account)
     341          429 :         , accountId_(account->getAccountID())
     342          429 :         , userId_(account->getUsername())
     343          858 :         , deviceId_(account->currentDeviceId())
     344          429 :         , swarmManager_(std::make_shared<SwarmManager>(
     345          429 :               NodeId(deviceId_),
     346          429 :               account->isMobile(),
     347          429 :               Manager::instance().getSeededRandomEngine(),
     348          429 :               [account = account_](const DeviceId& deviceId) {
     349          322 :                   if (auto acc = account.lock()) {
     350          322 :                       return acc->isConnectedWith(deviceId);
     351          322 :                   }
     352            0 :                   return false;
     353              :               },
     354          429 :               repository_->id(),
     355          858 :               [account = account_, conversationId = repository_->id(), deviceId = NodeId(deviceId_)]()
     356              :                   -> std::optional<MobileNodeInfo> {
     357            0 :                   auto acc = account.lock();
     358            0 :                   if (!acc || !acc->isMobile())
     359            0 :                       return std::nullopt;
     360            0 :                   const auto& identity = acc->identity();
     361            0 :                   if (!identity.first || !identity.second || !identity.second->issuer)
     362            0 :                       return std::nullopt;
     363              : 
     364            0 :                   const auto now = std::chrono::system_clock::now();
     365            0 :                   const auto certificateExpiry = identity.second->getExpiration();
     366            0 :                   const auto maximumExpiry = now + MAX_MOBILE_LEASE_DURATION;
     367            0 :                   const auto expiry = std::min(certificateExpiry, maximumExpiry);
     368            0 :                   if (expiry <= now)
     369            0 :                       return std::nullopt;
     370              : 
     371            0 :                   MobileLease lease {1,
     372            0 :                                      conversationId,
     373            0 :                                      identity.second->issuer->getId(),
     374              :                                      deviceId,
     375            0 :                                      toSecondsSinceEpoch(now),
     376            0 :                                      toSecondsSinceEpoch(expiry),
     377            0 :                                      {}};
     378            0 :                   lease.signature = identity.first->sign(mobileLeasePayload(lease));
     379            0 :                   return MobileNodeInfo {deviceId, std::move(lease)};
     380            0 :               },
     381            0 :               [this](const dht::InfoHash& issuerId) {
     382              :                   const auto members
     383            0 :                       = repository_->memberUris("", {MemberRole::INVITED, MemberRole::BANNED, MemberRole::LEFT});
     384            0 :                   return members.contains(issuerId.toString());
     385            0 :               },
     386            0 :               [account = account_](const NodeId& deviceId) -> std::shared_ptr<dht::crypto::Certificate> {
     387            0 :                   if (auto acc = account.lock())
     388            0 :                       return acc->certStore().getCertificate(deviceId.toString());
     389            0 :                   return {};
     390              :               },
     391            0 :               [account = account_](const NodeId& deviceId,
     392              :                                    std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb) {
     393            0 :                   if (auto acc = account.lock())
     394            0 :                       acc->findCertificate(deviceId, std::move(cb));
     395              :                   else
     396            0 :                       cb(nullptr);
     397            0 :               }))
     398          429 :         , transferManager_(std::make_shared<TransferManager>(accountId_,
     399              :                                                              "",
     400          429 :                                                              repository_->id(),
     401          429 :                                                              Manager::instance().getSeededRandomEngine()))
     402          429 :         , repoPath_(fileutils::get_data_dir() / accountId_ / "conversations" / repository_->id())
     403          429 :         , conversationDataPath_(fileutils::get_data_dir() / accountId_ / "conversation_data" / repository_->id())
     404          429 :         , fetchedPath_(conversationDataPath_ / ConversationDirectories::FETCHED)
     405          429 :         , sendingPath_(conversationDataPath_ / ConversationDirectories::SENDING)
     406          429 :         , preferencesPath_(conversationDataPath_ / ConversationDirectories::PREFERENCES)
     407          429 :         , statusPath_(conversationDataPath_ / ConversationDirectories::STATUS)
     408          429 :         , mobileNodesPath_(conversationDataPath_ / ConversationDirectories::MOBILE_NODES)
     409          429 :         , hostedCallsPath_(conversationDataPath_ / ConversationDirectories::HOSTED_CALLS)
     410          429 :         , activeCallsPath_(conversationDataPath_ / ConversationDirectories::ACTIVE_CALLS)
     411          429 :         , ioContext_(Manager::instance().ioContext())
     412         2592 :         , typers_(std::make_shared<Typers>(account, repository_->id()))
     413              :     {
     414          429 :         if (!commits.empty())
     415          211 :             initActiveCalls(repository_->convCommitsToMap(commits));
     416          429 :         loadStatus();
     417              :         // Restore mobility knowledge: in a swarm made of mostly mobile
     418              :         // devices, gossip alone is unavailable after a restart (nobody is
     419              :         // connected), so the persisted list is the only way to know which
     420              :         // devices need a wake-up notification.
     421          429 :         loadMobileNodes();
     422              :         // Self-contained callback: the swarm manager can outlive this Impl
     423              :         // (its timers hold shared_from_this), so do not capture `this`.
     424          858 :         swarmManager_->onMobileNodeInfosChanged(
     425          858 :             [path = mobileNodesPath_](const std::vector<MobileNodeInfo>& mobileNodes) {
     426            0 :                 std::lock_guard lk {dhtnet::fileutils::getFileLock(path)};
     427            0 :                 std::ofstream file(path, std::ios::trunc | std::ios::binary);
     428            0 :                 msgpack::pack(file, mobileNodes);
     429            0 :             });
     430          429 :         setupMemberCallback();
     431          483 :     }
     432              : 
     433          229 :     Impl(std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>&& repoAndCommits,
     434              :          const std::shared_ptr<JamiAccount>& account)
     435          229 :         : Impl(std::move(repoAndCommits.first), account, std::move(repoAndCommits.second))
     436          211 :     {}
     437              : 
     438              : public:
     439          184 :     Impl(const std::shared_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember = "")
     440          184 :         : Impl(ConversationRepository::createConversation(account, mode, otherMember), account)
     441          184 :     {}
     442              : 
     443           34 :     Impl(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId)
     444           34 :         : Impl(std::make_unique<ConversationRepository>(account, conversationId), account)
     445           34 :     {}
     446              : 
     447          233 :     Impl(const std::shared_ptr<JamiAccount>& account, const std::string& remoteDevice, const std::string& conversationId)
     448          233 :         : Impl(ConversationRepository::cloneConversation(account, remoteDevice, conversationId), account)
     449          211 :     {}
     450              : 
     451         2878 :     std::string toString() const
     452              :     {
     453        11509 :         return fmt::format(FMT_COMPILE("[Account {}] [Conversation {}]"), accountId_, repository_->id());
     454              :     }
     455              : 
     456            0 :     std::vector<std::map<std::string, std::string>> getConnectivity() const
     457              :     {
     458            0 :         return swarmManager_->getRoutingTableInfo();
     459              :     }
     460              : 
     461              :     mutable std::string fmtStr_;
     462              : 
     463          428 :     ~Impl() { stopTracking(); }
     464              : 
     465              :     /**
     466              :      * If, for whatever reason, the daemon is stopped while hosting a conference,
     467              :      * we need to announce the end of this call when restarting.
     468              :      * To avoid to keep active calls forever.
     469              :      */
     470              :     std::vector<std::string> commitsEndedCalls();
     471              :     bool isAdmin() const;
     472              : 
     473          366 :     void announce(const std::string& commitId, bool commitFromSelf = false)
     474              :     {
     475          366 :         std::vector<std::string> vec;
     476          366 :         if (!commitId.empty())
     477          363 :             vec.emplace_back(commitId);
     478          366 :         announce(vec, commitFromSelf);
     479          366 :     }
     480              : 
     481          382 :     void announce(const std::vector<std::string>& commits, bool commitFromSelf = false)
     482              :     {
     483          382 :         std::vector<ConversationCommit> convcommits;
     484          382 :         convcommits.reserve(commits.size());
     485          777 :         for (const auto& cid : commits) {
     486          395 :             if (auto commit = repository_->getCommit(cid)) {
     487          395 :                 convcommits.emplace_back(*commit);
     488          395 :             }
     489              :         }
     490          383 :         announce(repository_->convCommitsToMap(convcommits), commitFromSelf);
     491          382 :     }
     492              : 
     493              :     /**
     494              :      * Initialize activeCalls_ from the list of commits in the repository
     495              :      * @param commits Commits in reverse chronological order (i.e. from newest to oldest)
     496              :      */
     497          211 :     void initActiveCalls(const std::vector<std::map<std::string, std::string>>& commits) const
     498              :     {
     499          211 :         std::unordered_set<std::string> invalidHostUris;
     500          211 :         std::unordered_set<std::string> invalidCallIds;
     501              : 
     502          211 :         std::lock_guard lk(activeCallsMtx_);
     503         1289 :         for (const auto& commit : commits) {
     504         2156 :             if (commit.at(CommitKey::TYPE) == CommitType::MEMBER) {
     505              :                 // Each commit of type MEMBER has an "action" field whose value can be one
     506              :                 // of the following: "add", "join", "remove", "ban", "unban"
     507              :                 // In the case of "remove" and "ban", we need to add the member's URI to
     508              :                 // invalidHostUris to ensure that any call they may have started in the past
     509              :                 // is no longer considered active.
     510              :                 // For the other actions, there's no harm in adding the member's URI anyway,
     511              :                 // since it's not possible to start hosting a call before joining the swarm (or
     512              :                 // before getting unbanned in the case of previously banned members).
     513         1640 :                 invalidHostUris.emplace(commit.at(CommitKey::URI));
     514          777 :             } else if (commit.find(CommitKey::CONF_ID) != commit.end() && commit.find(CommitKey::URI) != commit.end()
     515          519 :                        && commit.find(CommitKey::DEVICE) != commit.end()) {
     516              :                 // The commit indicates either the end or the beginning of a call
     517              :                 // (depending on whether there is a "duration" field or not).
     518            1 :                 auto convId = repository_->id();
     519            2 :                 auto confId = commit.at(CommitKey::CONF_ID);
     520            2 :                 auto uri = commit.at(CommitKey::URI);
     521            1 :                 auto device = commit.at(CommitKey::DEVICE);
     522              : 
     523            3 :                 if (commit.find(CommitKey::DURATION) == commit.end()
     524            1 :                     && invalidCallIds.find(confId) == invalidCallIds.end()
     525            3 :                     && invalidHostUris.find(uri) == invalidHostUris.end()) {
     526            1 :                     std::map<std::string, std::string> activeCall;
     527            2 :                     activeCall["id"] = confId;
     528            2 :                     activeCall["uri"] = uri;
     529            1 :                     activeCall["device"] = device;
     530            1 :                     activeCalls_.emplace_back(activeCall);
     531            1 :                     JAMI_LOG("swarm:{} new active call detected: {} (on device {}, account {})",
     532              :                              convId,
     533              :                              confId,
     534              :                              device,
     535              :                              uri);
     536            1 :                 }
     537              :                 // Even if the call was active, we still add its ID to invalidCallIds to make sure it
     538              :                 // doesn't get added a second time. (This shouldn't happen normally, but in practice
     539              :                 // there are sometimes multiple commits indicating the beginning of the same call.)
     540            1 :                 invalidCallIds.emplace(confId);
     541            1 :             }
     542              :         }
     543          211 :         saveActiveCalls();
     544          211 :         emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_, repository_->id(), activeCalls_);
     545          211 :     }
     546              : 
     547              :     /**
     548              :      * Update activeCalls_ via announced commits (in load or via new commits)
     549              :      * @param commit        Commit to check
     550              :      * @param eraseOnly     If we want to ignore added commits
     551              :      * @param emitSig    If we want to emit to client
     552              :      * @note eraseOnly is used by loadMessages. This is a fail-safe, this SHOULD NOT happen
     553              :      */
     554           84 :     void updateActiveCalls(const std::map<std::string, std::string>& commit,
     555              :                            bool eraseOnly = false,
     556              :                            bool emitSig = true) const
     557              :     {
     558           84 :         if (!repository_)
     559            0 :             return;
     560          168 :         if (commit.at(CommitKey::TYPE) == CommitType::MEMBER) {
     561              :             // In this case, we need to check if we are not removing a hosting member or device
     562           22 :             std::lock_guard lk(activeCallsMtx_);
     563           22 :             auto it = activeCalls_.begin();
     564           22 :             auto updateActives = false;
     565           23 :             while (it != activeCalls_.end()) {
     566            5 :                 if (it->at("uri") == commit.at(CommitKey::URI) || it->at("device") == commit.at(CommitKey::URI)) {
     567            5 :                     JAMI_DEBUG("Removing {:s} from the active calls, because {:s} left",
     568              :                                it->at("id"),
     569              :                                commit.at(CommitKey::URI));
     570            1 :                     it = activeCalls_.erase(it);
     571            1 :                     updateActives = true;
     572              :                 } else {
     573            0 :                     ++it;
     574              :                 }
     575              :             }
     576           22 :             if (updateActives) {
     577            1 :                 saveActiveCalls();
     578            1 :                 if (emitSig)
     579            1 :                     emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_,
     580            1 :                                                                                  repository_->id(),
     581            1 :                                                                                  activeCalls_);
     582              :             }
     583           22 :             return;
     584           22 :         }
     585              :         // Else, it's a call information
     586          351 :         if (commit.find(CommitKey::CONF_ID) != commit.end() && commit.find(CommitKey::URI) != commit.end()
     587          289 :             && commit.find(CommitKey::DEVICE) != commit.end()) {
     588           55 :             auto convId = repository_->id();
     589          110 :             auto confId = commit.at(CommitKey::CONF_ID);
     590          110 :             auto uri = commit.at(CommitKey::URI);
     591           55 :             auto device = commit.at(CommitKey::DEVICE);
     592           55 :             std::lock_guard lk(activeCallsMtx_);
     593           55 :             auto itActive = std::find_if(activeCalls_.begin(), activeCalls_.end(), [&](const auto& value) {
     594          168 :                 return value.at("id") == confId && value.at("uri") == uri && value.at("device") == device;
     595              :             });
     596          165 :             if (commit.find(CommitKey::DURATION) == commit.end()) {
     597           31 :                 if (itActive == activeCalls_.end() && !eraseOnly) {
     598           31 :                     JAMI_DEBUG("swarm:{:s} new current call detected: {:s} on device {:s}, account {:s}",
     599              :                                convId,
     600              :                                confId,
     601              :                                device,
     602              :                                uri);
     603          155 :                     activeCalls_.emplace_back(std::map<std::string, std::string> {
     604              :                         {"id", confId},
     605              :                         {"uri", uri},
     606              :                         {"device", device},
     607          124 :                     });
     608           31 :                     saveActiveCalls();
     609           31 :                     if (emitSig)
     610           31 :                         emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_,
     611           31 :                                                                                      repository_->id(),
     612           31 :                                                                                      activeCalls_);
     613              :                 }
     614              :             } else {
     615           24 :                 if (itActive != activeCalls_.end()) {
     616           24 :                     itActive = activeCalls_.erase(itActive);
     617              :                     // Unlikely, but we must ensure that no duplicate exists
     618           48 :                     while (itActive != activeCalls_.end()) {
     619            0 :                         itActive = std::find_if(itActive, activeCalls_.end(), [&](const auto& value) {
     620            0 :                             return value.at("id") == confId && value.at("uri") == uri && value.at("device") == device;
     621              :                         });
     622            0 :                         if (itActive != activeCalls_.end()) {
     623            0 :                             JAMI_ERROR("Duplicate call found. (This is a bug)");
     624            0 :                             itActive = activeCalls_.erase(itActive);
     625              :                         }
     626              :                     }
     627              : 
     628           24 :                     if (eraseOnly) {
     629            0 :                         JAMI_WARNING("previous swarm:{:s} call finished detected: {:s} on device "
     630              :                                      "{:s}, account {:s}",
     631              :                                      convId,
     632              :                                      confId,
     633              :                                      device,
     634              :                                      uri);
     635              :                     } else {
     636           24 :                         JAMI_DEBUG("swarm:{:s} call finished: {:s} on device {:s}, account {:s}",
     637              :                                    convId,
     638              :                                    confId,
     639              :                                    device,
     640              :                                    uri);
     641              :                     }
     642              :                 }
     643           24 :                 saveActiveCalls();
     644           24 :                 if (emitSig)
     645           24 :                     emitSignal<libjami::ConfigurationSignal::ActiveCallsChanged>(accountId_,
     646           24 :                                                                                  repository_->id(),
     647           24 :                                                                                  activeCalls_);
     648              :             }
     649           55 :         }
     650           31 :     }
     651              : 
     652         1347 :     void announce(const std::vector<std::map<std::string, std::string>>& commits, bool commitFromSelf = false)
     653              :     {
     654         1347 :         if (!repository_)
     655            0 :             return;
     656         1346 :         auto convId = repository_->id();
     657         1346 :         if (repository_->mode() == ConversationMode::DOCUMENT) {
     658              :             // A document's commits are not messages: nothing here goes to the
     659              :             // clients' conversation views or to the plugins. What a merge brought
     660              :             // in -- checkpoints, attachments, a rename -- is replayed into the
     661              :             // live CRDT session instead. Member events still feed the internal
     662              :             // callback so the swarm's view of who to sync with stays fresh.
     663           60 :             bool memberEvent = false;
     664          120 :             for (const auto& c : commits)
     665          120 :                 memberEvent |= c.at(CommitKey::TYPE) == CommitType::MEMBER;
     666           60 :             if (memberEvent && onMembersChanged_)
     667           32 :                 onMembersChanged_(repository_->memberUris("", {}));
     668              :             // Nothing to replay for our own commits: what this device wrote came
     669              :             // out of the live session in the first place. From another thread:
     670              :             // announce() can run under writeMtx_ (a pull holds it) and the replay
     671              :             // asks the module for the conversation, which takes locks of its own.
     672           60 :             if (!commits.empty() && !commitFromSelf)
     673           19 :                 dht::ThreadPool::io().run([w = account_, parentId = repository_->parentConversationId(), convId] {
     674           19 :                     if (auto acc = w.lock())
     675           19 :                         if (auto collab = acc->collaborativeEditing())
     676           38 :                             collab->onRepositoryUpdated(parentId, convId);
     677           19 :                 });
     678           60 :             return;
     679              :         }
     680         1285 :         auto ok = !commits.empty();
     681         2574 :         auto lastId = ok ? commits.rbegin()->at(ConversationMapKeys::ID) : "";
     682         1285 :         addToHistory(loadedHistory_, commits, true, commitFromSelf);
     683         1287 :         if (ok) {
     684         1285 :             bool announceMember = false;
     685         2624 :             for (const auto& c : commits) {
     686              :                 // Announce member events
     687         2680 :                 if (c.at(CommitKey::TYPE) == CommitType::MEMBER) {
     688         4805 :                     if (c.find(CommitKey::URI) != c.end() && c.find(CommitKey::ACTION) != c.end()) {
     689         1922 :                         const auto& uri = c.at(CommitKey::URI);
     690          961 :                         const auto& actionStr = c.at(CommitKey::ACTION);
     691          961 :                         auto action = -1;
     692          961 :                         if (actionStr == CommitAction::ADD)
     693          461 :                             action = 0;
     694          500 :                         else if (actionStr == CommitAction::JOIN)
     695          477 :                             action = 1;
     696           23 :                         else if (actionStr == CommitAction::REMOVE)
     697            3 :                             action = 2;
     698           20 :                         else if (actionStr == CommitAction::BAN)
     699           19 :                             action = 3;
     700            1 :                         else if (actionStr == CommitAction::UNBAN)
     701            1 :                             action = 4;
     702          961 :                         if (actionStr == CommitAction::BAN || actionStr == CommitAction::REMOVE) {
     703              :                             // In this case, a potential host was removed during a call.
     704           22 :                             updateActiveCalls(c);
     705           22 :                             typers_->removeTyper(uri);
     706              :                         }
     707          961 :                         if (action != -1) {
     708          961 :                             announceMember = true;
     709          961 :                             emitSignal<libjami::ConversationSignal::ConversationMemberEvent>(accountId_,
     710              :                                                                                              convId,
     711              :                                                                                              uri,
     712              :                                                                                              action);
     713              :                         }
     714              :                     }
     715          758 :                 } else if (c.at(CommitKey::TYPE) == CommitType::CALL_HISTORY) {
     716           62 :                     updateActiveCalls(c);
     717              :                 }
     718              : #ifdef ENABLE_PLUGIN
     719         1340 :                 if (auto& pluginChatManager = Manager::instance().getJamiPluginManager().getChatServicesManager();
     720         1340 :                     pluginChatManager.hasHandlers()) {
     721            6 :                     auto cm = std::make_shared<JamiMessage>(accountId_, convId, c.at("author") != userId_, c, false);
     722            3 :                     cm->isSwarm = true;
     723            3 :                     pluginChatManager.publishMessage(cm);
     724              : 
     725            3 :                     const auto editIt = c.find(CommitKey::EDIT);
     726            3 :                     const bool isEdit = editIt != c.end() && !editIt->second.empty();
     727            6 :                     const bool isText = c.at(CommitKey::TYPE) == CommitType::TEXT;
     728              : 
     729            6 :                     if (const auto idIt = c.find("id"); idIt != c.end() && (isEdit || isText)) {
     730            1 :                         const std::string msgId = idIt->second;
     731              : 
     732              :                         // Snapshot under lock.
     733            1 :                         libjami::SwarmMessage snap;
     734              :                         {
     735            1 :                             std::lock_guard const lk(loadedHistory_.mutex);
     736            1 :                             if (auto it = loadedHistory_.quickAccess.find(msgId);
     737            1 :                                 it != loadedHistory_.quickAccess.end()) {
     738            1 :                                 snap = *it->second;
     739              :                             }
     740            1 :                         }
     741              : 
     742            1 :                         if (!snap.id.empty()) {
     743              :                             // Transform outside lock.
     744            3 :                             std::vector<libjami::SwarmMessage> single {snap};
     745            1 :                             pluginChatManager.transformSwarmMessages(single, accountId_, convId);
     746            0 :                             auto& transformed = single[0];
     747            0 :                             const auto boIt = transformed.pluginData.find("bodyOverwrite");
     748            0 :                             const bool hasOverwrite = boIt != transformed.pluginData.end() && !boIt->second.empty();
     749              : 
     750              :                             // Write back + build signal under one lock, emit outside.
     751            0 :                             libjami::SwarmMessage signalMsg;
     752              :                             {
     753            0 :                                 std::lock_guard const lk(loadedHistory_.mutex);
     754            0 :                                 if (isEdit || hasOverwrite) {
     755            0 :                                     signalMsg = injectEditionOverwrites(msgId, hasOverwrite ? boIt->second : "");
     756              :                                 } else {
     757            0 :                                     if (auto it = loadedHistory_.quickAccess.find(msgId);
     758            0 :                                         it != loadedHistory_.quickAccess.end()) {
     759            0 :                                         it->second->pluginData["bodyOverwrite"] = "";
     760              :                                     }
     761              :                                 }
     762            0 :                             }
     763            0 :                             if (!signalMsg.id.empty()) {
     764            0 :                                 emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_,
     765              :                                                                                              convId,
     766              :                                                                                              signalMsg);
     767              :                             }
     768            0 :                         }
     769            0 :                     }
     770            3 :                 } else
     771              : #endif
     772              :                 {
     773              :                     // No active handlers: still notify clients when an edit arrives.
     774         2674 :                     if (const auto editIt = c.find(CommitKey::EDIT); editIt != c.end() && !editIt->second.empty()) {
     775            9 :                         libjami::SwarmMessage signalMsg;
     776              :                         {
     777            9 :                             std::lock_guard const lk(loadedHistory_.mutex);
     778            9 :                             if (auto originalIt = loadedHistory_.quickAccess.find(editIt->second);
     779            9 :                                 originalIt != loadedHistory_.quickAccess.end()) {
     780            9 :                                 signalMsg = *originalIt->second;
     781              :                             }
     782            9 :                         }
     783            9 :                         if (!signalMsg.id.empty()) {
     784            9 :                             emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, convId, signalMsg);
     785              :                         }
     786            9 :                     }
     787              :                 }
     788              :             }
     789              : 
     790         1284 :             if (announceMember && onMembersChanged_) {
     791          960 :                 onMembersChanged_(repository_->memberUris("", {}));
     792              :             }
     793              :         }
     794         1349 :     }
     795              : 
     796          429 :     void loadStatus()
     797              :     {
     798              :         try {
     799              :             // read file
     800          856 :             auto file = fileutils::loadFile(statusPath_);
     801              :             // load values
     802            2 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     803            2 :             std::lock_guard lk {messageStatusMtx_};
     804            2 :             oh.get().convert(messagesStatus_);
     805          429 :         } catch (const std::exception& e) {
     806          427 :         }
     807          429 :     }
     808         1707 :     void saveStatus()
     809              :     {
     810         1707 :         std::ofstream file(statusPath_, std::ios::trunc | std::ios::binary);
     811         1707 :         msgpack::pack(file, messagesStatus_);
     812         1707 :     }
     813              : 
     814          429 :     void loadMobileNodes()
     815              :     {
     816              :         try {
     817          858 :             auto file = fileutils::loadFile(mobileNodesPath_);
     818            0 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     819              :             try {
     820            0 :                 std::vector<MobileNodeInfo> nodes;
     821            0 :                 oh.get().convert(nodes);
     822            0 :                 swarmManager_->setMobileNodes(nodes);
     823            0 :             } catch (const std::exception&) {
     824            0 :                 std::vector<NodeId> nodes;
     825            0 :                 oh.get().convert(nodes);
     826            0 :                 swarmManager_->setMobileNodes(nodes);
     827            0 :             }
     828          429 :         } catch (const std::exception& e) {
     829          429 :             return;
     830          429 :         }
     831              :     }
     832              : 
     833           18 :     void loadActiveCalls() const
     834              :     {
     835              :         try {
     836              :             // read file
     837           29 :             auto file = fileutils::loadFile(activeCallsPath_);
     838              :             // load values
     839            7 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     840            7 :             std::lock_guard lk {activeCallsMtx_};
     841            7 :             oh.get().convert(activeCalls_);
     842           18 :         } catch (const std::exception& e) {
     843           11 :             return;
     844           11 :         }
     845              :     }
     846              : 
     847          285 :     void saveActiveCalls() const
     848              :     {
     849          285 :         std::ofstream file(activeCallsPath_, std::ios::trunc | std::ios::binary);
     850          285 :         msgpack::pack(file, activeCalls_);
     851          285 :     }
     852              : 
     853           18 :     void loadHostedCalls() const
     854              :     {
     855              :         try {
     856              :             // read file
     857           30 :             auto file = fileutils::loadFile(hostedCallsPath_);
     858              :             // load values
     859            6 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     860            6 :             std::lock_guard lk {activeCallsMtx_};
     861            6 :             oh.get().convert(hostedCalls_);
     862           18 :         } catch (const std::exception& e) {
     863           12 :             return;
     864           12 :         }
     865              :     }
     866              : 
     867           43 :     void saveHostedCalls() const
     868              :     {
     869           43 :         std::ofstream file(hostedCallsPath_, std::ios::trunc | std::ios::binary);
     870           43 :         msgpack::pack(file, hostedCalls_);
     871           43 :     }
     872              : 
     873              :     void voteUnban(const std::string& contactUri, const std::string_view type, const OnDoneCb& cb);
     874              : 
     875              :     std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited,
     876              :                                                                bool includeLeft,
     877              :                                                                bool includeBanned) const;
     878              : 
     879              :     std::vector<std::map<std::string, std::string>> getTrackedMembers() const;
     880              : 
     881         6561 :     std::string_view memberBanType(const std::string& uri) const
     882              :     {
     883         6561 :         auto crt = fmt::format("{}.crt", uri);
     884         6561 :         auto bannedMember = repoPath_ / MemberPath::BANNED / MemberPath::MEMBERS / crt;
     885         6559 :         if (std::filesystem::is_regular_file(bannedMember))
     886           22 :             return "members"sv;
     887         6540 :         auto bannedAdmin = repoPath_ / MemberPath::BANNED / MemberPath::ADMINS / crt;
     888         6536 :         if (std::filesystem::is_regular_file(bannedAdmin))
     889            0 :             return "admins"sv;
     890         6539 :         auto bannedInvited = repoPath_ / MemberPath::BANNED / MemberPath::INVITED / uri;
     891         6539 :         if (std::filesystem::is_regular_file(bannedInvited))
     892            0 :             return "invited"sv;
     893         6540 :         return {};
     894         6562 :     }
     895              : 
     896         7330 :     bool isDeviceBanned(const std::string& deviceId) const
     897              :     {
     898         7330 :         auto crt = fmt::format("{}.crt", deviceId);
     899         7331 :         auto bannedDevice = repoPath_ / MemberPath::BANNED / MemberPath::DEVICES / crt;
     900        14660 :         return std::filesystem::is_regular_file(bannedDevice);
     901         7332 :     }
     902              : 
     903         4663 :     std::shared_ptr<dhtnet::ChannelSocket> gitSocket(const DeviceId& deviceId) const
     904              :     {
     905         4663 :         std::lock_guard lk(gitSocketMtx_);
     906         4663 :         auto deviceSockets = gitSocketList_.find(deviceId);
     907         9326 :         return (deviceSockets != gitSocketList_.end()) ? deviceSockets->second.get() : nullptr;
     908         4663 :     }
     909              : 
     910         2029 :     void addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket)
     911              :     {
     912         2029 :         GitSocket replaced;
     913              :         {
     914         2029 :             std::lock_guard lk(gitSocketMtx_);
     915         2027 :             auto& slot = gitSocketList_[deviceId];
     916              :             // Re-registering the channel we already own must not close it.
     917         2029 :             if (slot.get() == socket)
     918         1014 :                 return;
     919         1015 :             replaced = std::move(slot);
     920         1015 :             slot = socket;
     921         2029 :         }
     922              :         // Closing the replaced channel, if any, happens here, outside the lock.
     923         2029 :     }
     924          981 :     void removeGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& expected = {})
     925              :     {
     926          981 :         GitSocket socket;
     927              :         {
     928          981 :             std::lock_guard lk(gitSocketMtx_);
     929          981 :             auto deviceSockets = gitSocketList_.find(deviceId);
     930          981 :             if (deviceSockets == gitSocketList_.end())
     931          466 :                 return;
     932              :             // A dead channel must not evict the one that replaced it.
     933          515 :             if (expected && deviceSockets->second.get() != expected)
     934            0 :                 return;
     935          515 :             socket = std::move(deviceSockets->second);
     936          514 :             gitSocketList_.erase(deviceSockets);
     937          982 :         }
     938              :         // Closing the channel outside the lock tells the peer to stop serving it, and wakes up
     939              :         // any fetch blocked reading from it, which would otherwise hold
     940              :         // ConversationRepository::opMtx_ until its read times out.
     941          982 :     }
     942              : 
     943              :     void disconnectFromDevice(const DeviceId& deviceId);
     944              : 
     945              :     /**
     946              :      * Remove all git sockets and all DRT nodes associated with the given peer.
     947              :      * This is used when a swarm member is banned to ensure that we stop syncing
     948              :      * with them or sending them message notifications.
     949              :      */
     950              :     void disconnectFromPeer(const std::string& peerUri);
     951              : 
     952              :     std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited, bool includeLeft) const;
     953              : 
     954              :     std::function<void()> bootstrapCb_;
     955              :     std::mutex bootstrapMtx_ {};
     956              : #ifdef LIBJAMI_TEST
     957              :     std::function<void(std::string, BootstrapStatus)> bootstrapCbTest_;
     958              : #endif
     959              : 
     960              :     std::mutex writeMtx_ {};
     961              :     const std::unique_ptr<ConversationRepository> repository_;
     962              :     const std::weak_ptr<JamiAccount> account_;
     963              :     const std::string accountId_;
     964              :     const std::string userId_;
     965              :     const std::string deviceId_;
     966              :     const std::shared_ptr<SwarmManager> swarmManager_;
     967              :     std::atomic_bool isRemoving_ {false};
     968              :     std::vector<libjami::SwarmMessage> loadMessages(const LogOptions& options, History* optHistory = nullptr);
     969              :     void loadMissingBodyOverwrites();
     970              :     void reloadBodyOverwriteMessages();
     971              : 
     972              :     // Out-of-order calls are safe: only the latest edition's overwrite propagates to the original.
     973              :     void updateMessageBodyOverwrite(const std::string& messageId, std::string_view bodyOverwrite);
     974              :     // Must be called with loadedHistory_.mutex held.
     975              :     // Stores bodyOverwrite on messageId, propagates to original if edition, returns signal message.
     976              :     // Only propagates when messageId == originalMsg->latestEditionId, so out-of-order calls are safe.
     977              :     libjami::SwarmMessage injectEditionOverwrites(const std::string& messageId, std::string_view bodyOverwrite);
     978              : 
     979              :     void clearBodyOverwrites();
     980              :     void pull(const std::string& deviceId);
     981              : 
     982              :     // Avoid multiple fetch/merges at the same time.
     983              :     std::mutex pullcbsMtx_ {};
     984              :     // store current remote in fetch
     985              :     std::map<std::string, std::deque<std::pair<std::string, OnPullCb>>> fetchingRemotes_ {};
     986              :     const std::shared_ptr<TransferManager> transferManager_ {};
     987              :     const std::filesystem::path repoPath_ {};
     988              :     const std::filesystem::path conversationDataPath_ {};
     989              :     const std::filesystem::path fetchedPath_ {};
     990              : 
     991              :     // Manage last message displayed and status
     992              :     const std::filesystem::path sendingPath_ {};
     993              :     const std::filesystem::path preferencesPath_ {};
     994              :     const std::filesystem::path statusPath_ {};
     995              : 
     996              :     // Devices known to be mobile, persisted so that wake-up notifications
     997              :     // can be sent right after a restart, before any DRT gossip is received
     998              :     const std::filesystem::path mobileNodesPath_ {};
     999              : 
    1000              :     OnMembersChanged onMembersChanged_ {};
    1001              :     struct TrackedMember
    1002              :     {
    1003              :         std::set<DeviceId> devices;
    1004              :         std::set<DeviceId> failedDevices;
    1005              :     };
    1006              :     std::map<std::string, TrackedMember> trackedMembers_;
    1007              :     mutable std::mutex trackedMembersMtx_;
    1008              :     bool isTracking_ {false};
    1009              :     void setupMemberCallback();
    1010              :     void startTracking(std::weak_ptr<Conversation> w);
    1011              :     void stopTracking();
    1012              :     void rotateTrackedMembers(const std::string& memberUri = "", const DeviceId& deviceId = {});
    1013              :     void monitorConnection(std::weak_ptr<Conversation> w);
    1014              :     void onConnectionFailed(const DeviceId& deviceId, const std::string& memberUri = "");
    1015              : 
    1016              :     uint64_t presenceDeviceListenerToken_ {0};
    1017              : 
    1018              :     // Manage hosted calls on this device
    1019              :     std::filesystem::path hostedCallsPath_ {};
    1020              :     mutable std::map<std::string, uint64_t /* start time */> hostedCalls_ {};
    1021              :     // Manage active calls for this conversation (can be hosted by other devices)
    1022              :     std::filesystem::path activeCallsPath_ {};
    1023              :     mutable std::mutex activeCallsMtx_ {};
    1024              :     mutable std::vector<std::map<std::string, std::string>> activeCalls_ {};
    1025              : 
    1026              :     mutable std::mutex gitSocketMtx_ {};
    1027              :     GitSocketList gitSocketList_ {};
    1028              : 
    1029              :     // Bootstrap
    1030              :     const std::shared_ptr<asio::io_context> ioContext_;
    1031              : 
    1032              :     /**
    1033              :      * Loaded history represents the linearized history to show for clients
    1034              :      */
    1035              :     History loadedHistory_ {};
    1036              :     std::vector<std::shared_ptr<libjami::SwarmMessage>> addToHistory(
    1037              :         History& history,
    1038              :         const std::vector<std::map<std::string, std::string>>& commits,
    1039              :         bool messageReceived = false,
    1040              :         bool commitFromSelf = false);
    1041              : 
    1042              :     void handleReaction(History& history, const std::shared_ptr<libjami::SwarmMessage>& sharedCommit) const;
    1043              :     void handleEdition(History& history,
    1044              :                        const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
    1045              :                        bool messageReceived) const;
    1046              :     bool handleMessage(History& history,
    1047              :                        const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
    1048              :                        bool messageReceived) const;
    1049              :     void rectifyStatus(const std::shared_ptr<libjami::SwarmMessage>& message, History& history) const;
    1050              :     /**
    1051              :      * {uri, {
    1052              :      *          {"fetch", "commitId"},
    1053              :      *          {"fetched_ts", "timestamp"},
    1054              :      *          {"read", "commitId"},
    1055              :      *          {"read_ts", "timestamp"}
    1056              :      *       }
    1057              :      * }
    1058              :      */
    1059              :     mutable std::mutex messageStatusMtx_;
    1060              :     std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)> messageStatusCb_ {};
    1061              :     std::map<std::string, std::map<std::string, std::string>> messagesStatus_ {};
    1062              :     /**
    1063              :      * Status: 0 = commited, 1 = fetched, 2 = read
    1064              :      * This cache the curent status to add in the messages
    1065              :      */
    1066              :     // Note: only store int32_t cause it's easy to pass to dbus this way
    1067              :     // memberToStatus serves as a cache for loading messages
    1068              :     std::map<std::string, int32_t> memberToStatus;
    1069              : 
    1070              :     // futureStatus is used to store the status for receiving messages
    1071              :     // (because we're not sure to fetch the commit before receiving a status change for this)
    1072              :     std::map<std::string, std::map<std::string, int32_t>> futureStatus;
    1073              :     // Update internal structures regarding status
    1074              :     void updateStatus(const std::string& uri,
    1075              :                       libjami::Account::MessageStates status,
    1076              :                       const std::string& commitId,
    1077              :                       const std::string& ts,
    1078              :                       bool emit = false);
    1079              : 
    1080              :     std::shared_ptr<Typers> typers_;
    1081              : };
    1082              : 
    1083              : /**
    1084              :  * Stores bodyOverwrite on messageId, propagates to original if messageId is an edition commit,
    1085              :  * and returns the message to signal (original if edition, messageId's message otherwise),
    1086              :  * with each edition entry's "bodyOverwrite" injected from the corresponding commit's pluginData.
    1087              :  * Must be called with loadedHistory_.mutex held.
    1088              :  * @param messageId     ID of the edition commit or original message to update.
    1089              :  * @param bodyOverwrite New overwrite value; empty string marks the message as processed with no overwrite.
    1090              :  */
    1091              : libjami::SwarmMessage
    1092            0 : Conversation::Impl::injectEditionOverwrites(const std::string& messageId, const std::string_view bodyOverwrite)
    1093              : {
    1094            0 :     const auto it = loadedHistory_.quickAccess.find(messageId);
    1095            0 :     if (it == loadedHistory_.quickAccess.end()) {
    1096            0 :         return {};
    1097              :     }
    1098              : 
    1099            0 :     libjami::SwarmMessage* signalTarget = it->second.get();
    1100              : 
    1101            0 :     if (const auto editIt = it->second->body.find(CommitKey::EDIT); editIt != it->second->body.end()) {
    1102            0 :         it->second->pluginData["bodyOverwrite"] = bodyOverwrite;
    1103            0 :         if (editIt->second.empty()) {
    1104            0 :             return {};
    1105              :         }
    1106            0 :         const auto originalIt = loadedHistory_.quickAccess.find(editIt->second);
    1107            0 :         if (originalIt == loadedHistory_.quickAccess.end()) {
    1108            0 :             return {};
    1109              :         }
    1110            0 :         signalTarget = originalIt->second.get();
    1111            0 :     } else if (it->second->editions.empty()) {
    1112            0 :         it->second->pluginData["bodyOverwrite"] = bodyOverwrite;
    1113              :     }
    1114              : 
    1115            0 :     if (const auto msgEditionIt = std::ranges::find_if(signalTarget->editions,
    1116            0 :                                                        [&](const auto& e) {
    1117            0 :                                                            const auto idIt = e.find("id");
    1118            0 :                                                            return idIt != e.end() && idIt->second == messageId;
    1119              :                                                        });
    1120            0 :         msgEditionIt != signalTarget->editions.end()) {
    1121            0 :         (*msgEditionIt)["bodyOverwrite"] = bodyOverwrite;
    1122              :     }
    1123              : 
    1124            0 :     if (const auto latestEdIt = loadedHistory_.quickAccess.find(signalTarget->latestEditionId);
    1125            0 :         latestEdIt != loadedHistory_.quickAccess.end()) {
    1126            0 :         if (const auto latestEdBoIt = latestEdIt->second->pluginData.find("bodyOverwrite");
    1127            0 :             latestEdBoIt != latestEdIt->second->pluginData.end() && !latestEdBoIt->second.empty()) {
    1128            0 :             signalTarget->pluginData["bodyOverwrite"] = latestEdBoIt->second;
    1129              :         }
    1130              :     }
    1131            0 :     return *signalTarget;
    1132            0 : }
    1133              : 
    1134              : void
    1135          429 : Conversation::Impl::setupMemberCallback()
    1136              : {
    1137          429 :     repository_->onMembersChanged([this](const std::set<std::string>& memberUris) {
    1138              :         {
    1139         1182 :             std::lock_guard lk(trackedMembersMtx_);
    1140         1182 :             if (isTracking_) {
    1141          280 :                 if (auto acc = account_.lock()) {
    1142          598 :                     for (auto it = trackedMembers_.begin(); it != trackedMembers_.end();) {
    1143          318 :                         if (memberUris.find(it->first) == memberUris.end()) {
    1144           10 :                             acc->presenceManager()->untrackBuddy(it->first);
    1145           10 :                             it = trackedMembers_.erase(it);
    1146              :                         } else {
    1147          308 :                             ++it;
    1148              :                         }
    1149              :                     }
    1150          280 :                 }
    1151              :             }
    1152         1182 :         }
    1153              : 
    1154         1182 :         if (onMembersChanged_)
    1155         1182 :             onMembersChanged_(memberUris);
    1156         1182 :     });
    1157          429 : }
    1158              : 
    1159              : void
    1160          562 : Conversation::Impl::startTracking(std::weak_ptr<Conversation> w)
    1161              : {
    1162          562 :     auto acc = account_.lock();
    1163          562 :     if (!acc)
    1164            0 :         return;
    1165              : 
    1166              :     {
    1167          562 :         std::lock_guard lk(trackedMembersMtx_);
    1168          562 :         if (isTracking_)
    1169          117 :             return;
    1170          445 :         isTracking_ = true;
    1171          890 :         presenceDeviceListenerToken_ = acc->presenceManager()->addDeviceListener(
    1172          890 :             [w](const std::string& uri, const DeviceId& deviceId, bool online) {
    1173          719 :                 if (auto sthis = w.lock()) {
    1174          719 :                     if (online && sthis->isMember(uri)) {
    1175         1812 :                         sthis->addKnownDevices({deviceId}, uri);
    1176              :                     }
    1177          719 :                 }
    1178          719 :             });
    1179          562 :     }
    1180              : 
    1181          890 :     rotateTrackedMembers();
    1182          562 : }
    1183              : 
    1184              : void
    1185          705 : Conversation::Impl::stopTracking()
    1186              : {
    1187              :     // Collect data under trackedMembersMtx_, then release it before calling
    1188              :     // into PresenceManager to avoid lock-ordering inversion with the
    1189              :     // PresenceManager mutex (which is held when notifyListeners calls
    1190              :     // addKnownDevices, which in turn acquires trackedMembersMtx_).
    1191          705 :     uint64_t token = 0;
    1192          705 :     std::vector<std::string> urisToUntrack;
    1193              :     {
    1194          705 :         std::lock_guard lk(trackedMembersMtx_);
    1195          705 :         if (!isTracking_)
    1196          260 :             return;
    1197          445 :         isTracking_ = false;
    1198          445 :         token = presenceDeviceListenerToken_;
    1199          445 :         presenceDeviceListenerToken_ = 0;
    1200         1267 :         for (const auto& [uri, _] : trackedMembers_) {
    1201          822 :             urisToUntrack.push_back(uri);
    1202              :         }
    1203          445 :         trackedMembers_.clear();
    1204          704 :     }
    1205              : 
    1206          445 :     auto acc = account_.lock();
    1207          445 :     if (!acc)
    1208          160 :         return;
    1209              : 
    1210          285 :     if (token) {
    1211          285 :         acc->presenceManager()->removeDeviceListener(token);
    1212              :     }
    1213              : 
    1214          869 :     for (const auto& uri : urisToUntrack) {
    1215          583 :         acc->presenceManager()->untrackBuddy(uri);
    1216              :     }
    1217          865 : }
    1218              : 
    1219              : void
    1220          789 : Conversation::Impl::rotateTrackedMembers(const std::string& memberUri, const DeviceId& deviceId)
    1221              : {
    1222          789 :     auto acc = account_.lock();
    1223          789 :     if (!acc)
    1224            0 :         return;
    1225              : 
    1226          789 :     std::lock_guard lk(trackedMembersMtx_);
    1227          789 :     if (!isTracking_)
    1228          297 :         return;
    1229              : 
    1230          492 :     if (!memberUri.empty()) {
    1231           47 :         if (auto it = trackedMembers_.find(memberUri); it != trackedMembers_.end()) {
    1232           46 :             JAMI_WARNING("{} [device {}] Rotating tracked members after connection failure", toString(), deviceId);
    1233           46 :             auto& info = it->second;
    1234           46 :             info.failedDevices.insert(deviceId);
    1235           46 :             if (std::includes(info.failedDevices.begin(),
    1236              :                               info.failedDevices.end(),
    1237              :                               info.devices.begin(),
    1238              :                               info.devices.end())) {
    1239           15 :                 acc->presenceManager()->untrackBuddy(it->first);
    1240           15 :                 trackedMembers_.erase(it);
    1241              :             }
    1242              :         } else {
    1243            1 :             return;
    1244              :         }
    1245              :     }
    1246              : 
    1247          491 :     auto members = repository_->members();
    1248          491 :     size_t N = members.size();
    1249          491 :     size_t K = std::min(N, 3 + (size_t) std::log2(N));
    1250              : 
    1251              :     // If we are already trying enough devices, don't rotate
    1252          491 :     auto activeDevices = swarmManager_->getActiveNodesCount();
    1253          491 :     if (activeDevices >= 2 * K)
    1254            0 :         return;
    1255              : 
    1256          491 :     JAMI_WARNING("{} Refreshing tracked members: {}/{} active ({}/{} devices)",
    1257              :                  toString(),
    1258              :                  trackedMembers_.size(),
    1259              :                  K,
    1260              :                  activeDevices,
    1261              :                  2 * K);
    1262              : 
    1263              :     // Add new members if we have space
    1264          491 :     if (trackedMembers_.size() < K) {
    1265          461 :         std::vector<std::string> candidates;
    1266          461 :         candidates.reserve(N - trackedMembers_.size());
    1267         1542 :         for (const auto& m : members) {
    1268         1081 :             if (m.uri != memberUri && trackedMembers_.find(m.uri) == trackedMembers_.end()) {
    1269         1048 :                 candidates.push_back(m.uri);
    1270              :             }
    1271              :         }
    1272          461 :         if (!candidates.empty()) {
    1273          446 :             std::vector<std::string> chosen;
    1274          446 :             std::sample(candidates.begin(),
    1275              :                         candidates.end(),
    1276              :                         std::back_inserter(chosen),
    1277          446 :                         K - trackedMembers_.size(),
    1278          446 :                         Manager::instance().getSeededRandomEngine());
    1279         1293 :             for (const auto& uri : chosen) {
    1280          847 :                 acc->presenceManager()->trackBuddy(uri);
    1281          847 :                 trackedMembers_.emplace(uri, TrackedMember {});
    1282              :             }
    1283          446 :         }
    1284          461 :     }
    1285         1087 : }
    1286              : 
    1287              : void
    1288          344 : Conversation::Impl::onConnectionFailed(const DeviceId& deviceId, const std::string& memberUri)
    1289              : {
    1290          344 :     rotateTrackedMembers(memberUri, deviceId);
    1291          344 : }
    1292              : 
    1293              : void
    1294          524 : Conversation::Impl::monitorConnection(std::weak_ptr<Conversation> w)
    1295              : {
    1296          524 :     if (!swarmManager_->isConnected()) {
    1297          513 :         startTracking(w);
    1298              :     }
    1299              : 
    1300          524 :     swarmManager_->onConnectionChanged([w](bool ok) {
    1301          326 :         dht::ThreadPool::io().run([w, ok] {
    1302          326 :             if (auto sthis = w.lock()) {
    1303              :                 // Check the current connection state rather than relying on the
    1304              :                 // captured `ok` value, because the thread pool does not guarantee
    1305              :                 // execution order. Two rapid state changes (connected then
    1306              :                 // disconnected) could otherwise cause stopTracking to run after
    1307              :                 // startTracking, leaving the conversation untracked while the DRT
    1308              :                 // has no connected nodes.
    1309          326 :                 if (sthis->pimpl_->swarmManager_->isConnected()) {
    1310          277 :                     sthis->pimpl_->stopTracking();
    1311              :                 } else {
    1312           49 :                     sthis->pimpl_->startTracking(w);
    1313              :                 }
    1314          326 :                 if (ok) {
    1315          278 :                     if (sthis->pimpl_->bootstrapCb_)
    1316          278 :                         sthis->pimpl_->bootstrapCb_();
    1317              :                 }
    1318              : #ifdef LIBJAMI_TEST
    1319          326 :                 if (sthis->pimpl_->bootstrapCbTest_)
    1320           34 :                     sthis->pimpl_->bootstrapCbTest_(sthis->id(),
    1321           17 :                                                     ok ? BootstrapStatus::SUCCESS : BootstrapStatus::FAILED);
    1322              : #endif
    1323          326 :             }
    1324          326 :         });
    1325          326 :     });
    1326          524 : }
    1327              : 
    1328              : bool
    1329           20 : Conversation::Impl::isAdmin() const
    1330              : {
    1331           20 :     auto adminsPath = repoPath_ / MemberPath::ADMINS;
    1332           40 :     return std::filesystem::is_regular_file(fileutils::getFullPath(adminsPath, userId_ + ".crt"));
    1333           20 : }
    1334              : 
    1335              : void
    1336           14 : Conversation::Impl::disconnectFromDevice(const DeviceId& deviceId)
    1337              : {
    1338           28 :     swarmManager_->deleteNode({deviceId});
    1339              : 
    1340           14 :     GitSocket socket;
    1341              :     {
    1342           14 :         std::lock_guard lk(gitSocketMtx_);
    1343           14 :         if (auto it = gitSocketList_.find(deviceId); it != gitSocketList_.end()) {
    1344           14 :             socket = std::move(it->second);
    1345           14 :             gitSocketList_.erase(it);
    1346              :         }
    1347           14 :     }
    1348              :     // The channel is closed here, outside the lock.
    1349           14 : }
    1350              : 
    1351              : void
    1352           19 : Conversation::Impl::disconnectFromPeer(const std::string& peerUri)
    1353              : {
    1354           19 :     std::set<DeviceId> devicesToRemove;
    1355              : 
    1356           19 :     const auto nodes = swarmManager_->getAllNodes();
    1357           35 :     for (const auto node : nodes)
    1358           16 :         if (peerUri == repository_->uriFromDevice(node.toString()))
    1359            7 :             devicesToRemove.emplace(node);
    1360              : 
    1361              :     {
    1362           19 :         std::lock_guard lk(gitSocketMtx_);
    1363           43 :         for (const auto& [deviceId, socket] : gitSocketList_) {
    1364           24 :             auto cert = socket ? socket->peerCertificate() : nullptr;
    1365           48 :             if ((cert && cert->issuer && cert->issuer->getId().toString() == peerUri)
    1366           48 :                 || peerUri == repository_->uriFromDevice(deviceId.toString())) {
    1367           13 :                 devicesToRemove.emplace(deviceId);
    1368              :             }
    1369           24 :         }
    1370           19 :     }
    1371              : 
    1372           32 :     for (const auto& deviceId : devicesToRemove)
    1373           13 :         disconnectFromDevice(deviceId);
    1374           19 : }
    1375              : 
    1376              : std::vector<std::map<std::string, std::string>>
    1377          113 : Conversation::Impl::getMembers(bool includeInvited, bool includeLeft, bool includeBanned) const
    1378              : {
    1379          113 :     std::vector<std::map<std::string, std::string>> result;
    1380          113 :     auto members = repository_->members();
    1381          113 :     std::lock_guard lk(messageStatusMtx_);
    1382          340 :     for (const auto& member : members) {
    1383          227 :         if (member.role == MemberRole::BANNED && !includeBanned) {
    1384            0 :             continue;
    1385              :         }
    1386          227 :         if (member.role == MemberRole::INVITED && !includeInvited)
    1387            0 :             continue;
    1388          227 :         if (member.role == MemberRole::LEFT && !includeLeft)
    1389            0 :             continue;
    1390          227 :         auto mm = member.map();
    1391          227 :         auto it = messagesStatus_.find(member.uri);
    1392          227 :         if (it != messagesStatus_.end()) {
    1393          336 :             auto readIt = it->second.find("read");
    1394          168 :             if (readIt != it->second.end())
    1395          381 :                 mm[ConversationMapKeys::LAST_DISPLAYED] = readIt->second;
    1396              :         }
    1397          227 :         result.emplace_back(std::move(mm));
    1398          227 :     }
    1399          226 :     return result;
    1400          113 : }
    1401              : 
    1402              : std::vector<std::map<std::string, std::string>>
    1403            0 : Conversation::Impl::getTrackedMembers() const
    1404              : {
    1405            0 :     std::vector<std::map<std::string, std::string>> result;
    1406            0 :     std::lock_guard lk(trackedMembersMtx_);
    1407            0 :     for (const auto& [uri, member] : trackedMembers_) {
    1408            0 :         std::map<std::string, std::string> map;
    1409            0 :         map["uri"] = uri;
    1410            0 :         std::string devicesStr;
    1411            0 :         for (const auto& dev : member.devices) {
    1412            0 :             if (!devicesStr.empty())
    1413            0 :                 devicesStr += ";";
    1414            0 :             devicesStr += dev.toString();
    1415              :         }
    1416            0 :         map["devices"] = devicesStr;
    1417            0 :         result.emplace_back(std::move(map));
    1418            0 :     }
    1419            0 :     return result;
    1420            0 : }
    1421              : 
    1422              : std::vector<std::string>
    1423           18 : Conversation::Impl::commitsEndedCalls()
    1424              : {
    1425              :     // Handle current calls
    1426           18 :     std::vector<std::string> commits {};
    1427           18 :     std::unique_lock lk(writeMtx_);
    1428           18 :     std::unique_lock lkA(activeCallsMtx_);
    1429           18 :     for (const auto& hostedCall : hostedCalls_) {
    1430              :         // In this case, this means that we left
    1431              :         // the conference while still hosting it, so activeCalls
    1432              :         // will not be correctly updated
    1433              :         // We don't need to send notifications there, as peers will sync with presence
    1434            0 :         auto confId = hostedCall.first;
    1435            0 :         auto now = std::chrono::system_clock::now();
    1436            0 :         uint64_t nowConverted = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
    1437            0 :         auto duration = (nowConverted > hostedCall.second) ? (nowConverted - hostedCall.second) * 1000 : 0;
    1438            0 :         auto commitMessage = CommitMessage::conferenceHostingEnd(confId, deviceId_, userId_, duration);
    1439              : 
    1440            0 :         auto itActive = std::find_if(activeCalls_.begin(),
    1441              :                                      activeCalls_.end(),
    1442            0 :                                      [this, confId = hostedCall.first](const auto& value) {
    1443            0 :                                          return value.at("id") == confId && value.at("uri") == userId_
    1444            0 :                                                 && value.at("device") == deviceId_;
    1445              :                                      });
    1446            0 :         if (itActive != activeCalls_.end())
    1447            0 :             activeCalls_.erase(itActive);
    1448            0 :         commits.emplace_back(repository_->commitMessage(commitMessage.toString()));
    1449            0 :         JAMI_DEBUG("Removing hosted conference... {:s}", hostedCall.first);
    1450            0 :     }
    1451           18 :     hostedCalls_.clear();
    1452           18 :     saveActiveCalls();
    1453           18 :     saveHostedCalls();
    1454           36 :     return commits;
    1455           18 : }
    1456              : 
    1457              : std::vector<libjami::SwarmMessage>
    1458         2286 : Conversation::Impl::loadMessages(const LogOptions& options, History* optHistory)
    1459              : {
    1460         2286 :     auto history = optHistory ? optHistory : &loadedHistory_;
    1461              : 
    1462              :     // history->mutex is locked by the caller
    1463         2286 :     if (!repository_ || history->loading) {
    1464            0 :         return {};
    1465              :     }
    1466         2286 :     history->loading = true;
    1467              : 
    1468              :     // By convention, if options.nbOfCommits is zero, then we
    1469              :     // don't impose a limit on the number of commits returned.
    1470         2286 :     bool limitNbOfCommits = options.nbOfCommits > 0;
    1471              : 
    1472         2286 :     auto startLogging = options.from == "";
    1473         2286 :     auto breakLogging = false;
    1474         2286 :     auto currentHistorySize = loadedHistory_.messageList.size();
    1475         2286 :     std::vector<std::string> replies;
    1476         2286 :     std::vector<std::shared_ptr<libjami::SwarmMessage>> msgList;
    1477         4572 :     repository_->log(
    1478              :         /* preCondition */
    1479         4572 :         [&](const auto& id, const auto& author, const auto& commit) {
    1480        18668 :             if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) {
    1481            2 :                 return CallbackResult::Skip;
    1482              :             }
    1483        18664 :             if (id == options.to) {
    1484          982 :                 if (options.includeTo)
    1485            0 :                     breakLogging = true; // For the next commit
    1486              :             }
    1487        18641 :             if (replies.empty()) { // This avoid load until
    1488              :                 // NOTE: in the future, we may want to add "Reply-Body" in commit to avoid to load
    1489              :                 // until this commit
    1490        18660 :                 if ((limitNbOfCommits
    1491        18660 :                      && (loadedHistory_.messageList.size() - currentHistorySize) == options.nbOfCommits))
    1492            3 :                     return CallbackResult::Break; // Stop logging
    1493        18657 :                 if (breakLogging)
    1494            0 :                     return CallbackResult::Break; // Stop logging
    1495        18657 :                 if (id == options.to && !options.includeTo) {
    1496          980 :                     return CallbackResult::Break; // Stop logging
    1497              :                 }
    1498              :             }
    1499              : 
    1500        17662 :             if (!startLogging && options.from != "" && options.from == id)
    1501         1691 :                 startLogging = true;
    1502        17674 :             if (!startLogging)
    1503           26 :                 return CallbackResult::Skip; // Start logging after this one
    1504              : 
    1505        17648 :             if (options.fastLog) {
    1506        16575 :                 if (options.authorUri != "") {
    1507            1 :                     if (options.authorUri == repository_->uriFromDevice(author.email)) {
    1508            1 :                         return CallbackResult::Break; // Found author, stop
    1509              :                     }
    1510              :                 }
    1511              :             }
    1512              : 
    1513        17646 :             return CallbackResult::Ok; // Continue
    1514              :         },
    1515              :         /* emplaceCb */
    1516         4572 :         [&](auto&& cc) {
    1517        17662 :             if (limitNbOfCommits && (msgList.size() == options.nbOfCommits))
    1518          491 :                 return;
    1519        17170 :             auto optMessage = repository_->convCommitToMap(cc);
    1520        17158 :             if (!optMessage.has_value())
    1521            1 :                 return;
    1522        17158 :             auto message = optMessage.value();
    1523        51481 :             if (message.find(CommitKey::REPLY_TO) != message.end()) {
    1524            1 :                 auto it = std::find(replies.begin(), replies.end(), message.at(CommitKey::REPLY_TO));
    1525            1 :                 if (it == replies.end()) {
    1526            3 :                     replies.emplace_back(message.at(CommitKey::REPLY_TO));
    1527              :                 }
    1528              :             }
    1529        17154 :             auto it = std::find(replies.begin(), replies.end(), message.at("id"));
    1530        17156 :             if (it != replies.end()) {
    1531            1 :                 replies.erase(it);
    1532              :             }
    1533        17164 :             std::shared_ptr<libjami::SwarmMessage> firstMsg;
    1534        17164 :             if ((history == &loadedHistory_) && msgList.empty() && !loadedHistory_.messageList.empty()) {
    1535            0 :                 firstMsg = *loadedHistory_.messageList.rbegin();
    1536              :             }
    1537        51497 :             auto added = addToHistory(*history, {message}, false, false);
    1538        17164 :             if (!added.empty() && firstMsg) {
    1539            0 :                 emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, repository_->id(), *firstMsg);
    1540              :             }
    1541        17164 :             msgList.insert(msgList.end(), added.begin(), added.end());
    1542        34331 :         },
    1543              :         /* postCondition */
    1544            0 :         [&](auto, auto, auto) {
    1545              :             // Stop logging if there was a limit set on the number of commits
    1546              :             // to return and we reached it. This isn't strictly necessary since
    1547              :             // the check at the beginning of `emplaceCb` ensures that we won't
    1548              :             // return too many messages, but it prevents us from needlessly
    1549              :             // iterating over a (potentially) large number of commits.
    1550        17636 :             return limitNbOfCommits && (msgList.size() == options.nbOfCommits);
    1551              :         },
    1552         2286 :         options.from,
    1553         2286 :         options.logIfNotFound);
    1554              : 
    1555         2286 :     history->loading = false;
    1556         2286 :     history->cv.notify_all();
    1557              : 
    1558              :     // Convert for client (remove ptr)
    1559         2286 :     std::vector<libjami::SwarmMessage> ret;
    1560         2286 :     ret.reserve(msgList.size());
    1561        19444 :     for (const auto& msg : msgList) {
    1562        17158 :         ret.emplace_back(*msg);
    1563              :     }
    1564         2286 :     return ret;
    1565         2286 : }
    1566              : 
    1567              : void
    1568            3 : Conversation::Impl::handleReaction(History& history, const std::shared_ptr<libjami::SwarmMessage>& sharedCommit) const
    1569              : {
    1570            6 :     auto it = history.quickAccess.find(sharedCommit->body.at(CommitKey::REACT_TO));
    1571            3 :     auto peditIt = history.pendingEditions.find(sharedCommit->id);
    1572            3 :     if (peditIt != history.pendingEditions.end()) {
    1573            0 :         auto oldBody = sharedCommit->body;
    1574            0 :         sharedCommit->body[CommitKey::BODY] = peditIt->second.front()->body[CommitKey::BODY];
    1575            0 :         if (sharedCommit->body.at(CommitKey::BODY).empty())
    1576            0 :             return;
    1577            0 :         history.pendingEditions.erase(peditIt);
    1578            0 :     }
    1579            3 :     if (it != history.quickAccess.end()) {
    1580            3 :         it->second->reactions.emplace_back(sharedCommit->body);
    1581            3 :         emitSignal<libjami::ConversationSignal::ReactionAdded>(accountId_,
    1582            3 :                                                                repository_->id(),
    1583            3 :                                                                it->second->id,
    1584            3 :                                                                sharedCommit->body);
    1585              :     } else {
    1586            0 :         history.pendingReactions[sharedCommit->body.at(CommitKey::REACT_TO)].emplace_back(sharedCommit->body);
    1587              :     }
    1588              : }
    1589              : 
    1590              : void
    1591           12 : Conversation::Impl::handleEdition(History& history,
    1592              :                                   const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
    1593              :                                   bool messageReceived) const
    1594              : {
    1595           24 :     auto editId = sharedCommit->body.at(CommitKey::EDIT);
    1596           12 :     auto it = history.quickAccess.find(editId);
    1597           12 :     if (it != history.quickAccess.end()) {
    1598            9 :         auto baseCommit = it->second;
    1599            9 :         if (baseCommit) {
    1600           27 :             auto itReact = baseCommit->body.find(CommitKey::REACT_TO);
    1601            9 :             std::string toReplace = (baseCommit->type == CommitType::DATA_TRANSFER) ? CommitKey::TID : CommitKey::BODY;
    1602              :             // An edition need not carry the field it replaces: retiring a
    1603              :             // collaborative document says nothing but which commit it retires.
    1604              :             // Absent means empty, which is what an edition to nothing already
    1605              :             // means everywhere below.
    1606            9 :             std::string body;
    1607            9 :             if (auto itBody = sharedCommit->body.find(toReplace); itBody != sharedCommit->body.end())
    1608            7 :                 body = itBody->second;
    1609              :             // Edit reaction
    1610            9 :             if (itReact != baseCommit->body.end()) {
    1611            1 :                 baseCommit->body[toReplace] = body; // Replace body if pending
    1612            1 :                 it = history.quickAccess.find(itReact->second);
    1613            1 :                 auto itPending = history.pendingReactions.find(itReact->second);
    1614            1 :                 if (it != history.quickAccess.end()) {
    1615            1 :                     baseCommit = it->second; // Base commit
    1616            1 :                     auto itPreviousReact = std::find_if(baseCommit->reactions.begin(),
    1617            1 :                                                         baseCommit->reactions.end(),
    1618            1 :                                                         [&](const auto& reaction) {
    1619            3 :                                                             return reaction.at("id") == editId;
    1620              :                                                         });
    1621            1 :                     if (itPreviousReact != baseCommit->reactions.end()) {
    1622            1 :                         (*itPreviousReact)[toReplace] = body;
    1623            1 :                         if (body.empty()) {
    1624            1 :                             baseCommit->reactions.erase(itPreviousReact);
    1625            2 :                             emitSignal<libjami::ConversationSignal::ReactionRemoved>(accountId_,
    1626            1 :                                                                                      repository_->id(),
    1627            1 :                                                                                      baseCommit->id,
    1628              :                                                                                      editId);
    1629              :                         }
    1630              :                     }
    1631            0 :                 } else if (itPending != history.pendingReactions.end()) {
    1632              :                     // Else edit if pending
    1633            0 :                     auto itReaction = std::find_if(itPending->second.begin(),
    1634            0 :                                                    itPending->second.end(),
    1635            0 :                                                    [&](const auto& reaction) { return reaction.at("id") == editId; });
    1636            0 :                     if (itReaction != itPending->second.end()) {
    1637            0 :                         (*itReaction)[toReplace] = body;
    1638            0 :                         if (body.empty())
    1639            0 :                             itPending->second.erase(itReaction);
    1640              :                     }
    1641              :                 } else {
    1642              :                     // Add to pending edtions
    1643            0 :                     messageReceived ? history.pendingEditions[editId].emplace_front(sharedCommit)
    1644            0 :                                     : history.pendingEditions[editId].emplace_back(sharedCommit);
    1645              :                 }
    1646              :             } else {
    1647              :                 // Normal message
    1648            8 :                 auto editionBody = it->second->body;
    1649              :                 // Tag the superseded body with the commit id that introduced it, so the
    1650              :                 // client can look up per-edition plugin overwrites via quickAccess.
    1651           16 :                 editionBody["id"] = it->second->latestEditionId.empty() ? it->second->id : it->second->latestEditionId;
    1652            8 :                 it->second->editions.emplace(it->second->editions.begin(), std::move(editionBody));
    1653              : #ifdef ENABLE_PLUGIN
    1654           16 :                 if (const auto boIt = it->second->pluginData.find("bodyOverwrite");
    1655            8 :                     boIt != it->second->pluginData.end()) {
    1656            0 :                     it->second->editions.front()["bodyOverwrite"] = boIt->second;
    1657              :                 }
    1658              : #endif
    1659            8 :                 it->second->body[toReplace] = sharedCommit->body[toReplace];
    1660            8 :                 it->second->latestEditionId = sharedCommit->id;
    1661            8 :                 if (toReplace == CommitKey::TID) {
    1662              :                     // Avoid to replace fileId in client
    1663            6 :                     it->second->body["fileId"] = "";
    1664              :                 }
    1665              :                 // Remove reactions
    1666            8 :                 if (sharedCommit->body.at(toReplace).empty())
    1667            5 :                     it->second->reactions.clear();
    1668            8 :             }
    1669            9 :         }
    1670            9 :     } else {
    1671            3 :         messageReceived ? history.pendingEditions[editId].emplace_front(sharedCommit)
    1672            3 :                         : history.pendingEditions[editId].emplace_back(sharedCommit);
    1673              :     }
    1674           12 : }
    1675              : 
    1676              : bool
    1677        18464 : Conversation::Impl::handleMessage(History& history,
    1678              :                                   const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
    1679              :                                   bool messageReceived) const
    1680              : {
    1681        18464 :     if (messageReceived) {
    1682              :         // For a received message, we place it at the beginning of the list
    1683         1293 :         if (!history.messageList.empty())
    1684         1016 :             sharedCommit->linearizedParent = (*history.messageList.begin())->id;
    1685         1292 :         history.messageList.emplace_front(sharedCommit);
    1686              :     } else {
    1687              :         // For a loaded message, we load from newest to oldest
    1688              :         // So we change the parent of the last message.
    1689        17171 :         if (!history.messageList.empty())
    1690        14907 :             (*history.messageList.rbegin())->linearizedParent = sharedCommit->id;
    1691        17167 :         history.messageList.emplace_back(sharedCommit);
    1692              :     }
    1693              :     // Handle pending reactions/editions
    1694        18468 :     auto reactIt = history.pendingReactions.find(sharedCommit->id);
    1695        18468 :     if (reactIt != history.pendingReactions.end()) {
    1696            0 :         for (const auto& commitBody : reactIt->second)
    1697            0 :             sharedCommit->reactions.emplace_back(commitBody);
    1698            0 :         history.pendingReactions.erase(reactIt);
    1699              :     }
    1700        18467 :     auto peditIt = history.pendingEditions.find(sharedCommit->id);
    1701        18465 :     if (peditIt != history.pendingEditions.end()) {
    1702            0 :         auto oldBody = sharedCommit->body;
    1703              :         // Tag original body with its own message id for client identification.
    1704            0 :         oldBody["id"] = sharedCommit->id;
    1705            0 :         sharedCommit->latestEditionId = peditIt->second.front()->id;
    1706            0 :         if (sharedCommit->type == CommitType::DATA_TRANSFER) {
    1707            0 :             sharedCommit->body[CommitKey::TID] = peditIt->second.front()->body[CommitKey::TID];
    1708            0 :             sharedCommit->body["fileId"] = "";
    1709              :         } else {
    1710            0 :             sharedCommit->body[CommitKey::BODY] = peditIt->second.front()->body[CommitKey::BODY];
    1711              :         }
    1712            0 :         peditIt->second.pop_front();
    1713            0 :         for (const auto& commit : peditIt->second) {
    1714            0 :             auto edBody = commit->body;
    1715              :             // Tag each superseded body with its edition commit id, mirroring
    1716              :             // handleEdition so injectEditionOverwrites can find plugin overwrites.
    1717            0 :             edBody["id"] = commit->id;
    1718            0 :             sharedCommit->editions.emplace_back(std::move(edBody));
    1719            0 :         }
    1720            0 :         sharedCommit->editions.emplace_back(std::move(oldBody));
    1721            0 :         history.pendingEditions.erase(peditIt);
    1722            0 :     }
    1723              :     // Announce to client
    1724        18465 :     if (messageReceived)
    1725         1293 :         emitSignal<libjami::ConversationSignal::SwarmMessageReceived>(accountId_, repository_->id(), *sharedCommit);
    1726        18466 :     return !messageReceived;
    1727              : }
    1728              : 
    1729              : void
    1730        18477 : Conversation::Impl::rectifyStatus(const std::shared_ptr<libjami::SwarmMessage>& message, History& history) const
    1731              : {
    1732        18477 :     auto parentIt = history.quickAccess.find(message->linearizedParent);
    1733        18478 :     auto currentMessage = message;
    1734              : 
    1735        30502 :     while (parentIt != history.quickAccess.end()) {
    1736        12063 :         const auto& parent = parentIt->second;
    1737        12336 :         for (const auto& [peer, value] : message->status) {
    1738        11172 :             auto parentStatusIt = parent->status.find(peer);
    1739        11100 :             if (parentStatusIt == parent->status.end() || parentStatusIt->second < value) {
    1740          265 :                 parent->status[peer] = value;
    1741          530 :                 emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(accountId_,
    1742          265 :                                                                                       repository_->id(),
    1743              :                                                                                       peer,
    1744          265 :                                                                                       parent->id,
    1745              :                                                                                       value);
    1746        10894 :             } else if (parentStatusIt->second >= value) {
    1747        10890 :                 break;
    1748              :             }
    1749              :         }
    1750        12064 :         currentMessage = parent;
    1751        12087 :         parentIt = history.quickAccess.find(parent->linearizedParent);
    1752              :     }
    1753        18486 : }
    1754              : 
    1755              : std::vector<std::shared_ptr<libjami::SwarmMessage>>
    1756        18466 : Conversation::Impl::addToHistory(History& history,
    1757              :                                  const std::vector<std::map<std::string, std::string>>& commits,
    1758              :                                  bool messageReceived,
    1759              :                                  bool commitFromSelf)
    1760              : {
    1761              :     //
    1762              :     // NOTE: This function makes the following assumptions on its arguments:
    1763              :     // - The messages in "history" are in reverse chronological order (newest message
    1764              :     //   first, oldest message last).
    1765              :     // - If messageReceived is true, then the commits in "commits" are assumed to be in
    1766              :     //   chronological order (oldest to newest) and to be newer than the ones in "history".
    1767              :     //   They are therefore inserted at the beginning of the message list.
    1768              :     // - If messageReceived is false, then the commits in "commits" are assumed to be in
    1769              :     //   reverse chronological order (newest to oldest) and to be older than the ones in
    1770              :     //   "history". They are therefore appended at the end of the message list.
    1771              :     //
    1772        18466 :     auto acc = account_.lock();
    1773        18468 :     if (!acc)
    1774            0 :         return {};
    1775        18465 :     auto username = acc->getUsername();
    1776        18453 :     if (messageReceived && (&history == &loadedHistory_ && history.loading)) {
    1777            0 :         std::unique_lock lk(history.mutex);
    1778            0 :         history.cv.wait(lk, [&] { return !history.loading; });
    1779            0 :     }
    1780              : 
    1781              :     // Only set messages' status on history for client
    1782        18453 :     bool needToSetMessageStatus = !commitFromSelf && &history == &loadedHistory_;
    1783              : 
    1784        18453 :     std::vector<std::shared_ptr<libjami::SwarmMessage>> sharedCommits;
    1785              :     // Apply the document removals of this batch before anything else in it.
    1786              :     //
    1787              :     // A removal is always newer than the announcement it retires, but the batch is
    1788              :     // walked oldest first when messages come in and newest first when older ones
    1789              :     // are paged back in. Acting in batch order would therefore recreate the
    1790              :     // repository of every deleted document each time the user scrolls up.
    1791        36964 :     for (const auto& commit : commits) {
    1792        18520 :         auto typeIt = commit.find(CommitKey::TYPE);
    1793        18508 :         if (typeIt == commit.end() || typeIt->second != CommitType::COLLAB_DOC)
    1794        18508 :             continue;
    1795           57 :         auto editIt = commit.find(CommitKey::EDIT);
    1796           57 :         if (editIt == commit.end() || editIt->second.empty())
    1797           54 :             continue;
    1798              :         // A removal names no document of its own: which one it retires is read from
    1799              :         // the announcement it edits, the only commit the swarm ties to its author.
    1800              :         // Otherwise a member could retire somebody else's document.
    1801            3 :         if (auto announcement = repository_->getCommit(editIt->second)) {
    1802            3 :             const auto& docId = announcement->commitMsg.uri;
    1803            3 :             if (!docId.empty())
    1804            3 :                 acc->collaborativeEditing()->onDocumentRemoved(repository_->id(), docId);
    1805            3 :         }
    1806              :     }
    1807        36980 :     for (const auto& commit : commits) {
    1808        18518 :         auto commitId = commit.at("id");
    1809        18498 :         if (history.quickAccess.find(commitId) != history.quickAccess.end())
    1810            1 :             continue; // Already present
    1811        18523 :         auto typeIt = commit.find(CommitKey::TYPE);
    1812              :         // Nothing to show for the client, skip
    1813        18520 :         if (typeIt != commit.end() && typeIt->second == CommitType::MERGE)
    1814           45 :             continue;
    1815              :         // A collaborative document is announced here, but its content lives in a
    1816              :         // separate repository: make sure that repository exists locally so it can
    1817              :         // be replicated. The commit itself falls through and is displayed like a
    1818              :         // shared file.
    1819        18478 :         if (typeIt != commit.end() && typeIt->second == CommitType::COLLAB_DOC) {
    1820              :             // Removals were applied above, before any announcement of this batch.
    1821           57 :             auto editIt = commit.find(CommitKey::EDIT);
    1822           57 :             bool isRemoval = editIt != commit.end() && !editIt->second.empty();
    1823           57 :             if (!isRemoval) {
    1824          108 :                 if (auto uriIt = commit.find(CommitKey::URI); uriIt != commit.end() && !uriIt->second.empty())
    1825           54 :                     acc->collaborativeEditing()->onDocumentAnnounced(repository_->id(), uriIt->second);
    1826              :             }
    1827              :         }
    1828              : 
    1829        18478 :         auto sharedCommit = std::make_shared<libjami::SwarmMessage>();
    1830        18477 :         sharedCommit->fromMapStringString(commit);
    1831              : 
    1832        18475 :         if (needToSetMessageStatus) {
    1833          959 :             std::lock_guard lk(messageStatusMtx_);
    1834              :             // Check if we already have status information for the commit.
    1835          959 :             auto itFuture = futureStatus.find(sharedCommit->id);
    1836          959 :             if (itFuture != futureStatus.end()) {
    1837           12 :                 sharedCommit->status = std::move(itFuture->second);
    1838           12 :                 futureStatus.erase(itFuture);
    1839              :             }
    1840          959 :         }
    1841        18475 :         sharedCommits.emplace_back(sharedCommit);
    1842        18521 :     }
    1843              : 
    1844        18465 :     if (needToSetMessageStatus) {
    1845          954 :         constexpr int32_t SENDING = static_cast<int32_t>(libjami::Account::MessageStates::SENDING);
    1846          954 :         constexpr int32_t SENT = static_cast<int32_t>(libjami::Account::MessageStates::SENT);
    1847          954 :         constexpr int32_t DISPLAYED = static_cast<int32_t>(libjami::Account::MessageStates::DISPLAYED);
    1848              : 
    1849          954 :         std::lock_guard lk(messageStatusMtx_);
    1850        13617 :         for (const auto& member : repository_->members()) {
    1851              :             // For each member, we iterate over the commits to add in reverse chronological
    1852              :             // order (i.e. from newest to oldest) and set their status from the point of view
    1853              :             // of that member (as best we can given the information we have).
    1854              :             //
    1855              :             // The key assumption made in order to compute the status is that it can never decrease
    1856              :             // (with respect to the ordering SENDING < SENT < DISPLAYED) as we go back in time. We
    1857              :             // therefore start by setting the "status" variable below to the lowest possible value,
    1858              :             // and increase it when we encounter a commit for which it is justified to do so.
    1859              :             //
    1860              :             // If messageReceived is true, then the commits we are adding are the most recent in the
    1861              :             // conversation history, so the lowest possible value is SENDING.
    1862              :             //
    1863              :             // If messageReceived is false, then the commits we are adding are older than the ones
    1864              :             // that are already in the history, so the lowest possible value is the status of the
    1865              :             // oldest message in the history so far, which is stored in memberToStatus.
    1866        12671 :             auto status = SENDING;
    1867        12671 :             if (!messageReceived) {
    1868           12 :                 auto cache = memberToStatus[member.uri];
    1869           12 :                 if (cache > status)
    1870            9 :                     status = cache;
    1871              :             }
    1872        12671 :             auto& messagesStatus = messagesStatus_[member.uri];
    1873              : 
    1874        25308 :             for (auto it = sharedCommits.rbegin(); it != sharedCommits.rend(); it++) {
    1875        12687 :                 auto sharedCommit = *it;
    1876        12683 :                 auto previousStatus = status;
    1877        12683 :                 auto& commitStatus = sharedCommit->status[member.uri];
    1878              : 
    1879              :                 // Compute status for the current commit.
    1880        37969 :                 if (status < SENT && messagesStatus["fetched"] == sharedCommit->id) {
    1881           12 :                     status = SENT;
    1882              :                 }
    1883        37921 :                 if (messagesStatus["read"] == sharedCommit->id) {
    1884            1 :                     status = DISPLAYED;
    1885              :                 }
    1886        37922 :                 if (member.uri == sharedCommit->body.at("author")) {
    1887          956 :                     status = DISPLAYED;
    1888              :                 }
    1889        12637 :                 if (status < commitStatus) {
    1890            2 :                     status = commitStatus;
    1891              :                 }
    1892              : 
    1893              :                 // Store computed value.
    1894        12637 :                 commitStatus = status;
    1895              : 
    1896              :                 // Update messagesStatus_ if needed.
    1897        12637 :                 if (previousStatus == SENDING && status >= SENT) {
    1898         2865 :                     messagesStatus["fetched"] = sharedCommit->id;
    1899              :                 }
    1900        12637 :                 if (previousStatus <= SENT && status == DISPLAYED) {
    1901         2832 :                     messagesStatus["read"] = sharedCommit->id;
    1902              :                 }
    1903        12637 :             }
    1904              : 
    1905        12663 :             if (!messageReceived) {
    1906              :                 // Update memberToStatus with the status of the last (i.e. oldest) added commit.
    1907           12 :                 memberToStatus[member.uri] = status;
    1908              :             }
    1909          953 :         }
    1910          954 :     }
    1911              : 
    1912        18465 :     std::vector<std::shared_ptr<libjami::SwarmMessage>> messages;
    1913        36950 :     for (const auto& sharedCommit : sharedCommits) {
    1914        18482 :         history.quickAccess[sharedCommit->id] = sharedCommit;
    1915              : 
    1916        36965 :         auto reactToIt = sharedCommit->body.find(CommitKey::REACT_TO);
    1917        36942 :         auto editIt = sharedCommit->body.find(CommitKey::EDIT);
    1918        18466 :         if (reactToIt != sharedCommit->body.end() && !reactToIt->second.empty()) {
    1919            3 :             handleReaction(history, sharedCommit);
    1920        18477 :         } else if (editIt != sharedCommit->body.end() && !editIt->second.empty()) {
    1921           12 :             handleEdition(history, sharedCommit, messageReceived);
    1922        18464 :         } else if (handleMessage(history, sharedCommit, messageReceived)) {
    1923        17171 :             messages.emplace_back(sharedCommit);
    1924              :         }
    1925        18475 :         rectifyStatus(sharedCommit, history);
    1926              :     }
    1927              : 
    1928        18473 :     return messages;
    1929        18479 : }
    1930              : 
    1931          184 : Conversation::Conversation(const std::shared_ptr<JamiAccount>& account,
    1932              :                            ConversationMode mode,
    1933          184 :                            const std::string& otherMember)
    1934          184 :     : pimpl_ {new Impl {account, mode, otherMember}}
    1935          184 : {}
    1936              : 
    1937           34 : Conversation::Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId)
    1938           34 :     : pimpl_ {new Impl {account, conversationId}}
    1939           34 : {}
    1940              : 
    1941          233 : Conversation::Conversation(const std::shared_ptr<JamiAccount>& account,
    1942              :                            const std::string& remoteDevice,
    1943          233 :                            const std::string& conversationId)
    1944          233 :     : pimpl_ {new Impl {account, remoteDevice, conversationId}}
    1945          233 : {}
    1946              : 
    1947          428 : Conversation::~Conversation() {}
    1948              : 
    1949              : std::string
    1950         6380 : Conversation::id() const
    1951              : {
    1952         6380 :     return pimpl_->repository_ ? pimpl_->repository_->id() : "";
    1953              : }
    1954              : 
    1955              : void
    1956          172 : Conversation::addMember(const std::string& contactUri, const OnDoneCb& cb)
    1957              : {
    1958              :     try {
    1959          172 :         if (mode() == ConversationMode::ONE_TO_ONE) {
    1960              :             // Only authorize to add left members
    1961            1 :             auto initialMembers = getInitialMembers();
    1962            1 :             auto it = std::find(initialMembers.begin(), initialMembers.end(), contactUri);
    1963            1 :             if (it == initialMembers.end()) {
    1964            1 :                 JAMI_WARNING("Unable to add new member in one to one conversation");
    1965            1 :                 cb(false, "");
    1966            1 :                 return;
    1967              :             }
    1968            1 :         }
    1969            0 :     } catch (const std::exception& e) {
    1970            0 :         JAMI_WARNING("Unable to get mode: {}", e.what());
    1971            0 :         cb(false, "");
    1972            0 :         return;
    1973            0 :     }
    1974          171 :     if (isMember(contactUri, true)) {
    1975            0 :         JAMI_WARNING("Unable to add member {} because it's already a member", contactUri);
    1976            0 :         cb(false, "");
    1977            0 :         return;
    1978              :     }
    1979          171 :     if (isMemberBanned(contactUri)) {
    1980            2 :         if (pimpl_->isAdmin()) {
    1981            1 :             dht::ThreadPool::io().run([w = weak(), contactUri = std::move(contactUri), cb = std::move(cb)] {
    1982            1 :                 if (auto sthis = w.lock()) {
    1983            1 :                     auto members = sthis->pimpl_->repository_->members();
    1984            1 :                     auto type = sthis->pimpl_->memberBanType(contactUri);
    1985            1 :                     if (type.empty()) {
    1986            0 :                         cb(false, {});
    1987            0 :                         return;
    1988              :                     }
    1989            1 :                     sthis->pimpl_->voteUnban(contactUri, type, cb);
    1990            2 :                 }
    1991              :             });
    1992              :         } else {
    1993            1 :             JAMI_WARNING("Unable to add member {} because this member is blocked", contactUri);
    1994            2 :             cb(false, "");
    1995              :         }
    1996            2 :         return;
    1997              :     }
    1998              : 
    1999          169 :     dht::ThreadPool::io().run([w = weak(), contactUri = std::move(contactUri), cb = std::move(cb)] {
    2000          169 :         if (auto sthis = w.lock()) {
    2001              :             // Add member files and commit
    2002          169 :             std::unique_lock lk(sthis->pimpl_->writeMtx_);
    2003          169 :             auto commit = sthis->pimpl_->repository_->addMember(contactUri);
    2004          169 :             if (not commit.empty())
    2005          169 :                 sthis->pimpl_->announce(commit, true);
    2006          169 :             lk.unlock();
    2007          169 :             if (cb)
    2008          169 :                 cb(!commit.empty(), commit);
    2009          338 :         }
    2010          169 :     });
    2011              : }
    2012              : 
    2013              : std::shared_ptr<dhtnet::ChannelSocket>
    2014         4663 : Conversation::gitSocket(const DeviceId& deviceId) const
    2015              : {
    2016         4663 :     return pimpl_->gitSocket(deviceId);
    2017              : }
    2018              : 
    2019              : void
    2020         2028 : Conversation::addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket)
    2021              : {
    2022         2028 :     pimpl_->addGitSocket(deviceId, socket);
    2023         2028 : }
    2024              : 
    2025              : void
    2026          982 : Conversation::removeGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& expected)
    2027              : {
    2028          982 :     pimpl_->removeGitSocket(deviceId, expected);
    2029          980 : }
    2030              : 
    2031              : void
    2032          436 : Conversation::shutdownConnections()
    2033              : {
    2034          436 :     GitSocketList gitSockets;
    2035              :     {
    2036          436 :         std::lock_guard lk(pimpl_->gitSocketMtx_);
    2037          436 :         gitSockets = std::move(pimpl_->gitSocketList_);
    2038          436 :         pimpl_->gitSocketList_.clear();
    2039          436 :     }
    2040              :     // Closing the channels wakes up any fetch currently blocked reading from them.
    2041          436 :     gitSockets.clear();
    2042          436 :     if (pimpl_->swarmManager_)
    2043          436 :         pimpl_->swarmManager_->shutdown();
    2044          436 : }
    2045              : 
    2046              : void
    2047            0 : Conversation::connectivityChanged()
    2048              : {
    2049            0 :     if (pimpl_->swarmManager_)
    2050            0 :         pimpl_->swarmManager_->maintainBuckets();
    2051            0 : }
    2052              : 
    2053              : std::vector<jami::DeviceId>
    2054            0 : Conversation::getDeviceIdList() const
    2055              : {
    2056            0 :     return pimpl_->swarmManager_->getAllNodes();
    2057              : }
    2058              : 
    2059              : std::shared_ptr<Typers>
    2060            9 : Conversation::typers() const
    2061              : {
    2062            9 :     return pimpl_->typers_;
    2063              : }
    2064              : 
    2065              : std::vector<std::map<std::string, std::string>>
    2066            0 : Conversation::getConnectivity() const
    2067              : {
    2068            0 :     return pimpl_->getConnectivity();
    2069              : }
    2070              : 
    2071              : void
    2072            1 : Conversation::Impl::voteUnban(const std::string& contactUri, const std::string_view type, const OnDoneCb& cb)
    2073              : {
    2074              :     // Check if admin
    2075            1 :     if (!isAdmin()) {
    2076            0 :         JAMI_WARNING("You're not an admin of this repo. Unable to unblock {}", contactUri);
    2077            0 :         cb(false, {});
    2078            0 :         return;
    2079              :     }
    2080              : 
    2081              :     // Vote for removal
    2082            1 :     std::unique_lock lk(writeMtx_);
    2083            1 :     auto voteCommit = repository_->voteUnban(contactUri, type);
    2084            1 :     if (voteCommit.empty()) {
    2085            0 :         JAMI_WARNING("Unbanning {} failed", contactUri);
    2086            0 :         cb(false, "");
    2087            0 :         return;
    2088              :     }
    2089              : 
    2090            1 :     auto lastId = voteCommit;
    2091            1 :     std::vector<std::string> commits;
    2092            1 :     commits.emplace_back(voteCommit);
    2093              : 
    2094              :     // If admin, check vote
    2095            2 :     auto resolveCommit = repository_->resolveVote(contactUri, type, CommitAction::UNBAN);
    2096            1 :     if (!resolveCommit.empty()) {
    2097            1 :         commits.emplace_back(resolveCommit);
    2098            1 :         lastId = resolveCommit;
    2099            1 :         JAMI_WARNING("Vote solved for unbanning {}.", contactUri);
    2100              :     }
    2101            1 :     announce(commits, true);
    2102            1 :     lk.unlock();
    2103            1 :     if (cb)
    2104            1 :         cb(!lastId.empty(), lastId);
    2105            1 : }
    2106              : 
    2107              : void
    2108           17 : Conversation::removeMember(const std::string& contactUri, bool isDevice, const OnDoneCb& cb)
    2109              : {
    2110           34 :     dht::ThreadPool::io().run(
    2111           34 :         [w = weak(), contactUri = std::move(contactUri), isDevice = std::move(isDevice), cb = std::move(cb)] {
    2112           17 :             if (auto sthis = w.lock()) {
    2113              :                 // Check if admin
    2114           17 :                 if (!sthis->pimpl_->isAdmin()) {
    2115            1 :                     JAMI_WARNING("You're not an admin of this repo. Unable to block {}", contactUri);
    2116            1 :                     cb(false, {});
    2117            2 :                     return;
    2118              :                 }
    2119              : 
    2120              :                 // Get current user type
    2121           16 :                 std::string type;
    2122           16 :                 if (isDevice) {
    2123            1 :                     type = "devices";
    2124              :                 } else {
    2125           15 :                     auto members = sthis->pimpl_->repository_->members();
    2126           32 :                     for (const auto& member : members) {
    2127           32 :                         if (member.uri == contactUri) {
    2128           15 :                             if (member.role == MemberRole::INVITED) {
    2129            2 :                                 type = "invited";
    2130           13 :                             } else if (member.role == MemberRole::ADMIN) {
    2131            1 :                                 type = "admins";
    2132           12 :                             } else if (member.role == MemberRole::MEMBER) {
    2133           12 :                                 type = "members";
    2134              :                             }
    2135           15 :                             break;
    2136              :                         }
    2137              :                     }
    2138           15 :                     if (type.empty()) {
    2139            0 :                         cb(false, {});
    2140            0 :                         return;
    2141              :                     }
    2142           15 :                 }
    2143              : 
    2144              :                 // Vote for removal
    2145           16 :                 std::unique_lock lk(sthis->pimpl_->writeMtx_);
    2146           16 :                 auto voteCommit = sthis->pimpl_->repository_->voteKick(contactUri, type);
    2147           16 :                 if (voteCommit.empty()) {
    2148            1 :                     JAMI_WARNING("Kicking {} failed", contactUri);
    2149            2 :                     cb(false, "");
    2150            1 :                     return;
    2151              :                 }
    2152              : 
    2153           15 :                 auto lastId = voteCommit;
    2154           15 :                 std::vector<std::string> commits;
    2155           15 :                 commits.emplace_back(voteCommit);
    2156              : 
    2157              :                 // If admin, check vote
    2158           30 :                 auto resolveCommit = sthis->pimpl_->repository_->resolveVote(contactUri, type, CommitAction::BAN);
    2159           15 :                 if (!resolveCommit.empty()) {
    2160           15 :                     commits.emplace_back(resolveCommit);
    2161           15 :                     lastId = resolveCommit;
    2162           15 :                     JAMI_WARNING("Vote solved for {}. {} banned", contactUri, isDevice ? "Device" : "Member");
    2163           15 :                     if (isDevice)
    2164            1 :                         sthis->pimpl_->disconnectFromDevice(DeviceId(contactUri));
    2165              :                     else
    2166           14 :                         sthis->pimpl_->disconnectFromPeer(contactUri);
    2167              :                 }
    2168              : 
    2169           15 :                 sthis->pimpl_->announce(commits, true);
    2170           15 :                 lk.unlock();
    2171           15 :                 cb(!lastId.empty(), lastId);
    2172           35 :             }
    2173              :         });
    2174           17 : }
    2175              : 
    2176              : std::vector<std::map<std::string, std::string>>
    2177          113 : Conversation::getMembers(bool includeInvited, bool includeLeft, bool includeBanned) const
    2178              : {
    2179          113 :     return pimpl_->getMembers(includeInvited, includeLeft, includeBanned);
    2180              : }
    2181              : 
    2182              : std::vector<std::map<std::string, std::string>>
    2183            0 : Conversation::getTrackedMembers() const
    2184              : {
    2185            0 :     return pimpl_->getTrackedMembers();
    2186              : }
    2187              : 
    2188              : std::set<std::string>
    2189         2567 : Conversation::memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
    2190              : {
    2191         2567 :     return pimpl_->repository_->memberUris(filter, filteredRoles);
    2192              : }
    2193              : 
    2194              : std::vector<NodeId>
    2195         1792 : Conversation::peersToSyncWith() const
    2196              : {
    2197         1792 :     auto s = pimpl_->swarmManager_->getConnectedNodes();
    2198         1792 :     std::lock_guard lk(pimpl_->gitSocketMtx_);
    2199        13560 :     for (const auto& [deviceId, _] : pimpl_->gitSocketList_)
    2200        11773 :         if (std::find(s.cbegin(), s.cend(), deviceId) == s.cend())
    2201          892 :             s.emplace_back(deviceId);
    2202         3584 :     return s;
    2203         1792 : }
    2204              : 
    2205              : std::vector<MobileNodeTarget>
    2206         1770 : Conversation::mobileNodesToNotify() const
    2207              : {
    2208         1770 :     std::vector<MobileNodeTarget> targets;
    2209         1770 :     auto account = pimpl_->account_.lock();
    2210         1770 :     if (!account)
    2211            0 :         return targets;
    2212         1770 :     for (const auto& info : pimpl_->swarmManager_->getMobileNodeInfosToNotify()) {
    2213            0 :         const auto deviceId = info.id.toString();
    2214            0 :         auto uri = uriFromDevice(deviceId);
    2215              :         // A verified lease binds the device to its issuer, so the account URI is
    2216              :         // already known without touching the certificate.
    2217            0 :         if (uri.empty() && info.lease)
    2218            0 :             uri = info.lease->issuer_id.toString();
    2219            0 :         if (uri.empty()) {
    2220            0 :             auto cert = account->certStore().getCertificate(deviceId);
    2221            0 :             if (cert && cert->issuer)
    2222            0 :                 uri = cert->issuer->getId().toString();
    2223            0 :         }
    2224            0 :         if (!uri.empty() && isPeerAuthorized(uri, deviceId, false))
    2225            0 :             targets.emplace_back(MobileNodeTarget {info.id, std::move(uri)});
    2226            0 :     }
    2227         1768 :     return targets;
    2228         1768 : }
    2229              : 
    2230              : bool
    2231         1769 : Conversation::isBootstrapped() const
    2232              : {
    2233         1769 :     return pimpl_->swarmManager_->isConnected();
    2234              : }
    2235              : 
    2236              : std::string
    2237        14130 : Conversation::uriFromDevice(const std::string& deviceId) const
    2238              : {
    2239        14130 :     return pimpl_->repository_->uriFromDevice(deviceId);
    2240              : }
    2241              : 
    2242              : std::map<std::string, std::vector<DeviceId>>
    2243           25 : Conversation::memberDevices() const
    2244              : {
    2245           25 :     return pimpl_->repository_->devices();
    2246              : }
    2247              : 
    2248              : void
    2249            0 : Conversation::monitor()
    2250              : {
    2251            0 :     pimpl_->swarmManager_->getRoutingTable().printRoutingTable();
    2252            0 : }
    2253              : 
    2254              : std::string
    2255          211 : Conversation::join()
    2256              : {
    2257          211 :     return pimpl_->repository_->join();
    2258              : }
    2259              : 
    2260              : bool
    2261         9413 : Conversation::isMember(const std::string& uri, bool includeInvited) const
    2262              : {
    2263         9413 :     auto uriCrt = uri + ".crt"sv;
    2264        18823 :     if (std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::ADMINS / uriCrt)
    2265        18826 :         || std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::MEMBERS / uriCrt)) {
    2266         7155 :         return true;
    2267              :     }
    2268         2254 :     if (includeInvited) {
    2269         1677 :         if (std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::INVITED / uri)) {
    2270         1312 :             return true;
    2271              :         }
    2272          365 :         if (mode() == ConversationMode::ONE_TO_ONE) {
    2273            3 :             for (const auto& member : getInitialMembers()) {
    2274            2 :                 if (member == uri)
    2275            0 :                     return true;
    2276            1 :             }
    2277              :         }
    2278              :     }
    2279          942 :     return false;
    2280         9409 : }
    2281              : 
    2282              : bool
    2283         6560 : Conversation::isMemberBanned(const std::string& uri) const
    2284              : {
    2285         6560 :     return !pimpl_->memberBanType(uri).empty();
    2286              : }
    2287              : 
    2288              : bool
    2289         7332 : Conversation::isDeviceBanned(const std::string& deviceId) const
    2290              : {
    2291         7332 :     return pimpl_->isDeviceBanned(deviceId);
    2292              : }
    2293              : 
    2294              : bool
    2295         6351 : Conversation::isPeerAuthorized(const std::string& uri, const std::string& deviceId, bool includeInvited) const
    2296              : {
    2297         6351 :     return !isMemberBanned(uri) && !isDeviceBanned(deviceId) && isMember(uri, includeInvited);
    2298              : }
    2299              : 
    2300              : void
    2301          171 : Conversation::createCommit(CommitMessage&& message, OnCommitCb&& onCommit, OnDoneCb&& cb)
    2302              : {
    2303          171 :     if (!message.replyTo.empty()) {
    2304            2 :         if (!pimpl_->repository_->hasCommit(message.replyTo)) {
    2305            1 :             JAMI_ERROR("Replying to invalid commit {}", message.replyTo);
    2306            1 :             return;
    2307              :         }
    2308              :     }
    2309          340 :     dht::ThreadPool::io().run(
    2310          340 :         [w = weak(), message = std::move(message), onCommit = std::move(onCommit), cb = std::move(cb)] {
    2311          170 :             if (auto sthis = w.lock()) {
    2312          170 :                 std::unique_lock lk(sthis->pimpl_->writeMtx_);
    2313          170 :                 auto commit = sthis->pimpl_->repository_->commitMessage(message.toString());
    2314          170 :                 lk.unlock();
    2315          170 :                 if (onCommit)
    2316           14 :                     onCommit(commit);
    2317          170 :                 sthis->pimpl_->announce(commit, true);
    2318          169 :                 if (cb)
    2319          169 :                     cb(!commit.empty(), commit);
    2320          340 :             }
    2321          169 :         });
    2322              : }
    2323              : 
    2324              : bool
    2325        11764 : Conversation::hasCommit(const std::string& commitId) const
    2326              : {
    2327        11764 :     return pimpl_->repository_->hasCommit(commitId);
    2328              : }
    2329              : 
    2330              : std::optional<ConversationCommit>
    2331           36 : Conversation::getCommit(const std::string& commitId) const
    2332              : {
    2333           36 :     return pimpl_->repository_->getCommit(commitId);
    2334              : }
    2335              : 
    2336              : namespace {
    2337              : /**
    2338              :  * Announcement commit ids retired by a removal, from a full conversation log.
    2339              :  *
    2340              :  * Which document a removal retires is read from the announcement it edits, never from
    2341              :  * the removal itself: the swarm only checks that an edition carries the author of the
    2342              :  * commit it edits, so trusting an id carried by the removal would let a member retire
    2343              :  * a document somebody else created.
    2344              :  */
    2345              : std::set<std::string>
    2346           58 : retiredAnnouncements(const std::vector<std::map<std::string, std::string>>& commits)
    2347              : {
    2348           58 :     std::set<std::string> retired;
    2349          282 :     for (const auto& commit : commits) {
    2350          224 :         auto typeIt = commit.find(CommitKey::TYPE);
    2351          224 :         if (typeIt == commit.end() || typeIt->second != CommitType::COLLAB_DOC)
    2352          177 :             continue;
    2353           94 :         if (auto editIt = commit.find(CommitKey::EDIT); editIt != commit.end() && !editIt->second.empty())
    2354            2 :             retired.emplace(editIt->second);
    2355              :     }
    2356           58 :     return retired;
    2357            0 : }
    2358              : } // namespace
    2359              : 
    2360              : std::vector<std::map<std::string, std::string>>
    2361           58 : Conversation::collaborativeDocuments() const
    2362              : {
    2363           58 :     if (!pimpl_->repository_)
    2364            0 :         return {};
    2365              :     // Read straight from git so documents are found even when their announcing commit
    2366              :     // is not (or no longer) in the loaded message window.
    2367           58 :     LogOptions options;
    2368           58 :     options.skipMerge = true;
    2369           58 :     auto commits = pimpl_->repository_->convCommitsToMap(pimpl_->repository_->log(options));
    2370           58 :     auto retired = retiredAnnouncements(commits);
    2371           58 :     std::vector<std::map<std::string, std::string>> result;
    2372          282 :     for (auto& commit : commits) {
    2373          224 :         auto typeIt = commit.find(CommitKey::TYPE);
    2374          224 :         if (typeIt == commit.end() || typeIt->second != CommitType::COLLAB_DOC)
    2375          181 :             continue;
    2376           47 :         auto uriIt = commit.find(CommitKey::URI);
    2377           47 :         if (uriIt == commit.end() || uriIt->second.empty())
    2378            2 :             continue;
    2379           45 :         auto idIt = commit.find("id");
    2380           45 :         if (idIt != commit.end() && retired.count(idIt->second) != 0)
    2381            2 :             continue;
    2382              :         // A document is addressed by its own id everywhere -- opening, renaming,
    2383              :         // removing -- so that is what "id" carries here; the commit's own id only
    2384              :         // says which timeline interaction announced it.
    2385          344 :         result.push_back({{"id", uriIt->second},
    2386           86 :                           {"announcement", idIt != commit.end() ? idIt->second : ""},
    2387           86 :                           {"displayName", commit[CommitKey::DISPLAY_NAME]},
    2388           86 :                           {"mimeType", commit[CommitKey::MIME_TYPE]},
    2389           86 :                           {"author", commit["author"]},
    2390           86 :                           {"timestamp", commit["timestamp"]}});
    2391              :     }
    2392           58 :     return result;
    2393          316 : }
    2394              : 
    2395              : void
    2396            2 : Conversation::loadMessages(const OnLoadMessages& cb, const LogOptions& options)
    2397              : {
    2398            2 :     if (!cb)
    2399            0 :         return;
    2400            2 :     dht::ThreadPool::io().run([w = weak(), cb = std::move(cb), options] {
    2401            2 :         if (auto sthis = w.lock()) {
    2402            2 :             std::unique_lock lk(sthis->pimpl_->loadedHistory_.mutex);
    2403            2 :             auto result = sthis->pimpl_->loadMessages(options);
    2404            2 :             lk.unlock();
    2405            2 :             cb(std::move(result));
    2406            4 :         }
    2407            2 :     });
    2408              : }
    2409              : 
    2410              : void
    2411            3 : Conversation::Impl::loadMissingBodyOverwrites()
    2412              : {
    2413              : #ifdef ENABLE_PLUGIN
    2414            3 :     auto& pluginChatManager = Manager::instance().getJamiPluginManager().getChatServicesManager();
    2415            3 :     if (!pluginChatManager.hasHandlers()) {
    2416            2 :         return;
    2417              :     }
    2418              : 
    2419            1 :     const auto& convId = repository_->id();
    2420              : 
    2421              :     // Collect all TEXT commits (originals and editions) missing a bodyOverwrite.
    2422            1 :     std::vector<libjami::SwarmMessage> batch;
    2423              :     {
    2424            1 :         std::lock_guard const lk(loadedHistory_.mutex);
    2425            4 :         for (const auto& msg : loadedHistory_.quickAccess | std::views::values) {
    2426            3 :             if (msg->type == CommitType::TEXT
    2427           11 :                 && ((msg->editions.empty() && !msg->pluginData.contains("bodyOverwrite"))
    2428            3 :                     || (!msg->editions.empty() && !msg->editions.back().contains("bodyOverwrite")))) {
    2429            1 :                 batch.push_back(*msg);
    2430              :             }
    2431              :         }
    2432            1 :     }
    2433              : 
    2434            1 :     if (batch.empty()) {
    2435            0 :         return;
    2436              :     }
    2437              : 
    2438              :     // Single plugin call for all messages at once.
    2439            1 :     pluginChatManager.transformSwarmMessages(batch, accountId_, convId);
    2440              : 
    2441              :     // Write results back, deduplicate by signal-message id (multiple editions → same original).
    2442            1 :     std::map<std::string, libjami::SwarmMessage> signalMap;
    2443              :     {
    2444            1 :         std::lock_guard const lk(loadedHistory_.mutex);
    2445            2 :         for (const auto& transformed : batch) {
    2446            2 :             const auto boIt = transformed.pluginData.find("bodyOverwrite");
    2447            1 :             if (boIt == transformed.pluginData.end() || boIt->second.empty()) {
    2448            1 :                 if (const auto it = loadedHistory_.quickAccess.find(transformed.id);
    2449            1 :                     it != loadedHistory_.quickAccess.end()) {
    2450            3 :                     it->second->pluginData["bodyOverwrite"] = "";
    2451              :                 }
    2452            1 :                 continue;
    2453            1 :             }
    2454            0 :             if (auto signalMsg = injectEditionOverwrites(transformed.id, boIt->second); !signalMsg.id.empty()) {
    2455            0 :                 signalMap[signalMsg.id] = std::move(signalMsg);
    2456            0 :             }
    2457              :         }
    2458            1 :     }
    2459            1 :     for (const auto& msg : signalMap | std::views::values) {
    2460            0 :         emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, convId, msg);
    2461              :     }
    2462              : #endif
    2463            1 : }
    2464              : 
    2465              : void
    2466            3 : Conversation::loadMissingBodyOverwrites() const
    2467              : {
    2468            3 :     pimpl_->loadMissingBodyOverwrites();
    2469            3 : }
    2470              : 
    2471              : void
    2472            0 : Conversation::Impl::reloadBodyOverwriteMessages()
    2473              : {
    2474              : #ifdef ENABLE_PLUGIN
    2475              :     // Clear all bodyOverwrite* entries so loadMissingBodyOverwrites() reload all body overwrites.
    2476              :     {
    2477            0 :         std::lock_guard const lk(loadedHistory_.mutex);
    2478            0 :         for (const auto& msg : loadedHistory_.quickAccess | std::views::values) {
    2479            0 :             if (msg->editions.empty()) {
    2480            0 :                 msg->pluginData.erase("bodyOverwrite");
    2481              :             } else {
    2482            0 :                 msg->pluginData.erase("bodyOverwrite");
    2483            0 :                 for (auto& edition : msg->editions) {
    2484            0 :                     edition.erase("bodyOverwrite");
    2485              :                 }
    2486              :             }
    2487              :         }
    2488            0 :     }
    2489            0 :     loadMissingBodyOverwrites();
    2490              : #endif
    2491            0 : }
    2492              : 
    2493              : void
    2494            0 : Conversation::reloadBodyOverwriteMessages() const
    2495              : {
    2496            0 :     pimpl_->reloadBodyOverwriteMessages();
    2497            0 : }
    2498              : 
    2499              : void
    2500            0 : Conversation::Impl::updateMessageBodyOverwrite(const std::string& messageId, const std::string_view bodyOverwrite)
    2501              : {
    2502              : #ifdef ENABLE_PLUGIN
    2503            0 :     const auto& convId = repository_->id();
    2504            0 :     libjami::SwarmMessage signalMsg;
    2505              :     {
    2506            0 :         std::lock_guard const lk(loadedHistory_.mutex);
    2507            0 :         signalMsg = injectEditionOverwrites(messageId, bodyOverwrite);
    2508            0 :     }
    2509            0 :     if (!signalMsg.id.empty()) {
    2510            0 :         emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, convId, signalMsg);
    2511              :     }
    2512              : #endif
    2513            0 : }
    2514              : 
    2515              : void
    2516            0 : Conversation::updateMessageBodyOverwrite(const std::string& messageId, const std::string& bodyOverwrite) const
    2517              : {
    2518            0 :     pimpl_->updateMessageBodyOverwrite(messageId, bodyOverwrite);
    2519            0 : }
    2520              : 
    2521              : void
    2522            1 : Conversation::Impl::clearBodyOverwrites()
    2523              : {
    2524              : #ifdef ENABLE_PLUGIN
    2525            1 :     const auto convId = repository_->id();
    2526            1 :     std::map<std::string, libjami::SwarmMessage> signalMap;
    2527              :     {
    2528            1 :         std::lock_guard const lk(loadedHistory_.mutex);
    2529              :         // Track which message ids need a signal: their own overwrite was cleared,
    2530              :         // or one of their edition commits' overwrite was cleared.
    2531            4 :         for (const auto& [id, msg] : loadedHistory_.quickAccess) {
    2532            3 :             if (msg->editions.empty()) {
    2533            6 :                 msg->pluginData.erase("bodyOverwrite");
    2534            9 :                 if (!msg->body.contains(CommitKey::EDIT)) {
    2535            3 :                     signalMap[id] = *msg;
    2536              :                 }
    2537              :             } else {
    2538            0 :                 msg->pluginData.erase("bodyOverwrite");
    2539            0 :                 for (auto& edition : msg->editions) {
    2540            0 :                     edition.erase("bodyOverwrite");
    2541              :                 }
    2542            0 :                 signalMap[id] = *msg;
    2543              :             }
    2544              :         }
    2545            1 :     }
    2546            4 :     for (const auto& msg : signalMap | std::views::values) {
    2547            3 :         emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, convId, msg);
    2548              :     }
    2549              : #endif
    2550            1 : }
    2551              : 
    2552              : void
    2553            1 : Conversation::clearBodyOverwrites() const
    2554              : {
    2555            1 :     pimpl_->clearBodyOverwrites();
    2556            1 : }
    2557              : 
    2558              : void
    2559            0 : Conversation::clearCache()
    2560              : {
    2561            0 :     std::lock_guard lk(pimpl_->loadedHistory_.mutex);
    2562            0 :     pimpl_->loadedHistory_.messageList.clear();
    2563            0 :     pimpl_->loadedHistory_.quickAccess.clear();
    2564            0 :     pimpl_->loadedHistory_.pendingEditions.clear();
    2565            0 :     pimpl_->loadedHistory_.pendingReactions.clear();
    2566              :     {
    2567            0 :         std::lock_guard lk(pimpl_->messageStatusMtx_);
    2568            0 :         pimpl_->memberToStatus.clear();
    2569            0 :     }
    2570            0 : }
    2571              : 
    2572              : std::string
    2573         1992 : Conversation::lastCommitId() const
    2574              : {
    2575              :     {
    2576         1992 :         std::lock_guard lk(pimpl_->loadedHistory_.mutex);
    2577         1992 :         if (!pimpl_->loadedHistory_.messageList.empty())
    2578         1419 :             return (*pimpl_->loadedHistory_.messageList.begin())->id;
    2579         1993 :     }
    2580          573 :     LogOptions options;
    2581          573 :     options.nbOfCommits = 1;
    2582          573 :     options.skipMerge = true;
    2583          573 :     History optHistory;
    2584          573 :     std::scoped_lock lock(pimpl_->writeMtx_, optHistory.mutex);
    2585          573 :     auto res = pimpl_->loadMessages(options, &optHistory);
    2586          573 :     if (res.empty())
    2587            3 :         return {};
    2588          570 :     return (*optHistory.messageList.begin())->id;
    2589          573 : }
    2590              : 
    2591              : bool
    2592         1817 : Conversation::pull(const std::string& deviceId, OnPullCb&& cb, std::string commitId)
    2593              : {
    2594         1817 :     std::lock_guard lk(pimpl_->pullcbsMtx_);
    2595         5452 :     auto [it, notInProgress] = pimpl_->fetchingRemotes_.emplace(deviceId,
    2596         3634 :                                                                 std::deque<std::pair<std::string, OnPullCb>>());
    2597         1817 :     auto& pullcbs = it->second;
    2598            0 :     auto itPull = std::find_if(pullcbs.begin(), pullcbs.end(), [&](const auto& elem) {
    2599            0 :         return std::get<0>(elem) == commitId;
    2600         3635 :     });
    2601         1816 :     if (itPull != pullcbs.end()) {
    2602            0 :         JAMI_DEBUG("{} Ignoring request to pull from {:s} with commit {:s}: pull already in progress",
    2603              :                    pimpl_->toString(),
    2604              :                    deviceId,
    2605              :                    commitId);
    2606            0 :         cb(false);
    2607            0 :         return false;
    2608              :     }
    2609         1816 :     JAMI_DEBUG("{} [device {}] Pulling '{:s}'", pimpl_->toString(), deviceId, commitId);
    2610         1817 :     pullcbs.emplace_back(std::move(commitId), std::move(cb));
    2611         1816 :     if (notInProgress)
    2612         1812 :         dht::ThreadPool::io().run([w = weak(), deviceId] {
    2613         1814 :             if (auto sthis_ = w.lock())
    2614         1814 :                 sthis_->pimpl_->pull(deviceId);
    2615         1814 :         });
    2616         1818 :     return true;
    2617         1818 : }
    2618              : 
    2619              : void
    2620         1814 : Conversation::Impl::pull(const std::string& deviceId)
    2621              : {
    2622         1814 :     auto& repo = repository_;
    2623              : 
    2624         1814 :     std::string commitId;
    2625         1814 :     OnPullCb cb;
    2626              :     while (true) {
    2627              :         {
    2628         3631 :             std::lock_guard lk(pullcbsMtx_);
    2629         3631 :             auto it = fetchingRemotes_.find(deviceId);
    2630         3631 :             if (it == fetchingRemotes_.end()) {
    2631            0 :                 JAMI_ERROR("Could not find device {:s} in fetchingRemotes", deviceId);
    2632            0 :                 break;
    2633              :             }
    2634         3632 :             auto& pullcbs = it->second;
    2635         3631 :             if (pullcbs.empty()) {
    2636         1813 :                 fetchingRemotes_.erase(it);
    2637         1814 :                 break;
    2638              :             }
    2639         1818 :             auto& elem = pullcbs.front();
    2640         1817 :             commitId = std::move(std::get<0>(elem));
    2641         1818 :             cb = std::move(std::get<1>(elem));
    2642         1818 :             pullcbs.pop_front();
    2643         3631 :         }
    2644              :         // If recently fetched, the commit can already be there, so no need to do complex operations
    2645         1817 :         if (commitId != "" && repo->hasCommit(commitId)) {
    2646           12 :             cb(true);
    2647           52 :             continue;
    2648              :         }
    2649              :         // Pull from remote
    2650         1806 :         auto fetched = repo->fetch(deviceId);
    2651         1806 :         if (!fetched) {
    2652           40 :             cb(false);
    2653           40 :             continue;
    2654              :         }
    2655              : 
    2656         1766 :         auto oldHead = repo->getHead();
    2657              : 
    2658         1766 :         std::unique_lock lk(writeMtx_);
    2659              :         auto commits = repo->mergeHistory(deviceId,
    2660         1771 :                                           [this](const std::string& peerUri) { this->disconnectFromPeer(peerUri); });
    2661         1765 :         if (!commits.empty()) {
    2662          964 :             announce(commits);
    2663              :         }
    2664         1766 :         lk.unlock();
    2665              : 
    2666         1766 :         bool commitFound = false;
    2667         1766 :         if (commitId != "") {
    2668              :             // If `commitId` is non-empty, then we were attempting to pull a specific commit.
    2669              :             // We need to check if we actually got it; the fact that the fetch above was
    2670              :             // successful doesn't guarantee that we did.
    2671         1222 :             for (const auto& commit : commits) {
    2672         1931 :                 if (commit.at("id") == commitId) {
    2673          952 :                     commitFound = true;
    2674          952 :                     break;
    2675              :                 }
    2676              :             }
    2677              :         } else {
    2678          557 :             commitFound = true;
    2679              :         }
    2680         1765 :         if (!commitFound)
    2681          256 :             JAMI_WARNING("Successfully fetched from device {} but didn't receive expected commit {}",
    2682              :                          deviceId,
    2683              :                          commitId);
    2684              :         // WARNING: If its argument is `true`, this callback will attempt to send a message notification
    2685              :         //          for commit `commitId` to other members of the swarm. It's important that we only
    2686              :         //          send these notifications if we actually have the commit. Otherwise, we can end up
    2687              :         //          in a situation where the members of the swarm keep sending notifications to each
    2688              :         //          other for a commit that none of them have (note that we are unable to rule this out, as
    2689              :         //          nothing prevents a malicious user from intentionally sending a notification with
    2690              :         //          a fake commit ID).
    2691         1765 :         if (cb)
    2692         1766 :             cb(commitFound);
    2693              : 
    2694              :         // Announce if profile changed
    2695         1766 :         if (!commits.empty()) {
    2696         1930 :             auto diffStats = repo->diffStats("HEAD", oldHead);
    2697          965 :             auto changedFiles = repo->changedFiles(diffStats);
    2698          965 :             if (find(changedFiles.begin(), changedFiles.end(), "profile.vcf") != changedFiles.end()) {
    2699            6 :                 emitSignal<libjami::ConversationSignal::ConversationProfileUpdated>(accountId_,
    2700            6 :                                                                                     repo->id(),
    2701           12 :                                                                                     repo->infos());
    2702              :             }
    2703          965 :         }
    2704         3582 :     }
    2705         1814 : }
    2706              : 
    2707              : void
    2708         1815 : Conversation::sync(const std::string& member, const std::string& deviceId, OnPullCb&& cb, std::string commitId)
    2709              : {
    2710         1815 :     pull(deviceId, std::move(cb), commitId);
    2711         1817 :     dht::ThreadPool::io().run([member, deviceId, w = weak_from_this()] {
    2712         1817 :         auto sthis = w.lock();
    2713              :         // For waiting request, downloadFile
    2714         1817 :         for (const auto& wr : sthis->dataTransfer()->waitingRequests()) {
    2715            1 :             sthis->downloadFile(wr.interactionId, wr.fileId, wr.path, member, deviceId);
    2716         1817 :         }
    2717         1816 :     });
    2718         1817 : }
    2719              : 
    2720              : std::map<std::string, std::string>
    2721          300 : Conversation::generateInvitation(TimePoint sent) const
    2722              : {
    2723              :     // Invite the new member to the conversation
    2724          300 :     Json::Value root;
    2725          300 :     auto& metadata = root[ConversationMapKeys::METADATAS];
    2726          604 :     for (const auto& [k, v] : infos()) {
    2727          304 :         if (v.size() >= 64000) {
    2728            0 :             JAMI_WARNING("Cutting invite because the SIP message will be too long");
    2729            0 :             continue;
    2730              :         }
    2731          304 :         metadata[k] = v;
    2732          300 :     }
    2733          300 :     root[ConversationMapKeys::CONVERSATIONID] = id();
    2734          300 :     root[ConversationMapKeys::RECEIVED] = Json::Int64(toSecondsSinceEpoch(sent));
    2735          300 :     root[ConversationMapKeys::RECEIVED_MS] = Json::Int64(toMillisecondsSinceEpoch(sent));
    2736         1200 :     return {{"application/invite+json", json::toString(root)}};
    2737          600 : }
    2738              : 
    2739              : std::string
    2740           12 : Conversation::leave()
    2741              : {
    2742           12 :     setRemovingFlag();
    2743           12 :     std::lock_guard lk(pimpl_->writeMtx_);
    2744           24 :     return pimpl_->repository_->leave();
    2745           12 : }
    2746              : 
    2747              : void
    2748           16 : Conversation::setRemovingFlag()
    2749              : {
    2750           16 :     pimpl_->isRemoving_ = true;
    2751           16 : }
    2752              : 
    2753              : bool
    2754         4765 : Conversation::isRemoving()
    2755              : {
    2756         4765 :     return pimpl_->isRemoving_;
    2757              : }
    2758              : 
    2759              : void
    2760           27 : Conversation::erase()
    2761              : {
    2762           27 :     if (pimpl_->conversationDataPath_ != "")
    2763           27 :         dhtnet::fileutils::removeAll(pimpl_->conversationDataPath_, true);
    2764           27 :     if (!pimpl_->repository_)
    2765            0 :         return;
    2766           27 :     std::lock_guard lk(pimpl_->writeMtx_);
    2767           27 :     pimpl_->repository_->erase();
    2768           27 : }
    2769              : 
    2770              : ConversationMode
    2771         2816 : Conversation::mode() const
    2772              : {
    2773         2816 :     return pimpl_->repository_->mode();
    2774              : }
    2775              : 
    2776              : std::string
    2777           54 : Conversation::parentConversationId() const
    2778              : {
    2779           54 :     return pimpl_->repository_->parentConversationId();
    2780              : }
    2781              : 
    2782              : std::string
    2783            0 : Conversation::documentMimeType() const
    2784              : {
    2785            0 :     return pimpl_->repository_->documentMimeType();
    2786              : }
    2787              : 
    2788              : namespace {
    2789              : // The base64 update lines of every checkpoint commit in a document log,
    2790              : // oldest first, i.e. in the order the updates must be replayed.
    2791              : std::vector<std::string>
    2792           36 : collectUpdates(const std::vector<ConversationCommit>& commits)
    2793              : {
    2794           36 :     std::vector<std::string> updates;
    2795          202 :     for (auto it = commits.rbegin(); it != commits.rend(); ++it) {
    2796          166 :         if (it->commitMsg.type != CommitType::CHECKPOINT)
    2797          154 :             continue;
    2798           23 :         for (const auto& line : split_string(it->commitMsg.body, '\n'))
    2799           11 :             if (!line.empty())
    2800           23 :                 updates.emplace_back(line);
    2801              :     }
    2802           36 :     return updates;
    2803            0 : }
    2804              : } // namespace
    2805              : 
    2806              : std::vector<std::string>
    2807           36 : Conversation::documentUpdates() const
    2808              : {
    2809           36 :     LogOptions options;
    2810           36 :     options.skipMerge = true;
    2811           72 :     return collectUpdates(pimpl_->repository_->log(options));
    2812           36 : }
    2813              : 
    2814              : std::optional<std::vector<std::string>>
    2815            0 : Conversation::documentUpdatesAt(const std::string& commitId) const
    2816              : {
    2817            0 :     if (!getCommit(commitId))
    2818            0 :         return std::nullopt;
    2819            0 :     LogOptions options;
    2820            0 :     options.from = commitId;
    2821            0 :     options.skipMerge = true;
    2822            0 :     return collectUpdates(pimpl_->repository_->log(options));
    2823            0 : }
    2824              : 
    2825              : std::vector<std::map<std::string, std::string>>
    2826           61 : Conversation::documentHistory(size_t max) const
    2827              : {
    2828           61 :     LogOptions options;
    2829           61 :     options.skipMerge = true;
    2830           61 :     auto commits = pimpl_->repository_->log(options);
    2831           61 :     std::vector<std::map<std::string, std::string>> result;
    2832          236 :     for (const auto& commit : commits) {
    2833          175 :         if (commit.commitMsg.type != CommitType::CHECKPOINT)
    2834          157 :             continue;
    2835           18 :         size_t deltas = 0;
    2836           36 :         for (const auto& line : split_string(commit.commitMsg.body, '\n'))
    2837           18 :             if (!line.empty())
    2838           36 :                 ++deltas;
    2839          126 :         result.emplace_back(std::map<std::string, std::string> {
    2840           18 :             {"id", commit.id},
    2841           18 :             {"author", commit.authorId},
    2842           18 :             {"device", commit.author.email},
    2843            0 :             {"timestamp", std::to_string(commit.timestamp)},
    2844           18 :             {"deltas", std::to_string(deltas)},
    2845          108 :         });
    2846           18 :         if (max != 0 && result.size() >= max)
    2847            0 :             break;
    2848              :     }
    2849          122 :     return result;
    2850           79 : }
    2851              : 
    2852              : std::pair<std::string, std::string>
    2853            1 : Conversation::addDocumentAttachment(const std::vector<uint8_t>& data)
    2854              : {
    2855            1 :     std::unique_lock lk(pimpl_->writeMtx_);
    2856            1 :     auto headBefore = pimpl_->repository_->getHead();
    2857            1 :     auto attachmentId = pimpl_->repository_->addAttachment(data);
    2858            1 :     if (attachmentId.empty())
    2859            0 :         return {};
    2860            1 :     auto head = pimpl_->repository_->getHead();
    2861            1 :     if (head == headBefore)
    2862            0 :         return {attachmentId, {}}; // Same content already attached, nothing new to announce
    2863            1 :     pimpl_->announce(head, true);
    2864            1 :     return {attachmentId, head};
    2865            1 : }
    2866              : 
    2867              : std::vector<uint8_t>
    2868            3 : Conversation::documentAttachment(const std::string& attachmentId) const
    2869              : {
    2870            3 :     return pimpl_->repository_->attachment(attachmentId);
    2871              : }
    2872              : 
    2873              : std::vector<std::string>
    2874           36 : Conversation::documentAttachmentIds() const
    2875              : {
    2876           36 :     return pimpl_->repository_->attachmentIds();
    2877              : }
    2878              : 
    2879              : std::vector<std::string>
    2880           17 : Conversation::getInitialMembers() const
    2881              : {
    2882           17 :     return pimpl_->repository_->getInitialMembers();
    2883              : }
    2884              : 
    2885              : bool
    2886            0 : Conversation::isInitialMember(const std::string& uri) const
    2887              : {
    2888            0 :     auto members = getInitialMembers();
    2889            0 :     return std::find(members.begin(), members.end(), uri) != members.end();
    2890            0 : }
    2891              : 
    2892              : void
    2893           26 : Conversation::updateInfos(const std::map<std::string, std::string>& map, const OnDoneCb& cb)
    2894              : {
    2895           26 :     dht::ThreadPool::io().run([w = weak(), map = std::move(map), cb = std::move(cb)] {
    2896           26 :         if (auto sthis = w.lock()) {
    2897           26 :             auto& repo = sthis->pimpl_->repository_;
    2898           26 :             std::unique_lock lk(sthis->pimpl_->writeMtx_);
    2899           26 :             auto commit = repo->updateInfos(map);
    2900           26 :             sthis->pimpl_->announce(commit, true);
    2901           26 :             lk.unlock();
    2902           26 :             if (cb)
    2903           26 :                 cb(!commit.empty(), commit);
    2904           26 :             if (repo->mode() == ConversationMode::DOCUMENT)
    2905           18 :                 return; // A document is not a conversation for the client; a rename
    2906              :                         // is reported through CollaborativeDocumentRenamed instead
    2907            8 :             emitSignal<libjami::ConversationSignal::ConversationProfileUpdated>(sthis->pimpl_->accountId_,
    2908            8 :                                                                                 repo->id(),
    2909           16 :                                                                                 repo->infos());
    2910           70 :         }
    2911              :     });
    2912           26 : }
    2913              : 
    2914              : std::map<std::string, std::string>
    2915          385 : Conversation::infos() const
    2916              : {
    2917          385 :     return pimpl_->repository_->infos();
    2918              : }
    2919              : 
    2920              : void
    2921            8 : Conversation::updatePreferences(const std::map<std::string, std::string>& map)
    2922              : {
    2923            8 :     const auto& filePath = pimpl_->preferencesPath_;
    2924            8 :     auto prefs = map;
    2925            8 :     auto itLast = prefs.find(LAST_MODIFIED);
    2926            8 :     if (itLast != prefs.end()) {
    2927            3 :         std::error_code ec;
    2928            3 :         if (std::filesystem::is_regular_file(filePath, ec)) {
    2929            1 :             auto lastModified = fileutils::lastWriteTimeInSeconds(filePath);
    2930              :             try {
    2931            1 :                 if (lastModified >= to_int<uint64_t>(itLast->second))
    2932            0 :                     return;
    2933            0 :             } catch (...) {
    2934            0 :                 return;
    2935            0 :             }
    2936              :         }
    2937            3 :         prefs.erase(itLast);
    2938              :     }
    2939              : 
    2940            8 :     std::ofstream file(filePath, std::ios::trunc | std::ios::binary);
    2941            8 :     msgpack::pack(file, prefs);
    2942            8 :     emitSignal<libjami::ConversationSignal::ConversationPreferencesUpdated>(pimpl_->accountId_, id(), std::move(prefs));
    2943            8 : }
    2944              : 
    2945              : std::map<std::string, std::string>
    2946           92 : Conversation::preferences(bool includeLastModified) const
    2947              : {
    2948              :     try {
    2949           92 :         std::map<std::string, std::string> preferences;
    2950           92 :         const auto& filePath = pimpl_->preferencesPath_;
    2951          173 :         auto file = fileutils::loadFile(filePath);
    2952           11 :         msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
    2953           11 :         oh.get().convert(preferences);
    2954           11 :         if (includeLastModified)
    2955           24 :             preferences[LAST_MODIFIED] = std::to_string(fileutils::lastWriteTimeInSeconds(filePath));
    2956           11 :         return preferences;
    2957          173 :     } catch (const std::exception& e) {
    2958           81 :     }
    2959           81 :     return {};
    2960              : }
    2961              : 
    2962              : std::vector<uint8_t>
    2963            0 : Conversation::vCard() const
    2964              : {
    2965              :     try {
    2966            0 :         return fileutils::loadFile(pimpl_->repoPath_ / "profile.vcf");
    2967            0 :     } catch (...) {
    2968            0 :     }
    2969            0 :     return {};
    2970              : }
    2971              : 
    2972              : std::shared_ptr<TransferManager>
    2973         1922 : Conversation::dataTransfer() const
    2974              : {
    2975         1922 :     return pimpl_->transferManager_;
    2976              : }
    2977              : 
    2978              : bool
    2979           13 : Conversation::onFileChannelRequest(const std::string& member,
    2980              :                                    const std::string& fileId,
    2981              :                                    std::filesystem::path& path,
    2982              :                                    std::string& sha3sum) const
    2983              : {
    2984           13 :     if (!isMember(member))
    2985            0 :         return false;
    2986              : 
    2987           13 :     auto sep = fileId.find('_');
    2988           13 :     if (sep == std::string::npos)
    2989            0 :         return false;
    2990              : 
    2991           13 :     auto interactionId = fileId.substr(0, sep);
    2992           13 :     auto commit = getCommit(interactionId);
    2993           26 :     if (commit == std::nullopt || commit->commitMsg.tid.empty() || commit->commitMsg.sha3sum.empty()
    2994           26 :         || commit->commitMsg.type != CommitType::DATA_TRANSFER) {
    2995            0 :         JAMI_WARNING("[Account {:s}] {} requested invalid file transfer commit {}",
    2996              :                      pimpl_->accountId_,
    2997              :                      member,
    2998              :                      interactionId);
    2999            0 :         return false;
    3000              :     }
    3001              : 
    3002           13 :     path = dataTransfer()->path(fileId);
    3003           13 :     sha3sum = commit->commitMsg.sha3sum;
    3004              : 
    3005           13 :     return true;
    3006           13 : }
    3007              : 
    3008              : bool
    3009           14 : Conversation::downloadFile(const std::string& interactionId,
    3010              :                            const std::string& fileId,
    3011              :                            const std::string& path,
    3012              :                            const std::string&,
    3013              :                            const std::string& deviceId)
    3014              : {
    3015           14 :     auto commit = getCommit(interactionId);
    3016           14 :     if (commit == std::nullopt || commit->commitMsg.type != CommitType::DATA_TRANSFER) {
    3017            0 :         JAMI_ERROR("Commit doesn't exists or is not a file transfer {} (Conversation: {}) ", interactionId, id());
    3018            0 :         return false;
    3019              :     }
    3020           14 :     auto tid = commit->commitMsg.tid;
    3021           14 :     auto sha3sum = commit->commitMsg.sha3sum;
    3022           14 :     auto totalSize = commit->commitMsg.totalSize;
    3023              : 
    3024           14 :     if (tid.empty() || sha3sum.empty() || totalSize < 0) {
    3025            0 :         JAMI_ERROR("Invalid file transfer commit (missing tid, size or sha3)");
    3026            0 :         return false;
    3027              :     }
    3028              : 
    3029              :     // Be sure to not lock conversation
    3030           14 :     dht::ThreadPool().io().run([w = weak(), deviceId, fileId, interactionId, sha3sum, path, totalSize] {
    3031           14 :         if (auto shared = w.lock()) {
    3032           14 :             std::filesystem::path filePath(path);
    3033           14 :             if (filePath.empty()) {
    3034            0 :                 filePath = shared->dataTransfer()->path(fileId);
    3035              :             }
    3036              : 
    3037           14 :             std::error_code ec;
    3038           14 :             if (std::filesystem::file_size(filePath, ec) == static_cast<size_t>(totalSize)) {
    3039            1 :                 if (fileutils::sha3File(filePath) == sha3sum) {
    3040            1 :                     JAMI_WARNING("Ignoring request to download existing file: {}", filePath);
    3041            1 :                     return;
    3042              :                 }
    3043              :             }
    3044              : 
    3045           13 :             std::filesystem::path tempFilePath(filePath);
    3046           13 :             tempFilePath += ".tmp";
    3047           13 :             auto start = std::filesystem::file_size(tempFilePath, ec);
    3048           13 :             if (ec || start == static_cast<decltype(start)>(-1)) {
    3049           11 :                 start = 0;
    3050              :             }
    3051           13 :             size_t end = 0;
    3052              : 
    3053           13 :             auto acc = shared->pimpl_->account_.lock();
    3054           13 :             if (!acc)
    3055            0 :                 return;
    3056           13 :             shared->dataTransfer()->waitForTransfer(fileId, interactionId, sha3sum, path, totalSize);
    3057           13 :             acc->askForFileChannel(shared->id(), deviceId, interactionId, fileId, start, end);
    3058           28 :         }
    3059              :     });
    3060           14 :     return true;
    3061           14 : }
    3062              : 
    3063              : void
    3064         1963 : Conversation::hasFetched(const std::string& deviceId, const std::string& commitId)
    3065              : {
    3066         1963 :     dht::ThreadPool::io().run([w = weak(), deviceId, commitId]() {
    3067         1963 :         auto sthis = w.lock();
    3068         1963 :         if (!sthis)
    3069            0 :             return;
    3070              :         // Update fetched for Uri
    3071         1963 :         auto uri = sthis->uriFromDevice(deviceId);
    3072         1961 :         if (uri.empty() || uri == sthis->pimpl_->userId_)
    3073           46 :             return;
    3074              :         // When a user fetches a commit, the message is sent for this person
    3075         1916 :         sthis->pimpl_->updateStatus(uri,
    3076              :                                     libjami::Account::MessageStates::SENT,
    3077         1917 :                                     commitId,
    3078         3833 :                                     std::to_string(std::time(nullptr)),
    3079              :                                     true);
    3080         2009 :     });
    3081         1963 : }
    3082              : 
    3083              : void
    3084         1968 : Conversation::Impl::updateStatus(const std::string& uri,
    3085              :                                  libjami::Account::MessageStates st,
    3086              :                                  const std::string& commitId,
    3087              :                                  const std::string& ts,
    3088              :                                  bool emit)
    3089              : {
    3090              :     // This method can be called if peer send us a status or if another device sync. Emit will be true if a peer
    3091              :     // send us a status and will emit to other connected devices.
    3092         1968 :     LogOptions options;
    3093         1968 :     std::map<std::string, std::map<std::string, std::string>> newStatus;
    3094              :     {
    3095              :         // Update internal structures.
    3096         1968 :         std::lock_guard lk(messageStatusMtx_);
    3097         1968 :         auto& status = messagesStatus_[uri];
    3098         1967 :         auto& oldStatus = status[st == libjami::Account::MessageStates::SENT ? "fetched" : "read"];
    3099         1968 :         if (oldStatus == commitId)
    3100          261 :             return; // Nothing to do
    3101         1707 :         options.to = oldStatus;
    3102         1707 :         options.from = commitId;
    3103         1707 :         oldStatus = commitId;
    3104         1707 :         status[st == libjami::Account::MessageStates::SENT ? "fetched_ts" : "read_ts"] = ts;
    3105         1707 :         saveStatus();
    3106         1707 :         if (emit)
    3107         1672 :             newStatus[uri].insert(status.begin(), status.end());
    3108         1968 :     }
    3109         1707 :     if (emit && messageStatusCb_) {
    3110         1653 :         messageStatusCb_(newStatus);
    3111              :     }
    3112              :     // Update messages status for all commit between the old and new one
    3113         1707 :     options.logIfNotFound = false;
    3114         1707 :     options.fastLog = true;
    3115         1707 :     History optHistory;
    3116         1707 :     std::unique_lock lk(optHistory.mutex); // Avoid to announce messages while updating status.
    3117         1707 :     auto res = loadMessages(options, &optHistory);
    3118         1707 :     std::unique_lock mlk(messageStatusMtx_);
    3119         1707 :     std::vector<std::pair<std::string, int32_t>> statusToUpdate;
    3120         1707 :     if (res.size() == 0) {
    3121              :         // In this case, commit is not received yet, so we cache it
    3122           18 :         futureStatus[commitId][uri] = static_cast<int32_t>(st);
    3123              :     }
    3124        18287 :     for (const auto& [cid, _] : optHistory.quickAccess) {
    3125        16579 :         auto message = loadedHistory_.quickAccess.find(cid);
    3126        16577 :         if (message != loadedHistory_.quickAccess.end()) {
    3127              :             // Update message and emit to client,
    3128         4390 :             if (static_cast<int32_t>(st) > message->second->status[uri]) {
    3129         4383 :                 message->second->status[uri] = static_cast<int32_t>(st);
    3130         4382 :                 statusToUpdate.emplace_back(cid, static_cast<int32_t>(st));
    3131              :             }
    3132              :         } else {
    3133              :             // In this case, commit is not loaded by client, so we cache it
    3134              :             // No need to emit to client, they will get a correct status on load.
    3135        12189 :             futureStatus[cid][uri] = static_cast<int32_t>(st);
    3136              :         }
    3137              :     }
    3138         1707 :     mlk.unlock();
    3139         1707 :     lk.unlock();
    3140         6090 :     for (const auto& [cid, status] : statusToUpdate)
    3141         8760 :         emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(accountId_,
    3142         4376 :                                                                               repository_->id(),
    3143              :                                                                               uri,
    3144              :                                                                               cid,
    3145              :                                                                               static_cast<int>(status));
    3146         2229 : }
    3147              : 
    3148              : bool
    3149           18 : Conversation::setMessageDisplayed(const std::string& uri, const std::string& interactionId)
    3150              : {
    3151           18 :     std::lock_guard lk(pimpl_->messageStatusMtx_);
    3152           54 :     if (pimpl_->messagesStatus_[uri]["read"] == interactionId)
    3153            2 :         return false; // Nothing to do
    3154           16 :     dht::ThreadPool::io().run([w = weak(), uri, interactionId]() {
    3155           16 :         auto sthis = w.lock();
    3156           16 :         if (!sthis)
    3157            0 :             return;
    3158           16 :         sthis->pimpl_->updateStatus(uri,
    3159              :                                     libjami::Account::MessageStates::DISPLAYED,
    3160           16 :                                     interactionId,
    3161           32 :                                     std::to_string(std::time(nullptr)),
    3162              :                                     true);
    3163           16 :     });
    3164           16 :     return true;
    3165           18 : }
    3166              : 
    3167              : std::map<std::string, std::map<std::string, std::string>>
    3168           74 : Conversation::messageStatus() const
    3169              : {
    3170           74 :     std::lock_guard lk(pimpl_->messageStatusMtx_);
    3171          148 :     return pimpl_->messagesStatus_;
    3172           74 : }
    3173              : 
    3174              : void
    3175           50 : Conversation::updateMessageStatus(const std::map<std::string, std::map<std::string, std::string>>& messageStatus)
    3176              : {
    3177           50 :     std::unique_lock lk(pimpl_->messageStatusMtx_);
    3178           50 :     std::vector<std::tuple<libjami::Account::MessageStates, std::string, std::string, std::string>> stVec;
    3179          117 :     for (const auto& [uri, status] : messageStatus) {
    3180           67 :         auto& oldMs = pimpl_->messagesStatus_[uri];
    3181          397 :         if (status.find("fetched_ts") != status.end() && status.at("fetched") != oldMs["fetched"]) {
    3182          145 :             if (oldMs["fetched_ts"].empty() || std::stol(oldMs["fetched_ts"]) <= std::stol(status.at("fetched_ts"))) {
    3183            0 :                 stVec.emplace_back(libjami::Account::MessageStates::SENT,
    3184              :                                    uri,
    3185           31 :                                    status.at("fetched"),
    3186           93 :                                    status.at("fetched_ts"));
    3187              :             }
    3188              :         }
    3189          229 :         if (status.find("read_ts") != status.end() && status.at("read") != oldMs["read"]) {
    3190           12 :             if (oldMs["read_ts"].empty() || std::stol(oldMs["read_ts"]) <= std::stol(status.at("read_ts"))) {
    3191            0 :                 stVec.emplace_back(libjami::Account::MessageStates::DISPLAYED,
    3192              :                                    uri,
    3193            4 :                                    status.at("read"),
    3194           12 :                                    status.at("read_ts"));
    3195              :             }
    3196              :         }
    3197              :     }
    3198           50 :     lk.unlock();
    3199              : 
    3200           85 :     for (const auto& [status, uri, commitId, ts] : stVec) {
    3201           35 :         pimpl_->updateStatus(uri, status, commitId, ts);
    3202              :     }
    3203           50 : }
    3204              : 
    3205              : void
    3206          413 : Conversation::onMessageStatusChanged(
    3207              :     const std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)>& cb)
    3208              : {
    3209          413 :     std::unique_lock lk(pimpl_->messageStatusMtx_);
    3210          413 :     pimpl_->messageStatusCb_ = cb;
    3211          413 : }
    3212              : 
    3213              : #ifdef LIBJAMI_TEST
    3214              : void
    3215          524 : Conversation::onBootstrapStatus(const std::function<void(std::string, BootstrapStatus)>& cb)
    3216              : {
    3217          524 :     std::lock_guard lock(pimpl_->bootstrapMtx_);
    3218          524 :     pimpl_->bootstrapCbTest_ = cb;
    3219          524 : }
    3220              : 
    3221              : std::vector<libjami::SwarmMessage>
    3222            0 : Conversation::loadMessagesSync(const LogOptions& options)
    3223              : {
    3224            0 :     std::lock_guard lk(pimpl_->loadedHistory_.mutex);
    3225            0 :     auto result = pimpl_->loadMessages(options);
    3226            0 :     return result;
    3227            0 : }
    3228              : 
    3229              : void
    3230            0 : Conversation::announce(const std::vector<std::map<std::string, std::string>>& commits, bool commitFromSelf)
    3231              : {
    3232            0 :     pimpl_->announce(commits, commitFromSelf);
    3233            0 : }
    3234              : 
    3235              : void
    3236            0 : Conversation::announce(const std::string& commitId, bool commitFromSelf)
    3237              : {
    3238            0 :     pimpl_->announce(commitId, commitFromSelf);
    3239            0 : }
    3240              : #endif
    3241              : 
    3242              : void
    3243          524 : Conversation::bootstrap(std::function<void()> onBootstrapped, const std::vector<DeviceId>& knownDevices)
    3244              : {
    3245          524 :     std::lock_guard lock(pimpl_->bootstrapMtx_);
    3246          524 :     if (!pimpl_ || !pimpl_->repository_ || !pimpl_->swarmManager_)
    3247            0 :         return;
    3248              :     // Bootstrap the DRT from currently known devices. Since the per-device
    3249              :     // presence monitoring (monitorConnection/startTracking below), callers do
    3250              :     // not pass any device list here: candidates are injected as members get
    3251              :     // online, through addKnownDevices() with devices reported by the
    3252              :     // PresenceManager (i.e. announced on the DHT), and rotated on connection
    3253              :     // failure. The knownDevices parameter remains for callers/tests that
    3254              :     // already hold a list of live devices.
    3255              :     // If a connection succeeds, onConnectionChanged will be called with ok=true
    3256          524 :     pimpl_->bootstrapCb_ = std::move(onBootstrapped);
    3257          524 :     std::vector<DeviceId> devices = knownDevices;
    3258          524 :     JAMI_DEBUG("{} Bootstrap with {} device(s)", pimpl_->toString(), devices.size());
    3259              : 
    3260          524 :     if (!devices.empty()) {
    3261            0 :         pimpl_->swarmManager_->setKnownNodes(devices);
    3262              :     }
    3263              : 
    3264          524 :     pimpl_->monitorConnection(weak_from_this());
    3265              : 
    3266              :     // If is shutdown, the conversation was re-added, causing no new nodes to be connected, but just a classic
    3267              :     // connectivity change
    3268          524 :     if (pimpl_->swarmManager_->isShutdown()) {
    3269           17 :         pimpl_->swarmManager_->restart();
    3270           17 :         pimpl_->swarmManager_->maintainBuckets();
    3271          507 :     } else if (!pimpl_->swarmManager_->isConnected()) {
    3272              :         // A swarm manager that is up but holds no connected node will never get
    3273              :         // one on its own: setKnownNodes() only acts on ids it has never seen
    3274              :         // before, so members it already knows are simply skipped.
    3275              :         //
    3276              :         // A mobile client reaches that state every time it leaves and returns
    3277              :         // to the foreground: setAccountActive() leaves established connections
    3278              :         // alone by default, so the swarm manager is not shut down, yet the
    3279              :         // links die with the network. Bootstrapping then finds nothing to do,
    3280              :         // and the conversation stays silent until the process is restarted.
    3281          497 :         pimpl_->swarmManager_->maintainBuckets();
    3282              :     }
    3283          524 : }
    3284              : 
    3285              : void
    3286         1644 : Conversation::addKnownDevices(const std::vector<DeviceId>& devices, const std::string& memberUri)
    3287              : {
    3288         1644 :     if (devices.empty())
    3289          928 :         return;
    3290          716 :     if (!memberUri.empty()) {
    3291              :         // JAMI_WARNING("{} Adding {} known devices for member {}", pimpl_->toString(), devices.size(), memberUri);
    3292          716 :         std::lock_guard lk(pimpl_->trackedMembersMtx_);
    3293          716 :         auto it = pimpl_->trackedMembers_.find(memberUri);
    3294          716 :         if (it != pimpl_->trackedMembers_.end()) {
    3295          710 :             it->second.devices.insert(devices.begin(), devices.end());
    3296              :         }
    3297          716 :     } else {
    3298            0 :         JAMI_ERROR("{} Adding {} known devices without member URI", pimpl_->toString(), devices.size());
    3299              :     }
    3300          716 :     pimpl_->swarmManager_->setKnownNodes(devices);
    3301              : }
    3302              : 
    3303              : void
    3304          597 : Conversation::connectNode(const DeviceId& deviceId)
    3305              : {
    3306          597 :     pimpl_->swarmManager_->connectNode(deviceId);
    3307          597 : }
    3308              : 
    3309              : std::vector<std::string>
    3310           18 : Conversation::commitsEndedCalls()
    3311              : {
    3312           18 :     pimpl_->loadActiveCalls();
    3313           18 :     pimpl_->loadHostedCalls();
    3314           18 :     auto commits = pimpl_->commitsEndedCalls();
    3315           18 :     if (!commits.empty()) {
    3316              :         // Announce to client
    3317            0 :         dht::ThreadPool::io().run([w = weak(), commits] {
    3318            0 :             if (auto sthis = w.lock())
    3319            0 :                 sthis->pimpl_->announce(commits, true);
    3320            0 :         });
    3321              :     }
    3322           18 :     return commits;
    3323            0 : }
    3324              : 
    3325              : void
    3326          429 : Conversation::onMembersChanged(OnMembersChanged&& cb)
    3327              : {
    3328          429 :     pimpl_->onMembersChanged_ = std::move(cb);
    3329          429 : }
    3330              : 
    3331              : void
    3332          429 : Conversation::onNeedSocket(NeedSocketCb needSocket)
    3333              : {
    3334          858 :     pimpl_->swarmManager_->needSocketCb_ = [needSocket = std::move(needSocket),
    3335              :                                             w = weak()](const std::string& deviceId, ChannelCb&& cb, bool noNewSocket) {
    3336         1175 :         if (auto sthis = w.lock()) {
    3337         1173 :             auto wrappedCb = [cb = std::move(cb), w, deviceId](const std::shared_ptr<dhtnet::ChannelSocket>& socket) {
    3338         1174 :                 if (auto sthis = w.lock()) {
    3339         1171 :                     if (!socket) {
    3340          343 :                         if (auto acc = sthis->pimpl_->account_.lock()) {
    3341          343 :                             auto cert = acc->certStore().getCertificate(deviceId);
    3342          344 :                             if (cert && cert->issuer) {
    3343          344 :                                 sthis->pimpl_->onConnectionFailed(DeviceId(deviceId), cert->issuer->getId().toString());
    3344              :                             } else {
    3345            0 :                                 JAMI_WARNING("{} Unable to get member URI from device ID {}",
    3346              :                                              sthis->pimpl_->toString(),
    3347              :                                              deviceId);
    3348              :                             }
    3349          344 :                         } else {
    3350            0 :                             return false;
    3351          344 :                         }
    3352              :                     }
    3353         1175 :                 }
    3354         1175 :                 return cb(socket);
    3355         1173 :             };
    3356         2350 :             needSocket(sthis->id(), deviceId, std::move(wrappedCb), "application/im-gitmessage-id", noNewSocket);
    3357         2350 :         }
    3358         1604 :     };
    3359          429 : }
    3360              : 
    3361              : void
    3362         1080 : Conversation::addSwarmChannel(std::shared_ptr<dhtnet::ChannelSocket> channel)
    3363              : {
    3364         1080 :     auto deviceId = channel->deviceId();
    3365              :     // Transmit avatar if necessary
    3366              :     // We do this here, because at this point we know both sides are connected and in
    3367              :     // the same conversation
    3368              :     // addSwarmChannel is a bit more complex, but it should be the best moment to do this.
    3369         1080 :     auto cert = channel->peerCertificate();
    3370         1080 :     if (!cert || !cert->issuer)
    3371            0 :         return;
    3372         1080 :     auto member = cert->issuer->getId().toString();
    3373              :     // The TLS handshake authenticated this certificate: pin it so that any mobile
    3374              :     // lease this device gossips can be verified without a lookup.
    3375         1080 :     if (auto account = pimpl_->account_.lock())
    3376         1080 :         account->certStore().pinCertificate(cert);
    3377         1080 :     pimpl_->swarmManager_->addChannel(std::move(channel));
    3378         1080 :     dht::ThreadPool::io().run([member, deviceId, a = pimpl_->account_, w = weak_from_this()] {
    3379         1080 :         auto sthis = w.lock();
    3380         1080 :         if (auto account = a.lock()) {
    3381         1079 :             account->sendProfile(sthis->id(), member, deviceId.toString());
    3382         1080 :         }
    3383         1080 :     });
    3384         1080 : }
    3385              : 
    3386              : uint32_t
    3387            4 : Conversation::countInteractions(const std::string& toId, const std::string& fromId, const std::string& authorUri) const
    3388              : {
    3389            4 :     LogOptions options;
    3390            4 :     options.to = toId;
    3391            4 :     options.from = fromId;
    3392            4 :     options.authorUri = authorUri;
    3393            4 :     options.logIfNotFound = false;
    3394            4 :     options.fastLog = true;
    3395            4 :     History history;
    3396            4 :     std::lock_guard lk(history.mutex);
    3397            4 :     auto res = pimpl_->loadMessages(options, &history);
    3398            8 :     return res.size();
    3399            4 : }
    3400              : 
    3401              : void
    3402            4 : Conversation::search(uint32_t req, const Filter& filter, const std::shared_ptr<std::atomic_int>& flag) const
    3403              : {
    3404              :     // Because logging a conversation can take quite some time,
    3405              :     // do it asynchronously
    3406            4 :     dht::ThreadPool::io().run([w = weak(), req, filter, flag] {
    3407            4 :         if (auto sthis = w.lock()) {
    3408            4 :             History history;
    3409            4 :             std::vector<std::map<std::string, std::string>> commits {};
    3410              :             // std::regex_constants::ECMAScript is the default flag.
    3411            4 :             auto re = std::regex(filter.regexSearch,
    3412            4 :                                  filter.caseSensitive ? std::regex_constants::ECMAScript : std::regex_constants::icase);
    3413           12 :             sthis->pimpl_->repository_->log(
    3414            8 :                 [&](const std::string& /*id*/, const GitAuthor& author, const GitCommit& commit) {
    3415           20 :                     if (!filter.author.empty() && filter.author != sthis->uriFromDevice(author.email)) {
    3416              :                         // Filter author
    3417            0 :                         return CallbackResult::Skip;
    3418              :                     }
    3419           20 :                     auto commitTime = git_commit_time(commit.get());
    3420           20 :                     if (filter.before && filter.before < commitTime) {
    3421              :                         // Only get commits before this date
    3422            0 :                         return CallbackResult::Skip;
    3423              :                     }
    3424           20 :                     if (filter.after && filter.after > commitTime) {
    3425              :                         // Only get commits before this date
    3426            0 :                         if (git_commit_parentcount(commit.get()) <= 1)
    3427            0 :                             return CallbackResult::Break;
    3428              :                         else
    3429            0 :                             return CallbackResult::Skip; // Because we are sorting it with
    3430              :                                                          // GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME
    3431              :                     }
    3432              : 
    3433           20 :                     return CallbackResult::Ok; // Continue
    3434              :                 },
    3435            8 :                 [&](ConversationCommit&& cc) {
    3436           20 :                     if (auto optMessage = sthis->pimpl_->repository_->convCommitToMap(cc))
    3437           80 :                         sthis->pimpl_->addToHistory(history, {optMessage.value()}, false, false);
    3438           40 :                 },
    3439            8 :                 [&](const std::string& id, const GitAuthor&, ConversationCommit&) {
    3440           20 :                     if (id == filter.lastId)
    3441            0 :                         return true;
    3442           20 :                     return false;
    3443              :                 },
    3444              :                 "",
    3445              :                 false);
    3446              :             // Search on generated history
    3447           24 :             for (auto& message : history.messageList) {
    3448           20 :                 auto contentType = message->type;
    3449           20 :                 auto isSearchable = contentType == CommitType::TEXT || contentType == CommitType::DATA_TRANSFER;
    3450           20 :                 if (filter.type.empty() && !isSearchable) {
    3451              :                     // Not searchable, at least for now
    3452            8 :                     continue;
    3453           12 :                 } else if (contentType == filter.type || filter.type.empty()) {
    3454           12 :                     if (isSearchable) {
    3455              :                         // If it's a text match the body, else the display name
    3456           60 :                         auto body = contentType == CommitType::TEXT ? message->body.at(CommitKey::BODY)
    3457           36 :                                                                     : message->body.at(CommitKey::DISPLAY_NAME);
    3458           12 :                         std::smatch body_match;
    3459           12 :                         if (std::regex_search(body, body_match, re)) {
    3460            5 :                             auto commit = message->body;
    3461           10 :                             commit["id"] = message->id;
    3462           10 :                             commit[CommitKey::TYPE] = message->type;
    3463            5 :                             commits.emplace_back(commit);
    3464            5 :                         }
    3465           12 :                     } else {
    3466              :                         // Matching type, just add it to the results
    3467            0 :                         commits.emplace_back(message->body);
    3468              :                     }
    3469              : 
    3470           12 :                     if (filter.maxResult != 0 && commits.size() == filter.maxResult)
    3471            0 :                         break;
    3472              :                 }
    3473           20 :             }
    3474              : 
    3475            4 :             if (commits.size() > 0)
    3476            9 :                 emitSignal<libjami::ConversationSignal::MessagesFound>(req,
    3477            3 :                                                                        sthis->pimpl_->accountId_,
    3478            6 :                                                                        sthis->id(),
    3479            3 :                                                                        std::move(commits));
    3480              :             // If we're the latest thread, inform client that the search is finished
    3481            4 :             if ((*flag)-- == 1 /* decrement return the old value */) {
    3482            8 :                 emitSignal<libjami::ConversationSignal::MessagesFound>(
    3483            8 :                     req, sthis->pimpl_->accountId_, std::string {}, std::vector<std::map<std::string, std::string>> {});
    3484              :             }
    3485            8 :         }
    3486            4 :     });
    3487            4 : }
    3488              : 
    3489              : void
    3490           14 : Conversation::hostConference(CommitMessage&& message, OnDoneCb&& cb)
    3491              : {
    3492           14 :     if (message.confId.empty()) {
    3493            0 :         JAMI_ERROR("{}Malformed commit: no confId", pimpl_->toString());
    3494            0 :         return;
    3495              :     }
    3496              : 
    3497           14 :     auto now = std::chrono::system_clock::now();
    3498           14 :     auto nowSecs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
    3499              :     {
    3500           14 :         std::lock_guard lk(pimpl_->activeCallsMtx_);
    3501           14 :         pimpl_->hostedCalls_[message.confId] = nowSecs;
    3502           14 :         pimpl_->saveHostedCalls();
    3503           14 :     }
    3504              : 
    3505           14 :     createCommit(std::move(message), {}, std::move(cb));
    3506              : }
    3507              : 
    3508              : bool
    3509           20 : Conversation::isHosting(const std::string& confId) const
    3510              : {
    3511           20 :     auto info = infos();
    3512           64 :     if (info["rdvDevice"] == pimpl_->deviceId_ && info["rdvHost"] == pimpl_->userId_)
    3513            0 :         return true; // We are the current device Host
    3514           20 :     std::lock_guard lk(pimpl_->activeCallsMtx_);
    3515           20 :     return pimpl_->hostedCalls_.find(confId) != pimpl_->hostedCalls_.end();
    3516           20 : }
    3517              : 
    3518              : void
    3519           11 : Conversation::removeActiveConference(CommitMessage&& message, OnDoneCb&& cb)
    3520              : {
    3521           11 :     if (message.confId.empty()) {
    3522            0 :         JAMI_ERROR("{}Malformed commit: no confId", pimpl_->toString());
    3523            0 :         return;
    3524              :     }
    3525              : 
    3526           11 :     auto erased = false;
    3527              :     {
    3528           11 :         std::lock_guard lk(pimpl_->activeCallsMtx_);
    3529           11 :         erased = pimpl_->hostedCalls_.erase(message.confId);
    3530           11 :     }
    3531           11 :     if (erased) {
    3532           11 :         pimpl_->saveHostedCalls();
    3533           11 :         createCommit(std::move(message), {}, std::move(cb));
    3534              :     } else
    3535            0 :         cb(false, "");
    3536              : }
    3537              : 
    3538              : std::vector<std::map<std::string, std::string>>
    3539           39 : Conversation::currentCalls() const
    3540              : {
    3541           39 :     std::lock_guard lk(pimpl_->activeCallsMtx_);
    3542           78 :     return pimpl_->activeCalls_;
    3543           39 : }
    3544              : } // namespace jami
        

Generated by: LCOV version 2.0-1