LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1888 2569 73.5 %
Date: 2026-01-22 10:39:23 Functions: 229 633 36.2 %

          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 "conversationrepository.h"
      19             : 
      20             : #include "account_const.h"
      21             : #include "base64.h"
      22             : #include "jamiaccount.h"
      23             : #include "fileutils.h"
      24             : #include "gittransport.h"
      25             : #include "string_utils.h"
      26             : #include "client/ring_signal.h"
      27             : #include "vcard.h"
      28             : #include "json_utils.h"
      29             : #include "fileutils.h"
      30             : #include "logger.h"
      31             : #include "jami/conversation_interface.h"
      32             : 
      33             : #include <opendht/crypto.h>
      34             : 
      35             : #include <git2/blob.h>
      36             : #include <git2/buffer.h>
      37             : #include <git2/commit.h>
      38             : #include <git2/deprecated.h>
      39             : #include <git2/refs.h>
      40             : #include <git2/object.h>
      41             : #include <git2/indexer.h>
      42             : #include <git2/remote.h>
      43             : #include <git2/merge.h>
      44             : #include <git2/diff.h>
      45             : 
      46             : #include <algorithm>
      47             : #include <iterator>
      48             : #include <ctime>
      49             : #include <fstream>
      50             : #include <future>
      51             : #include <json/json.h>
      52             : #include <regex>
      53             : #include <exception>
      54             : #include <optional>
      55             : #include <memory>
      56             : #include <cstdint>
      57             : #include <utility>
      58             : 
      59             : using namespace std::string_view_literals;
      60             : constexpr auto DIFF_REGEX = " +\\| +[0-9]+.*"sv;
      61             : constexpr size_t MAX_FETCH_SIZE {256 * 1024 * 1024}; // 256Mb
      62             : 
      63             : namespace jami {
      64             : 
      65             : #ifdef LIBJAMI_TEST
      66             : bool ConversationRepository::DISABLE_RESET = false;
      67             : #endif
      68             : 
      69             : static const std::regex regex_display_name("<|>");
      70             : 
      71             : inline std::string_view
      72        6172 : as_view(const git_blob* blob)
      73             : {
      74        6172 :     return std::string_view(static_cast<const char*>(git_blob_rawcontent(blob)), git_blob_rawsize(blob));
      75             : }
      76             : inline std::string_view
      77        6172 : as_view(const GitObject& blob)
      78             : {
      79        6172 :     return as_view(reinterpret_cast<git_blob*>(blob.get()));
      80             : }
      81             : 
      82             : class ConversationRepository::Impl
      83             : {
      84             : public:
      85         426 :     Impl(const std::shared_ptr<JamiAccount>& account, const std::string& id)
      86         426 :         : account_(account)
      87         426 :         , id_(id)
      88         426 :         , accountId_(account->getAccountID())
      89         426 :         , userId_(account->getUsername())
      90         852 :         , deviceId_(account->currentDeviceId())
      91             :     {
      92         425 :         conversationDataPath_ = fileutils::get_data_dir() / accountId_ / "conversation_data" / id_;
      93         426 :         membersCache_ = conversationDataPath_ / "members";
      94         426 :         checkLocks();
      95         426 :         loadMembers();
      96         426 :         if (members_.empty()) {
      97         412 :             initMembers();
      98             :         }
      99         435 :     }
     100             : 
     101         426 :     void checkLocks()
     102             :     {
     103         426 :         auto repo = repository();
     104         426 :         if (!repo)
     105           0 :             throw std::logic_error("Invalid git repository");
     106             : 
     107         426 :         std::filesystem::path repoPath = git_repository_path(repo.get());
     108         426 :         std::error_code ec;
     109             : 
     110         426 :         auto indexPath = std::filesystem::path(repoPath / "index.lock");
     111         426 :         if (std::filesystem::exists(indexPath, ec)) {
     112           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}",
     113             :                          accountId_,
     114             :                          id_,
     115             :                          indexPath);
     116           0 :             std::filesystem::remove(indexPath, ec);
     117           0 :             if (ec)
     118           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     119             :                            accountId_,
     120             :                            id_,
     121             :                            indexPath,
     122             :                            ec.message());
     123             :         }
     124             : 
     125         852 :         auto refPath = std::filesystem::path(repoPath / "refs" / "heads" / "main.lock");
     126         426 :         if (std::filesystem::exists(refPath)) {
     127           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked, removing lock {}",
     128             :                          accountId_,
     129             :                          id_,
     130             :                          refPath);
     131           0 :             std::filesystem::remove(refPath, ec);
     132           0 :             if (ec)
     133           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     134             :                            accountId_,
     135             :                            id_,
     136             :                            refPath,
     137             :                            ec.message());
     138             :         }
     139             : 
     140         852 :         auto remotePath = std::filesystem::path(repoPath / "refs" / "remotes");
     141        1483 :         for (const auto& fileIt : std::filesystem::directory_iterator(remotePath, ec)) {
     142         205 :             auto refPath = fileIt.path() / "main.lock";
     143         205 :             if (std::filesystem::exists(refPath, ec)) {
     144           0 :                 JAMI_WARNING("[Account {}] [Conversation {}] Conversation is locked for remote {}, removing lock",
     145             :                              accountId_,
     146             :                              id_,
     147             :                              fileIt.path().filename());
     148           0 :                 std::filesystem::remove(refPath, ec);
     149           0 :                 if (ec)
     150           0 :                     JAMI_ERROR("[Account {}] [Conversation {}] Unable to remove lock {}: {}",
     151             :                                accountId_,
     152             :                                id_,
     153             :                                refPath,
     154             :                                ec.message());
     155             :             }
     156         630 :         }
     157             : 
     158         425 :         auto err = git_repository_state_cleanup(repo.get());
     159         426 :         if (err < 0) {
     160           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to clean up the repository: {}",
     161             :                        accountId_,
     162             :                        id_,
     163             :                        git_error_last()->message);
     164             :         }
     165         426 :     }
     166             : 
     167         426 :     void loadMembers()
     168             :     {
     169             :         try {
     170             :             // read file
     171         838 :             auto file = fileutils::loadFile(membersCache_);
     172             :             // load values
     173          14 :             msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
     174          14 :             std::lock_guard lk {membersMtx_};
     175          14 :             oh.get().convert(members_);
     176         426 :         } catch (const std::exception& e) {
     177         412 :         }
     178         426 :     }
     179             :     // Note: membersMtx_ needs to be locked when calling saveMembers
     180        1496 :     void saveMembers()
     181             :     {
     182        1496 :         std::ofstream file(membersCache_, std::ios::trunc | std::ios::binary);
     183        1496 :         msgpack::pack(file, members_);
     184             : 
     185        1495 :         if (onMembersChanged_) {
     186        1083 :             std::set<std::string> memberUris;
     187       14039 :             for (const auto& member : members_) {
     188       12956 :                 memberUris.emplace(member.uri);
     189             :             }
     190        1083 :             onMembersChanged_(memberUris);
     191        1084 :         }
     192        1496 :     }
     193             : 
     194             :     OnMembersChanged onMembersChanged_ {};
     195             : 
     196             :     // NOTE! We use temporary GitRepository to avoid keeping the file opened
     197             :     // TODO: check why git_remote_fetch() leaves pack-data opened
     198       46601 :     GitRepository repository() const
     199             :     {
     200       93202 :         auto path = fmt::format("{}/{}/conversations/{}", fileutils::get_data_dir().string(), accountId_, id_);
     201       46607 :         git_repository* repo = nullptr;
     202       46607 :         auto err = git_repository_open(&repo, path.c_str());
     203       46609 :         if (err < 0) {
     204           0 :             JAMI_ERROR("Unable to open Git repository: {} ({})", path, git_error_last()->message);
     205           0 :             return nullptr;
     206             :         }
     207       46609 :         return GitRepository(std::move(repo));
     208       46594 :     }
     209             : 
     210         513 :     std::string getDisplayName() const
     211             :     {
     212         513 :         auto shared = account_.lock();
     213         513 :         if (!shared)
     214           0 :             return {};
     215         513 :         auto name = shared->getDisplayName();
     216         513 :         if (name.empty())
     217           0 :             name = deviceId_;
     218         513 :         return std::regex_replace(name, regex_display_name, "");
     219         513 :     }
     220             : 
     221             :     GitSignature signature();
     222             :     bool mergeFastforward(const git_oid* target_oid, int is_unborn);
     223             :     std::string createMergeCommit(git_index* index, const std::string& wanted_ref);
     224             : 
     225             :     bool validCommits(const std::vector<ConversationCommit>& commits) const;
     226             :     bool checkValidUserDiff(const std::string& userDevice,
     227             :                             const std::string& commitId,
     228             :                             const std::string& parentId) const;
     229             :     bool checkVote(const std::string& userDevice, const std::string& commitId, const std::string& parentId) const;
     230             :     bool checkEdit(const std::string& userDevice, const ConversationCommit& commit) const;
     231             :     bool isValidUserAtCommit(const std::string& userDevice,
     232             :                              const std::string& commitId,
     233             :                              const git_buf& sig,
     234             :                              const git_buf& sig_data) const;
     235             :     bool checkInitialCommit(const std::string& userDevice,
     236             :                             const std::string& commitId,
     237             :                             const std::string& commitMsg) const;
     238             :     bool checkValidAdd(const std::string& userDevice,
     239             :                        const std::string& uriMember,
     240             :                        const std::string& commitid,
     241             :                        const std::string& parentId) const;
     242             :     bool checkValidJoins(const std::string& userDevice,
     243             :                          const std::string& uriMember,
     244             :                          const std::string& commitid,
     245             :                          const std::string& parentId) const;
     246             :     bool checkValidRemove(const std::string& userDevice,
     247             :                           const std::string& uriMember,
     248             :                           const std::string& commitid,
     249             :                           const std::string& parentId) const;
     250             :     bool checkValidVoteResolution(const std::string& userDevice,
     251             :                                   const std::string& uriMember,
     252             :                                   const std::string& commitId,
     253             :                                   const std::string& parentId,
     254             :                                   const std::string& voteType) const;
     255             :     bool checkValidProfileUpdate(const std::string& userDevice,
     256             :                                  const std::string& commitid,
     257             :                                  const std::string& parentId) const;
     258             :     bool checkValidMergeCommit(const std::string& mergeId, const std::vector<std::string>& parents) const;
     259             :     std::optional<std::set<std::string_view>> getDeltaPathsFromDiff(const GitDiff& diff) const;
     260             : 
     261             :     bool add(const std::string& path);
     262             :     void addUserDevice();
     263             :     void resetHard();
     264             :     // Verify that the device in the repository is still valid
     265             :     bool validateDevice();
     266             :     std::string commit(const std::string& msg, bool verifyDevice = true);
     267             :     std::string commitMessage(const std::string& msg, bool verifyDevice = true);
     268             :     ConversationMode mode() const;
     269             : 
     270             :     // NOTE! GitDiff needs to be deleted before repo
     271             :     GitDiff diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const;
     272             :     std::string diffStats(const std::string& newId, const std::string& oldId) const;
     273             :     std::string diffStats(const GitDiff& diff) const;
     274             : 
     275             :     std::vector<ConversationCommit> behind(const std::string& from) const;
     276             :     void forEachCommit(PreConditionCb&& preCondition,
     277             :                        std::function<void(ConversationCommit&&)>&& emplaceCb,
     278             :                        PostConditionCb&& postCondition,
     279             :                        const std::string& from = "",
     280             :                        bool logIfNotFound = true) const;
     281             :     std::vector<ConversationCommit> log(const LogOptions& options) const;
     282             : 
     283             :     GitObject fileAtTree(const std::string& path, const GitTree& tree) const;
     284             :     GitObject memberCertificate(std::string_view memberUri, const GitTree& tree) const;
     285             :     // NOTE! GitDiff needs to be deleted before repo
     286             :     GitTree treeAtCommit(git_repository* repo, const std::string& commitId) const;
     287             : 
     288             :     std::vector<std::string> getInitialMembers() const;
     289             : 
     290             :     bool resolveBan(const std::string_view type, const std::string& uri);
     291             :     bool resolveUnban(const std::string_view type, const std::string& uri);
     292             : 
     293             :     std::weak_ptr<JamiAccount> account_;
     294             :     const std::string id_;
     295             :     const std::string accountId_;
     296             :     const std::string userId_;
     297             :     const std::string deviceId_;
     298             :     mutable std::optional<ConversationMode> mode_ {};
     299             : 
     300             :     // Members utils
     301             :     mutable std::mutex membersMtx_ {};
     302             :     std::vector<ConversationMember> members_ {};
     303             : 
     304        1357 :     std::vector<ConversationMember> members() const
     305             :     {
     306        1357 :         std::lock_guard lk(membersMtx_);
     307        2714 :         return members_;
     308        1357 :     }
     309             : 
     310             :     std::filesystem::path conversationDataPath_ {};
     311             :     std::filesystem::path membersCache_ {};
     312             : 
     313         514 :     std::map<std::string, std::vector<DeviceId>> devices(bool ignoreExpired = true) const
     314             :     {
     315         514 :         auto acc = account_.lock();
     316         514 :         auto repo = repository();
     317         514 :         if (!repo or !acc)
     318           0 :             return {};
     319         514 :         std::map<std::string, std::vector<DeviceId>> memberDevices;
     320         514 :         std::string deviceDir = fmt::format("{}devices/", git_repository_workdir(repo.get()));
     321         514 :         std::error_code ec;
     322        2565 :         for (const auto& fileIt : std::filesystem::directory_iterator(deviceDir, ec)) {
     323             :             try {
     324        2046 :                 auto cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(fileIt.path()));
     325        1023 :                 if (!cert)
     326           0 :                     continue;
     327        1023 :                 if (ignoreExpired && cert->getExpiration() < std::chrono::system_clock::now())
     328           1 :                     continue;
     329        1022 :                 auto issuerUid = cert->getIssuerUID();
     330        1022 :                 if (!acc->certStore().getCertificate(issuerUid)) {
     331             :                     // Check that parentCert
     332           1 :                     auto memberFile = fmt::format("{}members/{}.crt", git_repository_workdir(repo.get()), issuerUid);
     333           1 :                     auto adminFile = fmt::format("{}admins/{}.crt", git_repository_workdir(repo.get()), issuerUid);
     334           2 :                     auto parentCert = std::make_shared<dht::crypto::Certificate>(dhtnet::fileutils::loadFile(
     335           2 :                         std::filesystem::is_regular_file(memberFile, ec) ? memberFile : adminFile));
     336           0 :                     if (parentCert && (ignoreExpired || parentCert->getExpiration() < std::chrono::system_clock::now()))
     337           0 :                         acc->certStore().pinCertificate(parentCert,
     338             :                                                         true); // Pin certificate to local store if not already done
     339           2 :                 }
     340        1021 :                 if (!acc->certStore().getCertificate(cert->getPublicKey().getLongId().toString())) {
     341           0 :                     acc->certStore().pinCertificate(cert,
     342             :                                                     true); // Pin certificate to local store if not already done
     343             :                 }
     344        1021 :                 memberDevices[cert->getIssuerUID()].emplace_back(cert->getPublicKey().getLongId());
     345             : 
     346        1025 :             } catch (const std::exception&) {
     347           1 :             }
     348         514 :         }
     349         514 :         return memberDevices;
     350         514 :     }
     351             : 
     352       14012 :     std::optional<ConversationCommit> getCommit(const std::string& commitId, bool logIfNotFound = true) const
     353             :     {
     354       14012 :         LogOptions options;
     355       14011 :         options.from = commitId;
     356       14012 :         options.nbOfCommits = 1;
     357       14012 :         options.logIfNotFound = logIfNotFound;
     358       14012 :         auto commits = log(options);
     359       14012 :         if (commits.empty())
     360        3032 :             return std::nullopt;
     361       10980 :         return std::move(commits[0]);
     362       14011 :     }
     363             : 
     364             :     bool resolveConflicts(git_index* index, const std::string& other_id);
     365             : 
     366        2945 :     std::set<std::string> memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
     367             :     {
     368        2945 :         std::lock_guard lk(membersMtx_);
     369        2945 :         std::set<std::string> ret;
     370       33423 :         for (const auto& member : members_) {
     371       30480 :             if ((filteredRoles.find(member.role) != filteredRoles.end())
     372       30475 :                 or (not filter.empty() and filter == member.uri))
     373        1864 :                 continue;
     374       28613 :             ret.emplace(member.uri);
     375             :         }
     376        5889 :         return ret;
     377        2944 :     }
     378             : 
     379             :     void initMembers();
     380             : 
     381             :     std::optional<std::map<std::string, std::string>> convCommitToMap(const ConversationCommit& commit) const;
     382             : 
     383             :     // Permissions
     384             :     MemberRole updateProfilePermLvl_ {MemberRole::ADMIN};
     385             : 
     386             :     /**
     387             :      * Retrieve the user related to a device using the account's certificate store.
     388             :      * @note deviceToUri_ is used to cache result and avoid always loading the certificate
     389             :      */
     390       34390 :     std::string uriFromDevice(const std::string& deviceId, const std::string& commitId = "") const
     391             :     {
     392             :         // Check if we have the device in cache.
     393       34390 :         std::lock_guard lk(deviceToUriMtx_);
     394       34391 :         auto it = deviceToUri_.find(deviceId);
     395       34396 :         if (it != deviceToUri_.end())
     396       33033 :             return it->second;
     397             : 
     398        1362 :         auto acc = account_.lock();
     399        1362 :         if (!acc)
     400           0 :             return {};
     401             : 
     402        1362 :         auto cert = acc->certStore().getCertificate(deviceId);
     403        1362 :         if (!cert || !cert->issuer) {
     404          22 :             if (!commitId.empty()) {
     405          22 :                 std::string uri = uriFromDeviceAtCommit(deviceId, commitId);
     406          22 :                 if (!uri.empty()) {
     407          21 :                     deviceToUri_.insert({deviceId, uri});
     408          21 :                     return uri;
     409             :                 }
     410          22 :             }
     411             :             // Not pinned, so load certificate from repo
     412           1 :             auto repo = repository();
     413           1 :             if (!repo)
     414           0 :                 return {};
     415           2 :             auto deviceFile = std::filesystem::path(git_repository_workdir(repo.get())) / "devices"
     416           3 :                               / fmt::format("{}.crt", deviceId);
     417           1 :             if (!std::filesystem::is_regular_file(deviceFile))
     418           1 :                 return {};
     419             :             try {
     420           0 :                 cert = std::make_shared<dht::crypto::Certificate>(fileutils::loadFile(deviceFile));
     421           0 :             } catch (const std::exception&) {
     422           0 :                 JAMI_WARNING("Unable to load certificate from {}", deviceFile);
     423           0 :             }
     424           0 :             if (!cert)
     425           0 :                 return {};
     426           2 :         }
     427        1340 :         auto issuerUid = cert->issuer ? cert->issuer->getId().toString() : cert->getIssuerUID();
     428        1340 :         if (issuerUid.empty())
     429           0 :             return {};
     430             : 
     431        1340 :         deviceToUri_.insert({deviceId, issuerUid});
     432        1340 :         return issuerUid;
     433       34391 :     }
     434             :     mutable std::mutex deviceToUriMtx_;
     435             :     mutable std::map<std::string, std::string> deviceToUri_;
     436             : 
     437             :     /**
     438             :      * Retrieve the user related to a device using certificate directly from the repository at a
     439             :      * specific commit.
     440             :      * @note Prefer uriFromDevice() if possible as it uses the cache.
     441             :      */
     442          22 :     std::string uriFromDeviceAtCommit(const std::string& deviceId, const std::string& commitId) const
     443             :     {
     444          22 :         auto repo = repository();
     445          22 :         if (!repo)
     446           0 :             return {};
     447          22 :         auto tree = treeAtCommit(repo.get(), commitId);
     448           0 :         auto deviceFile = fmt::format("devices/{}.crt", deviceId);
     449          22 :         auto blob_device = fileAtTree(deviceFile, tree);
     450          22 :         if (!blob_device) {
     451           4 :             JAMI_ERROR("{} announced but not found", deviceId);
     452           1 :             return {};
     453             :         }
     454          21 :         auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
     455          21 :         return deviceCert.getIssuerUID();
     456          22 :     }
     457             : 
     458             :     /**
     459             :      * Verify that a certificate modification is correct
     460             :      * @param certPath      Where the certificate is saved (relative path)
     461             :      * @param userUri       Account we want for this certificate
     462             :      * @param oldCert       Previous certificate. getId() should return the same id as the new
     463             :      *                      certificate.
     464             :      * @note There is a few exception because JAMS certificates are buggy right now
     465             :      */
     466         378 :     bool verifyCertificate(std::string_view certContent,
     467             :                            const std::string& userUri,
     468             :                            std::string_view oldCert = ""sv) const
     469             :     {
     470         378 :         auto cert = dht::crypto::Certificate(certContent);
     471         378 :         auto isDeviceCertificate = cert.getId().toString() != userUri;
     472         378 :         auto issuerUid = cert.getIssuerUID();
     473         378 :         if (isDeviceCertificate && issuerUid.empty()) {
     474             :             // Err for Jams certificates
     475           0 :             JAMI_ERROR("Empty issuer for {}", cert.getId().toString());
     476             :         }
     477         378 :         if (!oldCert.empty()) {
     478           3 :             auto deviceCert = dht::crypto::Certificate(oldCert);
     479           3 :             if (isDeviceCertificate) {
     480           2 :                 if (issuerUid != deviceCert.getIssuerUID()) {
     481             :                     // NOTE: Here, because JAMS certificate can be incorrectly formatted, there is
     482             :                     // just one valid possibility: passing from an empty issuer to
     483             :                     // the valid issuer.
     484           1 :                     if (issuerUid != userUri) {
     485           4 :                         JAMI_ERROR("Device certificate with a bad issuer {}", cert.getId().toString());
     486           1 :                         return false;
     487             :                     }
     488             :                 }
     489           1 :             } else if (cert.getId().toString() != userUri) {
     490           0 :                 JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     491           0 :                 return false;
     492             :             }
     493           2 :             if (cert.getId() != deviceCert.getId()) {
     494           0 :                 JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     495           0 :                 return false;
     496             :             }
     497           2 :             return true;
     498           3 :         }
     499             : 
     500             :         // If it's a device certificate, we need to verify that the issuer is not modified
     501         375 :         if (isDeviceCertificate) {
     502             :             // Check that issuer is the one we want.
     503             :             // NOTE: Still one case due to incorrectly formatted certificates from JAMS
     504         189 :             if (issuerUid != userUri && !issuerUid.empty()) {
     505           4 :                 JAMI_ERROR("Device certificate with a bad issuer {}", cert.getId().toString());
     506           1 :                 return false;
     507             :             }
     508         186 :         } else if (cert.getId().toString() != userUri) {
     509           0 :             JAMI_ERROR("Certificate with a bad ID {}", cert.getId().toString());
     510           0 :             return false;
     511             :         }
     512             : 
     513         374 :         return true;
     514         378 :     }
     515             : 
     516             :     std::mutex opMtx_; // Mutex for operations
     517             : };
     518             : 
     519             : /////////////////////////////////////////////////////////////////////////////////
     520             : 
     521             : /**
     522             :  * Creates an empty repository
     523             :  * @param path       Path of the new repository
     524             :  * @return The libgit2's managed repository
     525             :  */
     526             : GitRepository
     527         199 : create_empty_repository(const std::string& path)
     528             : {
     529         199 :     git_repository* repo = nullptr;
     530             :     git_repository_init_options opts;
     531         199 :     git_repository_init_options_init(&opts, GIT_REPOSITORY_INIT_OPTIONS_VERSION);
     532         199 :     opts.flags |= GIT_REPOSITORY_INIT_MKPATH;
     533         199 :     opts.initial_head = "main";
     534         199 :     if (git_repository_init_ext(&repo, path.c_str(), &opts) < 0) {
     535           0 :         JAMI_ERROR("Unable to create a git repository in {}", path);
     536             :     }
     537         398 :     return GitRepository(std::move(repo));
     538             : }
     539             : 
     540             : /**
     541             :  * Add all files to index
     542             :  * @param   repo
     543             :  * @return  if operation is successful
     544             :  */
     545             : bool
     546         376 : git_add_all(git_repository* repo)
     547             : {
     548             :     // git add -A
     549         376 :     git_index* index_ptr = nullptr;
     550         376 :     if (git_repository_index(&index_ptr, repo) < 0) {
     551           0 :         JAMI_ERROR("Unable to open repository index");
     552           0 :         return false;
     553             :     }
     554         376 :     GitIndex index {index_ptr};
     555         376 :     git_strarray array {nullptr, 0};
     556         376 :     git_index_add_all(index.get(), &array, 0, nullptr, nullptr);
     557         376 :     git_index_write(index.get());
     558         376 :     git_strarray_dispose(&array);
     559         376 :     return true;
     560         376 : }
     561             : 
     562             : /**
     563             :  * Adds initial files. This adds the certificate of the account in the /admins directory
     564             :  * the device's key in /devices and the CRLs in /CRLs.
     565             :  * @param   repo      The repository
     566             :  * @return  if files were added successfully
     567             :  */
     568             : bool
     569         199 : add_initial_files(GitRepository& repo,
     570             :                   const std::shared_ptr<JamiAccount>& account,
     571             :                   ConversationMode mode,
     572             :                   const std::string& otherMember = "")
     573             : {
     574         199 :     auto deviceId = account->currentDeviceId();
     575         199 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
     576         199 :     auto adminsPath = repoPath / MemberPath::ADMINS;
     577         199 :     auto devicesPath = repoPath / MemberPath::DEVICES;
     578         199 :     auto invitedPath = repoPath / MemberPath::INVITED;
     579         398 :     auto crlsPath = repoPath / "CRLs" / deviceId;
     580             : 
     581         199 :     if (!dhtnet::fileutils::recursive_mkdir(adminsPath, 0700)) {
     582           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", adminsPath);
     583           0 :         return false;
     584             :     }
     585             : 
     586         199 :     auto cert = account->identity().second;
     587         199 :     auto deviceCert = cert->toString(false);
     588         199 :     auto parentCert = cert->issuer;
     589         199 :     if (!parentCert) {
     590           0 :         JAMI_ERROR("Parent cert is null");
     591           0 :         return false;
     592             :     }
     593             : 
     594             :     // /admins
     595         597 :     auto adminPath = adminsPath / fmt::format("{}.crt", parentCert->getId().toString());
     596         199 :     std::ofstream file(adminPath, std::ios::trunc | std::ios::binary);
     597         199 :     if (!file.is_open()) {
     598           0 :         JAMI_ERROR("Unable to write data to {}", adminPath);
     599           0 :         return false;
     600             :     }
     601         199 :     file << parentCert->toString(true);
     602         199 :     file.close();
     603             : 
     604         199 :     if (!dhtnet::fileutils::recursive_mkdir(devicesPath, 0700)) {
     605           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", devicesPath);
     606           0 :         return false;
     607             :     }
     608             : 
     609             :     // /devices
     610         398 :     auto devicePath = devicesPath / fmt::format("{}.crt", deviceId);
     611         199 :     file = std::ofstream(devicePath, std::ios::trunc | std::ios::binary);
     612         199 :     if (!file.is_open()) {
     613           0 :         JAMI_ERROR("Unable to write data to {}", devicePath);
     614           0 :         return false;
     615             :     }
     616         199 :     file << deviceCert;
     617         199 :     file.close();
     618             : 
     619         199 :     if (!dhtnet::fileutils::recursive_mkdir(crlsPath, 0700)) {
     620           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", crlsPath);
     621           0 :         return false;
     622             :     }
     623             : 
     624             :     // /CRLs
     625         199 :     for (const auto& crl : account->identity().second->getRevocationLists()) {
     626           0 :         if (!crl)
     627           0 :             continue;
     628           0 :         auto crlPath = crlsPath / deviceId / (dht::toHex(crl->getNumber()) + ".crl");
     629           0 :         std::ofstream file(crlPath, std::ios::trunc | std::ios::binary);
     630           0 :         if (!file.is_open()) {
     631           0 :             JAMI_ERROR("Unable to write data to {}", crlPath);
     632           0 :             return false;
     633             :         }
     634           0 :         file << crl->toString();
     635           0 :         file.close();
     636         199 :     }
     637             : 
     638             :     // /invited for one to one
     639         199 :     if (mode == ConversationMode::ONE_TO_ONE) {
     640          69 :         if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
     641           0 :             JAMI_ERROR("Error when creating {}.", invitedPath);
     642           0 :             return false;
     643             :         }
     644          69 :         auto invitedMemberPath = invitedPath / otherMember;
     645          69 :         if (std::filesystem::is_regular_file(invitedMemberPath)) {
     646           0 :             JAMI_WARNING("Member {} already present", otherMember);
     647           0 :             return false;
     648             :         }
     649             : 
     650          69 :         std::ofstream file(invitedMemberPath, std::ios::trunc | std::ios::binary);
     651          69 :         if (!file.is_open()) {
     652           0 :             JAMI_ERROR("Unable to write data to {}", invitedMemberPath);
     653           0 :             return false;
     654             :         }
     655          69 :     }
     656             : 
     657         199 :     if (!git_add_all(repo.get())) {
     658           0 :         return false;
     659             :     }
     660             : 
     661         796 :     JAMI_LOG("Initial files added in {}", repoPath);
     662         199 :     return true;
     663         199 : }
     664             : 
     665             : /**
     666             :  * Sign and create the initial commit
     667             :  * @param repo          The Git repository
     668             :  * @param account       The account who signs
     669             :  * @param mode          The mode
     670             :  * @param otherMember   If one to one
     671             :  * @return              The first commit hash or empty if failed
     672             :  */
     673             : std::string
     674         199 : initial_commit(GitRepository& repo,
     675             :                const std::shared_ptr<JamiAccount>& account,
     676             :                ConversationMode mode,
     677             :                const std::string& otherMember = "")
     678             : {
     679         199 :     auto deviceId = std::string(account->currentDeviceId());
     680         199 :     auto name = account->getDisplayName();
     681         199 :     if (name.empty())
     682           0 :         name = deviceId;
     683         199 :     name = std::regex_replace(name, regex_display_name, "");
     684             : 
     685         199 :     git_signature* sig_ptr = nullptr;
     686         199 :     git_index* index_ptr = nullptr;
     687             :     git_oid tree_id, commit_id;
     688         199 :     git_tree* tree_ptr = nullptr;
     689             : 
     690             :     // Sign commit's buffer
     691         199 :     if (git_signature_new(&sig_ptr, name.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
     692           1 :         if (git_signature_new(&sig_ptr, deviceId.c_str(), deviceId.c_str(), std::time(nullptr), 0) < 0) {
     693           0 :             JAMI_ERROR("Unable to create a commit signature.");
     694           0 :             return {};
     695             :         }
     696             :     }
     697         199 :     GitSignature sig {sig_ptr};
     698             : 
     699         199 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
     700           0 :         JAMI_ERROR("Unable to open the repository index");
     701           0 :         return {};
     702             :     }
     703         199 :     GitIndex index {index_ptr};
     704             : 
     705         199 :     if (git_index_write_tree(&tree_id, index.get()) < 0) {
     706           0 :         JAMI_ERROR("Unable to write initial tree from index");
     707           0 :         return {};
     708             :     }
     709             : 
     710         199 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
     711           0 :         JAMI_ERROR("Unable to look up the initial tree");
     712           0 :         return {};
     713             :     }
     714         199 :     GitTree tree {tree_ptr};
     715             : 
     716         199 :     Json::Value json;
     717         199 :     json["mode"] = static_cast<int>(mode);
     718         199 :     if (mode == ConversationMode::ONE_TO_ONE) {
     719          69 :         json["invited"] = otherMember;
     720             :     }
     721         199 :     json["type"] = "initial";
     722             : 
     723         199 :     git_buf to_sign = {};
     724         398 :     if (git_commit_create_buffer(
     725         597 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, json::toString(json).c_str(), tree.get(), 0, nullptr)
     726         199 :         < 0) {
     727           0 :         JAMI_ERROR("Unable to create initial buffer");
     728           0 :         return {};
     729             :     }
     730             : 
     731         199 :     std::string signed_str = base64::encode(account->identity().first->sign((const uint8_t*) to_sign.ptr, to_sign.size));
     732             : 
     733             :     // git commit -S
     734         199 :     if (git_commit_create_with_signature(&commit_id, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
     735           0 :         git_buf_dispose(&to_sign);
     736           0 :         JAMI_ERROR("Unable to sign the initial commit");
     737           0 :         return {};
     738             :     }
     739         199 :     git_buf_dispose(&to_sign);
     740             : 
     741             :     // Move commit to main branch
     742         199 :     git_commit* commit = nullptr;
     743         199 :     if (git_commit_lookup(&commit, repo.get(), &commit_id) == 0) {
     744         199 :         git_reference* ref = nullptr;
     745         199 :         git_branch_create(&ref, repo.get(), "main", commit, true);
     746         199 :         git_commit_free(commit);
     747         199 :         git_reference_free(ref);
     748             :     }
     749             : 
     750         199 :     auto commit_str = git_oid_tostr_s(&commit_id);
     751         199 :     if (commit_str)
     752         199 :         return commit_str;
     753           0 :     return {};
     754         199 : }
     755             : 
     756             : //////////////////////////////////
     757             : 
     758             : GitSignature
     759         513 : ConversationRepository::Impl::signature()
     760             : {
     761         513 :     auto name = getDisplayName();
     762         513 :     if (name.empty()) {
     763           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to create a commit signature: no name set", accountId_, id_);
     764           0 :         return nullptr;
     765             :     }
     766             : 
     767         513 :     git_signature* sig_ptr = nullptr;
     768             :     // Sign commit's buffer
     769         513 :     if (git_signature_new(&sig_ptr, name.c_str(), deviceId_.c_str(), std::time(nullptr), 0) < 0) {
     770             :         // Maybe the display name is invalid (like " ") - try without
     771           1 :         int err = git_signature_new(&sig_ptr, deviceId_.c_str(), deviceId_.c_str(), std::time(nullptr), 0);
     772           1 :         if (err < 0) {
     773           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create a commit signature: {}", accountId_, id_, err);
     774           0 :             return nullptr;
     775             :         }
     776             :     }
     777         513 :     return GitSignature(sig_ptr);
     778         513 : }
     779             : 
     780             : std::string
     781          21 : ConversationRepository::Impl::createMergeCommit(git_index* index, const std::string& wanted_ref)
     782             : {
     783          21 :     if (!validateDevice()) {
     784           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", accountId_, id_);
     785           0 :         return {};
     786             :     }
     787             :     // The merge will occur between current HEAD and wanted_ref
     788          21 :     git_reference* head_ref_ptr = nullptr;
     789          21 :     auto repo = repository();
     790          21 :     if (!repo || git_repository_head(&head_ref_ptr, repo.get()) < 0) {
     791           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD reference", accountId_, id_);
     792           0 :         return {};
     793             :     }
     794          21 :     GitReference head_ref {head_ref_ptr};
     795             : 
     796             :     // Maybe that's a ref, so DWIM it
     797          21 :     git_reference* merge_ref_ptr = nullptr;
     798          21 :     git_reference_dwim(&merge_ref_ptr, repo.get(), wanted_ref.c_str());
     799          21 :     GitReference merge_ref {merge_ref_ptr};
     800             : 
     801          21 :     GitSignature sig {signature()};
     802             : 
     803             :     // Prepare a standard merge commit message
     804          21 :     const char* msg_target = nullptr;
     805          21 :     if (merge_ref) {
     806           0 :         git_branch_name(&msg_target, merge_ref.get());
     807             :     } else {
     808          21 :         msg_target = wanted_ref.c_str();
     809             :     }
     810             : 
     811          21 :     auto commitMsg = fmt::format("Merge {} '{}'", merge_ref ? "branch" : "commit", msg_target);
     812             : 
     813             :     // Set up our parent commits
     814          63 :     GitCommit parents[2];
     815          21 :     git_commit* parent = nullptr;
     816          21 :     if (git_reference_peel((git_object**) &parent, head_ref.get(), GIT_OBJ_COMMIT) < 0) {
     817           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to peel HEAD reference", accountId_, id_);
     818           0 :         return {};
     819             :     }
     820          21 :     parents[0] = GitCommit(parent);
     821             :     git_oid commit_id;
     822          21 :     if (git_oid_fromstr(&commit_id, wanted_ref.c_str()) < 0) {
     823           0 :         return {};
     824             :     }
     825          21 :     git_annotated_commit* annotated_ptr = nullptr;
     826          21 :     if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
     827           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
     828           0 :         return {};
     829             :     }
     830          21 :     GitAnnotatedCommit annotated {annotated_ptr};
     831          21 :     if (git_commit_lookup(&parent, repo.get(), git_annotated_commit_id(annotated.get())) < 0) {
     832           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up commit {}", accountId_, id_, wanted_ref);
     833           0 :         return {};
     834             :     }
     835          21 :     parents[1] = GitCommit(parent);
     836             : 
     837             :     // Prepare our commit tree
     838             :     git_oid tree_oid;
     839          21 :     git_tree* tree_ptr = nullptr;
     840          21 :     if (git_index_write_tree_to(&tree_oid, index, repo.get()) < 0) {
     841           0 :         const git_error* err = giterr_last();
     842           0 :         if (err)
     843           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to write index: {}", accountId_, id_, err->message);
     844           0 :         return {};
     845             :     }
     846          21 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_oid) < 0) {
     847           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up tree", accountId_, id_);
     848           0 :         return {};
     849             :     }
     850          21 :     GitTree tree {tree_ptr};
     851             : 
     852             :     // Commit
     853          21 :     git_buf to_sign = {};
     854             :     // The last argument of git_commit_create_buffer is of type
     855             :     // 'const git_commit **' in all versions of libgit2 except 1.8.0,
     856             :     // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
     857             : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
     858             :     && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
     859          21 :     git_commit* const parents_ptr[2] {parents[0].get(), parents[1].get()};
     860             : #else
     861             :     const git_commit* parents_ptr[2] {parents[0].get(), parents[1].get()};
     862             : #endif
     863          42 :     if (git_commit_create_buffer(
     864          42 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, commitMsg.c_str(), tree.get(), 2, &parents_ptr[0])
     865          21 :         < 0) {
     866           0 :         const git_error* err = giterr_last();
     867           0 :         if (err)
     868           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer: {}",
     869             :                        accountId_,
     870             :                        id_,
     871             :                        err->message);
     872           0 :         return {};
     873             :     }
     874             : 
     875          21 :     auto account = account_.lock();
     876          21 :     if (!account)
     877           0 :         return {};
     878             :     // git commit -S
     879          21 :     auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
     880          21 :     auto signed_buf = account->identity().first->sign(to_sign_vec);
     881          21 :     std::string signed_str = base64::encode(signed_buf);
     882             :     git_oid commit_oid;
     883          21 :     if (git_commit_create_with_signature(&commit_oid, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
     884           0 :         git_buf_dispose(&to_sign);
     885           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
     886           0 :         return {};
     887             :     }
     888          21 :     git_buf_dispose(&to_sign);
     889             : 
     890          21 :     auto commit_str = git_oid_tostr_s(&commit_oid);
     891          21 :     if (commit_str) {
     892          84 :         JAMI_LOG("[Account {}] [Conversation {}] New merge commit added with id: {}", accountId_, id_, commit_str);
     893             :         // Move commit to main branch
     894          21 :         git_reference* ref_ptr = nullptr;
     895          21 :         if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_oid, true, nullptr) < 0) {
     896           0 :             const git_error* err = giterr_last();
     897           0 :             if (err) {
     898           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}",
     899             :                            accountId_,
     900             :                            id_,
     901             :                            err->message);
     902           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, ECOMMIT, err->message);
     903             :             }
     904           0 :             return {};
     905             :         }
     906          21 :         git_reference_free(ref_ptr);
     907             :     }
     908             : 
     909             :     // We're done merging. Clean up the repository state and index
     910          21 :     git_repository_state_cleanup(repo.get());
     911             : 
     912          21 :     git_object* target_ptr = nullptr;
     913          21 :     if (git_object_lookup(&target_ptr, repo.get(), &commit_oid, GIT_OBJ_COMMIT) != 0) {
     914           0 :         const git_error* err = giterr_last();
     915           0 :         if (err)
     916           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}: {}",
     917             :                        accountId_,
     918             :                        id_,
     919             :                        git_oid_tostr_s(&commit_oid),
     920             :                        err->message);
     921           0 :         return {};
     922             :     }
     923          21 :     GitObject target {target_ptr};
     924             : 
     925          21 :     git_reset(repo.get(), target.get(), GIT_RESET_HARD, nullptr);
     926             : 
     927          21 :     return commit_str ? commit_str : "";
     928         105 : }
     929             : 
     930             : bool
     931         889 : ConversationRepository::Impl::mergeFastforward(const git_oid* target_oid, int is_unborn)
     932             : {
     933             :     // Initialize target
     934         889 :     git_reference* target_ref_ptr = nullptr;
     935         889 :     auto repo = repository();
     936         889 :     if (!repo) {
     937           0 :         JAMI_ERROR("[Account {}] [Conversation {}] No repository found", accountId_, id_);
     938           0 :         return false;
     939             :     }
     940         888 :     if (is_unborn) {
     941           0 :         git_reference* head_ref_ptr = nullptr;
     942             :         // HEAD reference is unborn, lookup manually so we don't try to resolve it
     943           0 :         if (git_reference_lookup(&head_ref_ptr, repo.get(), "HEAD") < 0) {
     944           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to look up HEAD ref", accountId_, id_);
     945           0 :             return false;
     946             :         }
     947           0 :         GitReference head_ref {head_ref_ptr};
     948             : 
     949             :         // Grab the reference HEAD should be pointing to
     950           0 :         const auto* symbolic_ref = git_reference_symbolic_target(head_ref.get());
     951             : 
     952             :         // Create our main reference on the target OID
     953           0 :         if (git_reference_create(&target_ref_ptr, repo.get(), symbolic_ref, target_oid, 0, nullptr) < 0) {
     954           0 :             const git_error* err = giterr_last();
     955           0 :             if (err)
     956           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] failed to create main reference: {}",
     957             :                            accountId_,
     958             :                            id_,
     959             :                            err->message);
     960           0 :             return false;
     961             :         }
     962             : 
     963         888 :     } else if (git_repository_head(&target_ref_ptr, repo.get()) < 0) {
     964             :         // HEAD exists, just look up and resolve
     965           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to get HEAD reference", accountId_, id_);
     966           0 :         return false;
     967             :     }
     968         889 :     GitReference target_ref {target_ref_ptr};
     969             : 
     970             :     // Look up the target object
     971         889 :     git_object* target_ptr = nullptr;
     972         889 :     if (git_object_lookup(&target_ptr, repo.get(), target_oid, GIT_OBJ_COMMIT) != 0) {
     973           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to look up OID {}",
     974             :                    accountId_,
     975             :                    id_,
     976             :                    git_oid_tostr_s(target_oid));
     977           0 :         return false;
     978             :     }
     979         889 :     GitObject target {target_ptr};
     980             : 
     981             :     // Checkout the result so the workdir is in the expected state
     982             :     git_checkout_options ff_checkout_options;
     983         889 :     git_checkout_init_options(&ff_checkout_options, GIT_CHECKOUT_OPTIONS_VERSION);
     984         889 :     ff_checkout_options.checkout_strategy = GIT_CHECKOUT_SAFE;
     985         889 :     if (git_checkout_tree(repo.get(), target.get(), &ff_checkout_options) != 0) {
     986           0 :         if (auto err = git_error_last())
     987           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: {}",
     988             :                        accountId_,
     989             :                        id_,
     990             :                        err->message);
     991             :         else
     992           0 :             JAMI_ERROR("[Account {}] [Conversation {}] failed to checkout HEAD reference: unknown error",
     993             :                        accountId_,
     994             :                        id_);
     995           0 :         return false;
     996             :     }
     997             : 
     998             :     // Move the target reference to the target OID
     999             :     git_reference* new_target_ref;
    1000         889 :     if (git_reference_set_target(&new_target_ref, target_ref.get(), target_oid, nullptr) < 0) {
    1001           0 :         JAMI_ERROR("[Account {}] [Conversation {}] failed to move HEAD reference", accountId_, id_);
    1002           0 :         return false;
    1003             :     }
    1004         889 :     git_reference_free(new_target_ref);
    1005             : 
    1006         889 :     return true;
    1007         889 : }
    1008             : 
    1009             : bool
    1010         321 : ConversationRepository::Impl::add(const std::string& path)
    1011             : {
    1012         321 :     auto repo = repository();
    1013         321 :     if (!repo)
    1014           0 :         return false;
    1015         321 :     git_index* index_ptr = nullptr;
    1016         321 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
    1017           0 :         JAMI_ERROR("Unable to open repository index");
    1018           0 :         return false;
    1019             :     }
    1020         321 :     GitIndex index {index_ptr};
    1021         321 :     if (git_index_add_bypath(index.get(), path.c_str()) != 0) {
    1022           0 :         const git_error* err = giterr_last();
    1023           0 :         if (err)
    1024           0 :             JAMI_ERROR("Error when adding file: {}", err->message);
    1025           0 :         return false;
    1026             :     }
    1027         321 :     return git_index_write(index.get()) == 0;
    1028         321 : }
    1029             : 
    1030             : bool
    1031         156 : ConversationRepository::Impl::checkValidUserDiff(const std::string& userDevice,
    1032             :                                                  const std::string& commitId,
    1033             :                                                  const std::string& parentId) const
    1034             : {
    1035             :     // Retrieve tree for recent commit
    1036         156 :     auto repo = repository();
    1037         156 :     if (!repo)
    1038           0 :         return false;
    1039             :     // Here, we check that a file device is modified or not.
    1040         156 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1041         156 :     if (changedFiles.size() == 0)
    1042         148 :         return true;
    1043             : 
    1044             :     // If a certificate is modified (in the changedFiles), it MUST be a certificate from the user
    1045             :     // Retrieve userUri
    1046           8 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1047           8 :     auto userUri = uriFromDevice(userDevice, commitId);
    1048           8 :     if (userUri.empty())
    1049           0 :         return false;
    1050             : 
    1051           8 :     std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
    1052           8 :     std::string adminsFile = fmt::format("admins/{}.crt", userUri);
    1053           0 :     std::string membersFile = fmt::format("members/{}.crt", userUri);
    1054           8 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1055           8 :     if (not treeNew or not treeOld)
    1056           0 :         return false;
    1057          12 :     for (const auto& changedFile : changedFiles) {
    1058           9 :         if (changedFile == adminsFile || changedFile == membersFile) {
    1059             :             // In this case, we should verify it's not added (normal commit, not a member change)
    1060             :             // but only updated
    1061           1 :             auto oldFile = fileAtTree(changedFile, treeOld);
    1062           1 :             if (!oldFile) {
    1063           0 :                 JAMI_ERROR("Invalid file modified: {}", changedFile);
    1064           0 :                 return false;
    1065             :             }
    1066           1 :             auto newFile = fileAtTree(changedFile, treeNew);
    1067           1 :             if (!verifyCertificate(as_view(newFile), userUri, as_view(oldFile))) {
    1068           0 :                 JAMI_ERROR("Invalid certificate {}", changedFile);
    1069           0 :                 return false;
    1070             :             }
    1071           9 :         } else if (changedFile == userDeviceFile) {
    1072             :             // In this case, device is added or modified (certificate expiration)
    1073           4 :             auto oldFile = fileAtTree(changedFile, treeOld);
    1074           4 :             std::string_view oldCert;
    1075           4 :             if (oldFile)
    1076           2 :                 oldCert = as_view(oldFile);
    1077           4 :             auto newFile = fileAtTree(changedFile, treeNew);
    1078           4 :             if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
    1079           4 :                 JAMI_ERROR("Invalid certificate {}", changedFile);
    1080           1 :                 return false;
    1081             :             }
    1082           5 :         } else {
    1083             :             // Invalid file detected
    1084          16 :             JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) mode());
    1085           4 :             return false;
    1086             :         }
    1087             :     }
    1088             : 
    1089           3 :     return true;
    1090         156 : }
    1091             : 
    1092             : bool
    1093           2 : ConversationRepository::Impl::checkEdit(const std::string& userDevice, const ConversationCommit& commit) const
    1094             : {
    1095           2 :     auto repo = repository();
    1096           2 :     if (!repo)
    1097           0 :         return false;
    1098           2 :     auto userUri = uriFromDevice(userDevice, commit.id);
    1099           2 :     if (userUri.empty())
    1100           0 :         return false;
    1101             :     // Check that edited commit is found, for the same author, and editable (plain/text)
    1102           2 :     auto commitMap = convCommitToMap(commit);
    1103           2 :     if (commitMap == std::nullopt) {
    1104           0 :         return false;
    1105             :     }
    1106           4 :     auto editedId = commitMap->at("edit");
    1107           2 :     auto editedCommit = getCommit(editedId);
    1108           2 :     if (editedCommit == std::nullopt) {
    1109           0 :         JAMI_ERROR("Commit {:s} not found", editedId);
    1110           0 :         return false;
    1111             :     }
    1112           2 :     auto editedCommitMap = convCommitToMap(*editedCommit);
    1113           4 :     if (editedCommitMap == std::nullopt or editedCommitMap->at("author").empty()
    1114           4 :         or editedCommitMap->at("author") != commitMap->at("author") or commitMap->at("author") != userUri) {
    1115           0 :         JAMI_ERROR("Edited commit {:s} got a different author ({:s})", editedId, commit.id);
    1116           0 :         return false;
    1117             :     }
    1118           2 :     if (editedCommitMap->at("type") == "text/plain") {
    1119           1 :         return true;
    1120             :     }
    1121           1 :     if (editedCommitMap->at("type") == "application/data-transfer+json") {
    1122           0 :         if (editedCommitMap->find("tid") != editedCommitMap->end())
    1123           0 :             return true;
    1124             :     }
    1125           4 :     JAMI_ERROR("Edited commit {:s} is not valid!", editedId);
    1126           1 :     return false;
    1127           2 : }
    1128             : 
    1129             : bool
    1130           8 : ConversationRepository::Impl::checkVote(const std::string& userDevice,
    1131             :                                         const std::string& commitId,
    1132             :                                         const std::string& parentId) const
    1133             : {
    1134             :     // Check that maximum deviceFile and a vote is added
    1135           8 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1136           8 :     if (changedFiles.size() == 0) {
    1137           2 :         return true;
    1138           6 :     } else if (changedFiles.size() > 2) {
    1139           0 :         return false;
    1140             :     }
    1141             :     // If modified, it's the first commit of a device, we check
    1142             :     // that the file wasn't there previously. And the vote MUST be added
    1143           6 :     std::string deviceFile = "";
    1144           6 :     std::string votedFile = "";
    1145          13 :     for (const auto& changedFile : changedFiles) {
    1146             :         // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1147           8 :         if (changedFile == fmt::format("devices/{}.crt", userDevice)) {
    1148           2 :             deviceFile = changedFile;
    1149           6 :         } else if (changedFile.find("votes") == 0) {
    1150           5 :             votedFile = changedFile;
    1151             :         } else {
    1152             :             // Invalid file detected
    1153           4 :             JAMI_ERROR("Invalid vote file detected: {}", changedFile);
    1154           1 :             return false;
    1155             :         }
    1156             :     }
    1157             : 
    1158           5 :     if (votedFile.empty()) {
    1159           0 :         JAMI_WARNING("No vote detected for commit {}", commitId);
    1160           0 :         return false;
    1161             :     }
    1162             : 
    1163           5 :     auto repo = repository();
    1164           5 :     if (!repo)
    1165           0 :         return false;
    1166           5 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1167           5 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1168           5 :     if (not treeNew or not treeOld)
    1169           0 :         return false;
    1170             : 
    1171           5 :     auto userUri = uriFromDevice(userDevice, commitId);
    1172           5 :     if (userUri.empty())
    1173           0 :         return false;
    1174             :     // Check that voter is admin
    1175           0 :     auto adminFile = fmt::format("admins/{}.crt", userUri);
    1176             : 
    1177           5 :     if (!fileAtTree(adminFile, treeOld)) {
    1178           0 :         JAMI_ERROR("Vote from non admin: {}", userUri);
    1179           0 :         return false;
    1180             :     }
    1181             : 
    1182             :     // Check votedFile path
    1183           5 :     static const std::regex regex_votes("votes.(\\w+).(members|devices|admins|invited).(\\w+).(\\w+)");
    1184           5 :     std::svmatch base_match;
    1185           5 :     if (!std::regex_match(votedFile, base_match, regex_votes) or base_match.size() != 5) {
    1186           0 :         JAMI_WARNING("Invalid votes path: {}", votedFile);
    1187           0 :         return false;
    1188             :     }
    1189             : 
    1190           5 :     std::string_view matchedUri = svsub_match_view(base_match[4]);
    1191           5 :     if (matchedUri != userUri) {
    1192           0 :         JAMI_ERROR("Admin voted for other user: {:s} vs {:s}", userUri, matchedUri);
    1193           0 :         return false;
    1194             :     }
    1195           5 :     std::string_view votedUri = svsub_match_view(base_match[3]);
    1196           5 :     std::string_view type = svsub_match_view(base_match[2]);
    1197           5 :     std::string_view voteType = svsub_match_view(base_match[1]);
    1198           5 :     if (voteType != "ban" && voteType != "unban") {
    1199           0 :         JAMI_ERROR("Unrecognized vote {:s}", voteType);
    1200           0 :         return false;
    1201             :     }
    1202             : 
    1203             :     // Check that vote file is empty and wasn't modified
    1204           5 :     if (fileAtTree(votedFile, treeOld)) {
    1205           0 :         JAMI_ERROR("Invalid voted file modified: {:s}", votedFile);
    1206           0 :         return false;
    1207             :     }
    1208           5 :     auto vote = fileAtTree(votedFile, treeNew);
    1209           5 :     if (!vote) {
    1210           0 :         JAMI_ERROR("No vote file found for: {:s}", userUri);
    1211           0 :         return false;
    1212             :     }
    1213           5 :     auto voteContent = as_view(vote);
    1214           5 :     if (!voteContent.empty()) {
    1215           0 :         JAMI_ERROR("Vote file not empty: {:s}", votedFile);
    1216           0 :         return false;
    1217             :     }
    1218             : 
    1219             :     // Check that peer voted is only other device or other member
    1220           5 :     if (type != "devices") {
    1221           5 :         if (votedUri == userUri) {
    1222           0 :             JAMI_ERROR("Detected vote for self: {:s}", votedUri);
    1223           0 :             return false;
    1224             :         }
    1225           5 :         if (voteType == "ban") {
    1226             :             // file in members or admin or invited
    1227           0 :             auto invitedFile = fmt::format("invited/{}", votedUri);
    1228           5 :             if (!memberCertificate(votedUri, treeOld) && !fileAtTree(invitedFile, treeOld)) {
    1229           0 :                 JAMI_ERROR("No member file found for vote: {:s}", votedUri);
    1230           0 :                 return false;
    1231             :             }
    1232           5 :         }
    1233             :     } else {
    1234             :         // Check not current device
    1235           0 :         if (votedUri == userDevice) {
    1236           0 :             JAMI_ERROR("Detected vote for self: {:s}", votedUri);
    1237           0 :             return false;
    1238             :         }
    1239             :         // File in devices
    1240           0 :         deviceFile = fmt::format("devices/{}.crt", votedUri);
    1241           0 :         if (!fileAtTree(deviceFile, treeOld)) {
    1242           0 :             JAMI_ERROR("No device file found for vote: {:s}", votedUri);
    1243           0 :             return false;
    1244             :         }
    1245             :     }
    1246             : 
    1247           5 :     return true;
    1248           8 : }
    1249             : 
    1250             : bool
    1251         754 : ConversationRepository::Impl::checkValidAdd(const std::string& userDevice,
    1252             :                                             const std::string& uriMember,
    1253             :                                             const std::string& commitId,
    1254             :                                             const std::string& parentId) const
    1255             : {
    1256         754 :     auto repo = repository();
    1257         754 :     if (not repo)
    1258           0 :         return false;
    1259             : 
    1260             :     // std::string repoPath = git_repository_workdir(repo.get());
    1261         754 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    1262           1 :         auto initialMembers = getInitialMembers();
    1263           1 :         auto it = std::find(initialMembers.begin(), initialMembers.end(), uriMember);
    1264           1 :         if (it == initialMembers.end()) {
    1265           4 :             JAMI_ERROR("Invalid add in one to one conversation: {}", uriMember);
    1266           1 :             return false;
    1267             :         }
    1268           1 :     }
    1269             : 
    1270         753 :     auto userUri = uriFromDevice(userDevice, commitId);
    1271         752 :     if (userUri.empty())
    1272           0 :         return false;
    1273             : 
    1274             :     // Check that only /invited/uri.crt is added & deviceFile & CRLs
    1275         752 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1276         753 :     if (changedFiles.size() == 0) {
    1277           0 :         return false;
    1278         753 :     } else if (changedFiles.size() > 3) {
    1279           0 :         return false;
    1280             :     }
    1281             : 
    1282             :     // Check that user added is not sender
    1283         753 :     if (userUri == uriMember) {
    1284           0 :         JAMI_ERROR("Member tried to add self: {}", userUri);
    1285           0 :         return false;
    1286             :     }
    1287             : 
    1288             :     // If modified, it's the first commit of a device, we check
    1289             :     // that the file wasn't there previously. And the member MUST be added
    1290             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1291         753 :     std::string deviceFile = "";
    1292         753 :     std::string invitedFile = "";
    1293        1506 :     std::string crlFile = std::string("CRLs/") + userUri;
    1294        1505 :     for (const auto& changedFile : changedFiles) {
    1295         754 :         if (changedFile == std::string("devices/") + userDevice + ".crt") {
    1296           1 :             deviceFile = changedFile;
    1297         753 :         } else if (changedFile == std::string("invited/") + uriMember) {
    1298         751 :             invitedFile = changedFile;
    1299           2 :         } else if (changedFile == crlFile) {
    1300             :             // Nothing to do
    1301             :         } else {
    1302             :             // Invalid file detected
    1303           8 :             JAMI_ERROR("Invalid add file detected: {}", changedFile);
    1304           2 :             return false;
    1305             :         }
    1306             :     }
    1307             : 
    1308         751 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1309         751 :     if (not treeOld)
    1310           0 :         return false;
    1311             : 
    1312         751 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1313         751 :     auto blob_invite = fileAtTree(invitedFile, treeNew);
    1314         751 :     if (!blob_invite) {
    1315           0 :         JAMI_ERROR("Invitation not found for commit {}", commitId);
    1316           0 :         return false;
    1317             :     }
    1318             : 
    1319         751 :     auto invitation = as_view(blob_invite);
    1320         751 :     if (!invitation.empty()) {
    1321           0 :         JAMI_ERROR("Invitation not empty for commit {}", commitId);
    1322           0 :         return false;
    1323             :     }
    1324             : 
    1325             :     // Check that user not in /banned
    1326             :     std::string bannedFile = fmt::format("{}/{}/{}.crt",
    1327        1502 :                                          MemberPath::BANNED.string(),
    1328         751 :                                          MemberPath::MEMBERS.string(),
    1329         751 :                                          uriMember);
    1330         751 :     if (fileAtTree(bannedFile, treeOld)) {
    1331           0 :         JAMI_ERROR("Tried to add banned member: {}", bannedFile);
    1332           0 :         return false;
    1333             :     }
    1334             : 
    1335         751 :     return true;
    1336         754 : }
    1337             : 
    1338             : bool
    1339         791 : ConversationRepository::Impl::checkValidJoins(const std::string& userDevice,
    1340             :                                               const std::string& uriMember,
    1341             :                                               const std::string& commitId,
    1342             :                                               const std::string& parentId) const
    1343             : {
    1344             :     // Check no other files changed
    1345        1582 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1346         791 :     auto invitedFile = fmt::format("invited/{}", uriMember);
    1347         791 :     auto membersFile = fmt::format("members/{}.crt", uriMember);
    1348           0 :     auto deviceFile = fmt::format("devices/{}.crt", userDevice);
    1349             : 
    1350        3158 :     for (auto& file : changedFiles) {
    1351        2367 :         if (file != invitedFile && file != membersFile && file != deviceFile) {
    1352           0 :             JAMI_ERROR("Unwanted file {} found", file);
    1353           0 :             return false;
    1354             :         }
    1355             :     }
    1356             : 
    1357             :     // Retrieve tree for commits
    1358         791 :     auto repo = repository();
    1359         791 :     assert(repo);
    1360         791 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1361         790 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1362         791 :     if (not treeNew or not treeOld)
    1363           0 :         return false;
    1364             : 
    1365             :     // Check /invited
    1366         791 :     if (fileAtTree(invitedFile, treeNew)) {
    1367           8 :         JAMI_ERROR("{} invited not removed", uriMember);
    1368           2 :         return false;
    1369             :     }
    1370         789 :     if (!fileAtTree(invitedFile, treeOld)) {
    1371           4 :         JAMI_ERROR("{} invited not found", uriMember);
    1372           1 :         return false;
    1373             :     }
    1374             : 
    1375             :     // Check /members added
    1376         788 :     if (!fileAtTree(membersFile, treeNew)) {
    1377           0 :         JAMI_ERROR("{} members not found", uriMember);
    1378           0 :         return false;
    1379             :     }
    1380         788 :     if (fileAtTree(membersFile, treeOld)) {
    1381           0 :         JAMI_ERROR("{} members found too soon", uriMember);
    1382           0 :         return false;
    1383             :     }
    1384             : 
    1385             :     // Check /devices added
    1386         788 :     if (!fileAtTree(deviceFile, treeNew)) {
    1387           0 :         JAMI_ERROR("{} devices not found", uriMember);
    1388           0 :         return false;
    1389             :     }
    1390             : 
    1391             :     // Check certificate
    1392         788 :     auto blob_device = fileAtTree(deviceFile, treeNew);
    1393         788 :     if (!blob_device) {
    1394           0 :         JAMI_ERROR("{} announced but not found", deviceFile);
    1395           0 :         return false;
    1396             :     }
    1397         788 :     auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1398         788 :     auto blob_member = fileAtTree(membersFile, treeNew);
    1399         788 :     if (!blob_member) {
    1400           0 :         JAMI_ERROR("{} announced but not found", userDevice);
    1401           0 :         return false;
    1402             :     }
    1403         788 :     auto memberCert = dht::crypto::Certificate(as_view(blob_member));
    1404         788 :     if (memberCert.getId().toString() != deviceCert.getIssuerUID() || deviceCert.getIssuerUID() != uriMember) {
    1405           0 :         JAMI_ERROR("Incorrect device certificate {} for user {}", userDevice, uriMember);
    1406           0 :         return false;
    1407             :     }
    1408             : 
    1409         788 :     return true;
    1410         791 : }
    1411             : 
    1412             : bool
    1413           1 : ConversationRepository::Impl::checkValidRemove(const std::string& userDevice,
    1414             :                                                const std::string& uriMember,
    1415             :                                                const std::string& commitId,
    1416             :                                                const std::string& parentId) const
    1417             : {
    1418             :     // Retrieve tree for recent commit
    1419           1 :     auto repo = repository();
    1420           1 :     if (!repo)
    1421           0 :         return false;
    1422           1 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1423           1 :     if (not treeOld)
    1424           0 :         return false;
    1425             : 
    1426           2 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1427             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1428           1 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1429           1 :     std::string adminFile = fmt::format("admins/{}.crt", uriMember);
    1430           1 :     std::string memberFile = fmt::format("members/{}.crt", uriMember);
    1431           1 :     std::string crlFile = fmt::format("CRLs/{}", uriMember);
    1432           0 :     std::string invitedFile = fmt::format("invited/{}", uriMember);
    1433           1 :     std::vector<std::string> devicesRemoved;
    1434             : 
    1435             :     // Check that no weird file is added nor removed
    1436           1 :     static const std::regex regex_devices("devices.(\\w+)\\.crt");
    1437           1 :     std::smatch base_match;
    1438           3 :     for (const auto& f : changedFiles) {
    1439           2 :         if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile || f == invitedFile) {
    1440             :             // Ignore
    1441           2 :             continue;
    1442           0 :         } else if (std::regex_match(f, base_match, regex_devices)) {
    1443           0 :             if (base_match.size() == 2)
    1444           0 :                 devicesRemoved.emplace_back(base_match[1]);
    1445             :         } else {
    1446           0 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1447           0 :             return false;
    1448             :         }
    1449             :     }
    1450             : 
    1451             :     // Check that removed devices are for removed member (or directly uriMember)
    1452           1 :     for (const auto& deviceUri : devicesRemoved) {
    1453           0 :         deviceFile = fmt::format("devices/{}.crt", deviceUri);
    1454           0 :         auto blob_device = fileAtTree(deviceFile, treeOld);
    1455           0 :         if (!blob_device) {
    1456           0 :             JAMI_ERROR("Device not found added ({})", deviceFile);
    1457           0 :             return false;
    1458             :         }
    1459           0 :         auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1460           0 :         auto userUri = deviceCert.getIssuerUID();
    1461             : 
    1462           0 :         if (uriMember != userUri and uriMember != deviceUri /* If device is removed */) {
    1463           0 :             JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
    1464           0 :             return false;
    1465             :         }
    1466           0 :     }
    1467             : 
    1468           1 :     return true;
    1469           1 : }
    1470             : 
    1471             : bool
    1472          10 : ConversationRepository::Impl::checkValidVoteResolution(const std::string& userDevice,
    1473             :                                                        const std::string& uriMember,
    1474             :                                                        const std::string& commitId,
    1475             :                                                        const std::string& parentId,
    1476             :                                                        const std::string& voteType) const
    1477             : {
    1478             :     // Retrieve tree for recent commit
    1479          10 :     auto repo = repository();
    1480          10 :     if (!repo)
    1481           0 :         return false;
    1482          10 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1483          10 :     if (not treeOld)
    1484           0 :         return false;
    1485             : 
    1486          20 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1487             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1488          10 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1489          10 :     std::string adminFile = fmt::format("admins/{}.crt", uriMember);
    1490          10 :     std::string memberFile = fmt::format("members/{}.crt", uriMember);
    1491          10 :     std::string crlFile = fmt::format("CRLs/{}", uriMember);
    1492           0 :     std::string invitedFile = fmt::format("invited/{}", uriMember);
    1493          10 :     std::vector<std::string> voters;
    1494          10 :     std::vector<std::string> devicesRemoved;
    1495          10 :     std::vector<std::string> bannedFiles;
    1496             :     // Check that no weird file is added nor removed
    1497             : 
    1498          20 :     const std::regex regex_votes("votes." + voteType + ".(members|devices|admins|invited).(\\w+).(\\w+)");
    1499          10 :     static const std::regex regex_devices("devices.(\\w+)\\.crt");
    1500          10 :     static const std::regex regex_banned("banned.(members|devices|admins).(\\w+)\\.crt");
    1501          10 :     static const std::regex regex_banned_invited("banned.(invited).(\\w+)");
    1502          10 :     std::smatch base_match;
    1503          25 :     for (const auto& f : changedFiles) {
    1504          17 :         if (f == deviceFile || f == adminFile || f == memberFile || f == crlFile || f == invitedFile) {
    1505             :             // Ignore
    1506           5 :             continue;
    1507          12 :         } else if (std::regex_match(f, base_match, regex_votes)) {
    1508           5 :             if (base_match.size() != 4 or base_match[2] != uriMember) {
    1509           0 :                 JAMI_ERROR("Invalid vote file detected: {}", f);
    1510           2 :                 return false;
    1511             :             }
    1512           5 :             voters.emplace_back(base_match[3]);
    1513             :             // Check that votes were not added here
    1514           5 :             if (!fileAtTree(f, treeOld)) {
    1515           0 :                 JAMI_ERROR("invalid vote added ({})", f);
    1516           0 :                 return false;
    1517             :             }
    1518           7 :         } else if (std::regex_match(f, base_match, regex_devices)) {
    1519           0 :             if (base_match.size() == 2)
    1520           0 :                 devicesRemoved.emplace_back(base_match[1]);
    1521           7 :         } else if (std::regex_match(f, base_match, regex_banned)
    1522           7 :                    || std::regex_match(f, base_match, regex_banned_invited)) {
    1523           5 :             bannedFiles.emplace_back(f);
    1524           5 :             if (base_match.size() != 3 or base_match[2] != uriMember) {
    1525           0 :                 JAMI_ERROR("Invalid banned file detected : {}", f);
    1526           0 :                 return false;
    1527             :             }
    1528             :         } else {
    1529           8 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1530           2 :             return false;
    1531             :         }
    1532             :     }
    1533             : 
    1534             :     // Check that removed devices are for removed member (or directly uriMember)
    1535           8 :     for (const auto& deviceUri : devicesRemoved) {
    1536           0 :         deviceFile = fmt::format("devices/{}.crt", deviceUri);
    1537           0 :         if (voteType == "ban") {
    1538             :             // If we ban a device, it should be there before
    1539           0 :             if (!fileAtTree(deviceFile, treeOld)) {
    1540           0 :                 JAMI_ERROR("Device not found added ({})", deviceFile);
    1541           0 :                 return false;
    1542             :             }
    1543           0 :         } else if (voteType == "unban") {
    1544             :             // If we unban a device, it should not be there before
    1545           0 :             if (fileAtTree(deviceFile, treeOld)) {
    1546           0 :                 JAMI_ERROR("Device not found added ({})", deviceFile);
    1547           0 :                 return false;
    1548             :             }
    1549             :         }
    1550           0 :         if (uriMember != uriFromDevice(deviceUri) and uriMember != deviceUri /* If device is removed */) {
    1551           0 :             JAMI_ERROR("Device removed but not for removed user ({})", deviceFile);
    1552           0 :             return false;
    1553             :         }
    1554             :     }
    1555             : 
    1556           8 :     auto userUri = uriFromDevice(userDevice, commitId);
    1557           8 :     if (userUri.empty())
    1558           0 :         return false;
    1559             : 
    1560             :     // Check that voters are admins
    1561           8 :     adminFile = fmt::format("admins/{}.crt", userUri);
    1562           8 :     if (!fileAtTree(adminFile, treeOld)) {
    1563           4 :         JAMI_ERROR("admin file ({}) not found", adminFile);
    1564           1 :         return false;
    1565             :     }
    1566             : 
    1567             :     // If not for self check that vote is valid and not added
    1568           7 :     auto nbAdmins = 0;
    1569           7 :     auto nbVotes = 0;
    1570           7 :     std::string repoPath = git_repository_workdir(repo.get());
    1571          14 :     for (const auto& certificate : dhtnet::fileutils::readDirectory(repoPath + "admins")) {
    1572           7 :         if (certificate.find(".crt") == std::string::npos) {
    1573           0 :             JAMI_WARNING("Incorrect file found: {}", certificate);
    1574           0 :             continue;
    1575           0 :         }
    1576           7 :         nbAdmins += 1;
    1577          14 :         auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
    1578           7 :         if (std::find(voters.begin(), voters.end(), adminUri) != voters.end()) {
    1579           5 :             nbVotes += 1;
    1580             :         }
    1581          14 :     }
    1582             : 
    1583           7 :     if (nbAdmins == 0 or (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) < .5) {
    1584           8 :         JAMI_ERROR("Incomplete vote detected (commit: {})", commitId);
    1585           2 :         return false;
    1586             :     }
    1587             : 
    1588             :     // If not for self check that member or device certificate is moved to banned/
    1589           5 :     return !bannedFiles.empty();
    1590          10 : }
    1591             : 
    1592             : bool
    1593           7 : ConversationRepository::Impl::checkValidProfileUpdate(const std::string& userDevice,
    1594             :                                                       const std::string& commitId,
    1595             :                                                       const std::string& parentId) const
    1596             : {
    1597             :     // Retrieve tree for recent commit
    1598           7 :     auto repo = repository();
    1599           7 :     if (!repo)
    1600           0 :         return false;
    1601           7 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1602           7 :     auto treeOld = treeAtCommit(repo.get(), parentId);
    1603           7 :     if (not treeNew or not treeOld)
    1604           0 :         return false;
    1605             : 
    1606           7 :     auto userUri = uriFromDevice(userDevice, commitId);
    1607           7 :     if (userUri.empty())
    1608           0 :         return false;
    1609             : 
    1610             :     // Check if profile is changed by an user with correct privilege
    1611           7 :     auto valid = false;
    1612           7 :     if (updateProfilePermLvl_ == MemberRole::ADMIN) {
    1613           0 :         std::string adminFile = fmt::format("admins/{}.crt", userUri);
    1614           7 :         auto adminCert = fileAtTree(adminFile, treeNew);
    1615           7 :         valid |= adminCert != nullptr;
    1616           7 :     }
    1617           7 :     if (updateProfilePermLvl_ >= MemberRole::MEMBER) {
    1618           0 :         std::string memberFile = fmt::format("members/{}.crt", userUri);
    1619           0 :         auto memberCert = fileAtTree(memberFile, treeNew);
    1620           0 :         valid |= memberCert != nullptr;
    1621           0 :     }
    1622             : 
    1623           7 :     if (!valid) {
    1624           4 :         JAMI_ERROR("Profile changed from unauthorized user: {} ({})", userDevice, userUri);
    1625           1 :         return false;
    1626             :     }
    1627             : 
    1628          12 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, parentId));
    1629             :     // Check that no weird file is added nor removed
    1630           0 :     std::string userDeviceFile = fmt::format("devices/{}.crt", userDevice);
    1631          12 :     for (const auto& f : changedFiles) {
    1632           7 :         if (f == "profile.vcf") {
    1633             :             // Ignore
    1634           2 :         } else if (f == userDeviceFile) {
    1635             :             // In this case, device is added or modified (certificate expiration)
    1636           1 :             auto oldFile = fileAtTree(f, treeOld);
    1637           1 :             std::string_view oldCert;
    1638           1 :             if (oldFile)
    1639           0 :                 oldCert = as_view(oldFile);
    1640           1 :             auto newFile = fileAtTree(f, treeNew);
    1641           1 :             if (!verifyCertificate(as_view(newFile), userUri, oldCert)) {
    1642           0 :                 JAMI_ERROR("Invalid certificate {}", f);
    1643           0 :                 return false;
    1644             :             }
    1645           1 :         } else {
    1646           4 :             JAMI_ERROR("Unwanted changed file detected: {}", f);
    1647           1 :             return false;
    1648             :         }
    1649             :     }
    1650           5 :     return true;
    1651           7 : }
    1652             : 
    1653             : /**
    1654             :  * @brief Get the deltas from a git diff
    1655             :  * @param diff The diff object to extract deltas from
    1656             :  * @return The set of git_diff deltas extracted from the diff
    1657             :  */
    1658             : std::optional<std::set<std::string_view>>
    1659          18 : ConversationRepository::Impl::getDeltaPathsFromDiff(const GitDiff& diff) const
    1660             : {
    1661          18 :     std::set<std::string_view> deltas_set = {};
    1662          64 :     for (size_t delta_idx = 0, delta_count = git_diff_num_deltas(diff.get()); delta_idx < delta_count; delta_idx++) {
    1663          46 :         const git_diff_delta* delta = git_diff_get_delta(diff.get(), delta_idx);
    1664          46 :         if (!delta) {
    1665           0 :             JAMI_LOG("[Account {}] [Conversation {}] Index of delta out of range!", accountId_, id_);
    1666           0 :             return std::nullopt;
    1667             :         }
    1668             : 
    1669          46 :         deltas_set.emplace(std::string_view(delta->old_file.path));
    1670          46 :         deltas_set.emplace(std::string_view(delta->new_file.path));
    1671             :     }
    1672          18 :     return deltas_set;
    1673          18 : }
    1674             : 
    1675             : /**
    1676             :  * @brief Validate the merge commit by ensuring the absence of invalid files
    1677             :  * @param mergeId The id of the merge commit
    1678             :  * @param parents The two commit IDs of parent's of the merge commit
    1679             :  * @return bool Whether or not invalid files were found in the merge commit
    1680             :  */
    1681             : bool
    1682           9 : ConversationRepository::Impl::checkValidMergeCommit(const std::string& mergeId,
    1683             :                                                     const std::vector<std::string>& parents) const
    1684             : {
    1685             :     // Get the repository associated with this implementation
    1686           9 :     auto repo = repository();
    1687           9 :     if (!repo)
    1688           0 :         return false;
    1689             : 
    1690             :     // Check for exactly two parents
    1691           9 :     if (static_cast<int>(parents.size()) != 2)
    1692           0 :         return false;
    1693             : 
    1694             :     // Get the tree of the merge commit
    1695           9 :     GitTree merge_commit_tree = treeAtCommit(repo.get(), mergeId);
    1696             : 
    1697             :     // Get the diff of the merge commit and the first parent
    1698           9 :     GitTree first_tree = treeAtCommit(repo.get(), parents[0]);
    1699           9 :     git_diff* diff_merge_tree_to_first_tree = nullptr;
    1700           9 :     if (git_diff_tree_to_tree(&diff_merge_tree_to_first_tree,
    1701             :                               repo.get(),
    1702             :                               first_tree.get(),
    1703             :                               merge_commit_tree.get(),
    1704             :                               nullptr)
    1705           9 :         < 0) {
    1706           0 :         const git_error* err = giterr_last();
    1707           0 :         if (err)
    1708           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Failed to git diff of merge and first parent "
    1709             :                        "failed: {}",
    1710             :                        accountId_,
    1711             :                        id_,
    1712             :                        err->message);
    1713           0 :         return false;
    1714             :     }
    1715           9 :     GitDiff first_diff {diff_merge_tree_to_first_tree};
    1716             : 
    1717             :     // Get the diff of the merge commit and the second parent
    1718           9 :     GitTree second_tree = treeAtCommit(repo.get(), parents[1]);
    1719           9 :     git_diff* diff_merge_tree_to_second_tree = nullptr;
    1720           9 :     if (git_diff_tree_to_tree(&diff_merge_tree_to_second_tree,
    1721             :                               repo.get(),
    1722             :                               second_tree.get(),
    1723             :                               merge_commit_tree.get(),
    1724             :                               nullptr)
    1725           9 :         < 0) {
    1726           0 :         const git_error* err = giterr_last();
    1727           0 :         if (err)
    1728           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Failed to git diff of merge and second parent "
    1729             :                        "failed: {}",
    1730             :                        accountId_,
    1731             :                        id_,
    1732             :                        err->message);
    1733           0 :         return false;
    1734             :     }
    1735           9 :     GitDiff second_diff {diff_merge_tree_to_second_tree};
    1736             : 
    1737             :     // Get the deltas of the first parent's commit
    1738           9 :     auto first_parent_deltas_set = getDeltaPathsFromDiff(first_diff);
    1739             :     // Get the deltas of the second parent's commit
    1740           9 :     auto second_parent_deltas_set = getDeltaPathsFromDiff(second_diff);
    1741           9 :     if (first_parent_deltas_set == std::nullopt || second_parent_deltas_set == std::nullopt) {
    1742           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get deltas from diffs for merge commit {}",
    1743             :                    accountId_,
    1744             :                    id_,
    1745             :                    mergeId);
    1746           0 :         return false;
    1747             :     }
    1748             :     // Get the intersection of the deltas of both parents
    1749           9 :     std::set<std::string_view> parent_deltas_intersection_set = {};
    1750           9 :     std::set_intersection(first_parent_deltas_set->begin(),
    1751             :                           first_parent_deltas_set->end(),
    1752             :                           second_parent_deltas_set->begin(),
    1753             :                           second_parent_deltas_set->end(),
    1754             :                           std::inserter(parent_deltas_intersection_set, parent_deltas_intersection_set.begin()));
    1755             : 
    1756             :     // The intersection of the set of diffs of both the parents of the merge commit should be be the
    1757             :     // empty set (i.e. no deltas in the intersection vector). This ensures that no malicious files
    1758             :     // have been added into the merge commit itself.
    1759           9 :     if (not parent_deltas_intersection_set.empty()) {
    1760           1 :         return false;
    1761             :     }
    1762           8 :     return true;
    1763           9 : }
    1764             : 
    1765             : bool
    1766        1720 : ConversationRepository::Impl::isValidUserAtCommit(const std::string& userDevice,
    1767             :                                                   const std::string& commitId,
    1768             :                                                   const git_buf& sig,
    1769             :                                                   const git_buf& sig_data) const
    1770             : {
    1771        1720 :     auto acc = account_.lock();
    1772        1720 :     if (!acc)
    1773           0 :         return false;
    1774        1720 :     auto cert = acc->certStore().getCertificate(userDevice);
    1775        1720 :     auto hasPinnedCert = cert and cert->issuer;
    1776        1720 :     auto repo = repository();
    1777        1720 :     if (not repo)
    1778           0 :         return false;
    1779             : 
    1780             :     // Retrieve tree for commit
    1781        1720 :     auto tree = treeAtCommit(repo.get(), commitId);
    1782        1720 :     if (not tree)
    1783           0 :         return false;
    1784             : 
    1785             :     // Check that /devices/userDevice.crt exists
    1786           0 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1787        1720 :     auto blob_device = fileAtTree(deviceFile, tree);
    1788        1720 :     if (!blob_device) {
    1789           4 :         JAMI_ERROR("{} announced but not found", deviceFile);
    1790           1 :         return false;
    1791             :     }
    1792        1719 :     auto deviceCert = dht::crypto::Certificate(as_view(blob_device));
    1793        1719 :     auto userUri = deviceCert.getIssuerUID();
    1794        1719 :     if (userUri.empty()) {
    1795           0 :         JAMI_ERROR("{} got no issuer UID", deviceFile);
    1796           0 :         if (not hasPinnedCert) {
    1797           0 :             return false;
    1798             :         } else {
    1799             :             // HACK: JAMS device's certificate does not contains any issuer
    1800             :             // So, getIssuerUID() will be empty here, so there is no way
    1801             :             // to get the userURI from this certificate.
    1802             :             // Uses pinned certificate if one.
    1803           0 :             userUri = cert->issuer->getId().toString();
    1804             :         }
    1805             :     }
    1806             : 
    1807             :     // Check that /(members|admins)/userUri.crt exists
    1808        1719 :     auto blob_parent = memberCertificate(userUri, tree);
    1809        1719 :     if (not blob_parent) {
    1810           0 :         JAMI_ERROR("Certificate not found for {}", userUri);
    1811           0 :         return false;
    1812             :     }
    1813             : 
    1814             :     // Check that certificates were still valid
    1815        1719 :     auto parentCert = dht::crypto::Certificate(as_view(blob_parent));
    1816             : 
    1817             :     git_oid oid;
    1818        1719 :     git_commit* commit_ptr = nullptr;
    1819        1719 :     if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    1820           0 :         JAMI_WARNING("Failed to look up commit {}", commitId);
    1821           0 :         return false;
    1822             :     }
    1823        1719 :     GitCommit commit {commit_ptr};
    1824             : 
    1825        1719 :     auto commitTime = std::chrono::system_clock::from_time_t(git_commit_time(commit.get()));
    1826        1719 :     if (deviceCert.getExpiration() < commitTime) {
    1827           0 :         JAMI_ERROR("Certificate {} expired", deviceCert.getId().toString());
    1828           0 :         return false;
    1829             :     }
    1830        1719 :     if (parentCert.getExpiration() < commitTime) {
    1831           0 :         JAMI_ERROR("Certificate {} expired", parentCert.getId().toString());
    1832           0 :         return false;
    1833             :     }
    1834             : 
    1835             :     //  Verify the signature (git verify-commit)
    1836        1719 :     auto pk = base64::decode(std::string_view(sig.ptr, sig.size));
    1837        1719 :     bool valid_signature = deviceCert.getPublicKey().checkSignature(reinterpret_cast<const uint8_t*>(sig_data.ptr),
    1838        1718 :                                                                     sig_data.size,
    1839        1719 :                                                                     pk.data(),
    1840             :                                                                     pk.size());
    1841             : 
    1842        1719 :     if (!valid_signature) {
    1843           4 :         JAMI_WARNING("Commit {} not signed by device {}.", git_oid_tostr_s(&oid), userDevice);
    1844           1 :         return false;
    1845             :     }
    1846             : 
    1847        1718 :     auto res = parentCert.getId().toString() == userUri;
    1848        1718 :     if (res && not hasPinnedCert) {
    1849          72 :         acc->certStore().pinCertificate(std::move(deviceCert));
    1850          72 :         acc->certStore().pinCertificate(std::move(parentCert));
    1851             :     }
    1852        1718 :     return res;
    1853        1720 : }
    1854             : 
    1855             : bool
    1856         187 : ConversationRepository::Impl::checkInitialCommit(const std::string& userDevice,
    1857             :                                                  const std::string& commitId,
    1858             :                                                  const std::string& commitMsg) const
    1859             : {
    1860         187 :     auto account = account_.lock();
    1861         187 :     auto repo = repository();
    1862         187 :     if (not account or not repo) {
    1863           0 :         JAMI_WARNING("Invalid repository detected");
    1864           0 :         return false;
    1865             :     }
    1866             : 
    1867         187 :     auto treeNew = treeAtCommit(repo.get(), commitId);
    1868         187 :     auto userUri = uriFromDevice(userDevice, commitId);
    1869         187 :     if (userUri.empty())
    1870           0 :         return false;
    1871             : 
    1872         374 :     auto changedFiles = ConversationRepository::changedFiles(diffStats(commitId, ""));
    1873             :     // NOTE: libgit2 return a diff with /, not DIR_SEPARATOR_DIR
    1874             : 
    1875             :     try {
    1876         187 :         mode();
    1877           0 :     } catch (...) {
    1878           0 :         JAMI_ERROR("Invalid mode detected for commit: {}", commitId);
    1879           0 :         return false;
    1880           0 :     }
    1881             : 
    1882         187 :     std::string invited = {};
    1883         187 :     if (mode_ == ConversationMode::ONE_TO_ONE) {
    1884          59 :         Json::Value cm;
    1885          59 :         if (json::parse(commitMsg, cm)) {
    1886          59 :             invited = cm["invited"].asString();
    1887             :         }
    1888          59 :     }
    1889             : 
    1890         187 :     auto hasDevice = false, hasAdmin = false;
    1891         187 :     std::string adminsFile = fmt::format("admins/{}.crt", userUri);
    1892         187 :     std::string deviceFile = fmt::format("devices/{}.crt", userDevice);
    1893         187 :     std::string crlFile = fmt::format("CRLs/{}", userUri);
    1894           0 :     std::string invitedFile = fmt::format("invited/{}", invited);
    1895             : 
    1896             :     // Check that admin cert is added
    1897             :     // Check that device cert is added
    1898             :     // Check CRLs added
    1899             :     // Check that no other file is added
    1900             :     // Check if invited file present for one to one.
    1901         617 :     for (const auto& changedFile : changedFiles) {
    1902         432 :         if (changedFile == adminsFile) {
    1903         186 :             hasAdmin = true;
    1904         186 :             auto newFile = fileAtTree(changedFile, treeNew);
    1905         186 :             if (!verifyCertificate(as_view(newFile), userUri)) {
    1906           0 :                 JAMI_ERROR("Invalid certificate found {}", changedFile);
    1907           0 :                 return false;
    1908             :             }
    1909         432 :         } else if (changedFile == deviceFile) {
    1910         186 :             hasDevice = true;
    1911         186 :             auto newFile = fileAtTree(changedFile, treeNew);
    1912         186 :             if (!verifyCertificate(as_view(newFile), userUri)) {
    1913           4 :                 JAMI_ERROR("Invalid certificate found {}", changedFile);
    1914           1 :                 return false;
    1915             :             }
    1916         246 :         } else if (changedFile == crlFile || changedFile == invitedFile) {
    1917             :             // Nothing to do
    1918          59 :             continue;
    1919             :         } else {
    1920             :             // Invalid file detected
    1921           4 :             JAMI_ERROR("Invalid add file detected: {} {}", changedFile, (int) *mode_);
    1922           1 :             return false;
    1923             :         }
    1924             :     }
    1925             : 
    1926         185 :     return hasDevice && hasAdmin;
    1927         187 : }
    1928             : 
    1929             : bool
    1930         525 : ConversationRepository::Impl::validateDevice()
    1931             : {
    1932         525 :     auto repo = repository();
    1933         525 :     auto account = account_.lock();
    1934         525 :     if (!account || !repo) {
    1935           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Invalid repository detected", accountId_, id_);
    1936           0 :         return false;
    1937             :     }
    1938         525 :     auto path = fmt::format("devices/{}.crt", deviceId_);
    1939         525 :     std::filesystem::path devicePath = git_repository_workdir(repo.get());
    1940         525 :     devicePath /= path;
    1941         525 :     if (!std::filesystem::is_regular_file(devicePath)) {
    1942           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to find file {}", accountId_, id_, devicePath);
    1943           0 :         return false;
    1944             :     }
    1945             : 
    1946         525 :     auto wrongDeviceFile = false;
    1947             :     try {
    1948        1050 :         auto deviceCert = dht::crypto::Certificate(fileutils::loadFile(devicePath));
    1949         525 :         wrongDeviceFile = !account->isValidAccountDevice(deviceCert);
    1950         525 :     } catch (const std::exception&) {
    1951           0 :         wrongDeviceFile = true;
    1952           0 :     }
    1953         525 :     if (wrongDeviceFile) {
    1954          12 :         JAMI_WARNING(
    1955             :             "[Account {}] [Conversation {}] Device certificate is no longer valid. Attempting to update certificate.",
    1956             :             accountId_,
    1957             :             id_);
    1958             :         // Replace certificate with current cert
    1959           3 :         auto cert = account->identity().second;
    1960           3 :         if (!cert || !account->isValidAccountDevice(*cert)) {
    1961           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Current device's certificate is invalid. A migration is needed",
    1962             :                        accountId_,
    1963             :                        id_);
    1964           0 :             return false;
    1965             :         }
    1966           3 :         std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    1967           3 :         if (!file.is_open()) {
    1968           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to write data to {}", accountId_, id_, devicePath);
    1969           0 :             return false;
    1970             :         }
    1971           3 :         file << cert->toString(false);
    1972           3 :         file.close();
    1973           3 :         if (!add(path)) {
    1974           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to add file {}", accountId_, id_, devicePath);
    1975           0 :             return false;
    1976             :         }
    1977           3 :     }
    1978             : 
    1979             :     // Check account cert (a new device can be added but account certifcate can be the old one!)
    1980         525 :     auto adminPath = fmt::format("admins/{}.crt", userId_);
    1981         525 :     auto memberPath = fmt::format("members/{}.crt", userId_);
    1982         525 :     std::filesystem::path parentPath = git_repository_workdir(repo.get());
    1983         525 :     std::filesystem::path relativeParentPath;
    1984         525 :     if (std::filesystem::is_regular_file(parentPath / adminPath))
    1985         308 :         relativeParentPath = adminPath;
    1986         217 :     else if (std::filesystem::is_regular_file(parentPath / memberPath))
    1987         216 :         relativeParentPath = memberPath;
    1988         525 :     parentPath /= relativeParentPath;
    1989         525 :     if (relativeParentPath.empty()) {
    1990           4 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid parent path (not in members or admins)", accountId_, id_);
    1991           1 :         return false;
    1992             :     }
    1993         524 :     wrongDeviceFile = false;
    1994             :     try {
    1995        1048 :         auto parentCert = dht::crypto::Certificate(fileutils::loadFile(parentPath));
    1996         524 :         wrongDeviceFile = !account->isValidAccountDevice(parentCert);
    1997         524 :     } catch (const std::exception&) {
    1998           0 :         wrongDeviceFile = true;
    1999           0 :     }
    2000         524 :     if (wrongDeviceFile) {
    2001           4 :         JAMI_WARNING(
    2002             :             "[Account {}] [Conversation {}] Account certificate is no longer valid. Attempting to update certificate.",
    2003             :             accountId_,
    2004             :             id_);
    2005           1 :         auto cert = account->identity().second;
    2006           1 :         auto newCert = cert->issuer;
    2007           1 :         if (newCert && std::filesystem::is_regular_file(parentPath)) {
    2008           1 :             std::ofstream file(parentPath, std::ios::trunc | std::ios::binary);
    2009           1 :             if (!file.is_open()) {
    2010           0 :                 JAMI_ERROR("Unable to write data to {}", path);
    2011           0 :                 return false;
    2012             :             }
    2013           1 :             file << newCert->toString(true);
    2014           1 :             file.close();
    2015           1 :             if (!add(relativeParentPath.string())) {
    2016           0 :                 JAMI_WARNING("Unable to add file {}", path);
    2017           0 :                 return false;
    2018             :             }
    2019           1 :         }
    2020           1 :     }
    2021             : 
    2022         524 :     return true;
    2023         525 : }
    2024             : 
    2025             : std::string
    2026         492 : ConversationRepository::Impl::commit(const std::string& msg, bool verifyDevice)
    2027             : {
    2028         492 :     if (verifyDevice && !validateDevice()) {
    2029           4 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Invalid device", accountId_, id_);
    2030           1 :         return {};
    2031             :     }
    2032         491 :     GitSignature sig = signature();
    2033         491 :     if (!sig) {
    2034           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to generate signature", accountId_, id_);
    2035           0 :         return {};
    2036             :     }
    2037         491 :     auto account = account_.lock();
    2038             : 
    2039             :     // Retrieve current index
    2040         491 :     git_index* index_ptr = nullptr;
    2041         491 :     auto repo = repository();
    2042         491 :     if (!repo)
    2043           0 :         return {};
    2044         491 :     if (git_repository_index(&index_ptr, repo.get()) < 0) {
    2045           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to open repository index", accountId_, id_);
    2046           0 :         return {};
    2047             :     }
    2048         491 :     GitIndex index {index_ptr};
    2049             : 
    2050             :     git_oid tree_id;
    2051         491 :     if (git_index_write_tree(&tree_id, index.get()) < 0) {
    2052           0 :         JAMI_ERROR("[Account {}] [Conversation {}] commit failed: Unable to write initial tree from index",
    2053             :                    accountId_,
    2054             :                    id_);
    2055           0 :         return {};
    2056             :     }
    2057             : 
    2058         491 :     git_tree* tree_ptr = nullptr;
    2059         491 :     if (git_tree_lookup(&tree_ptr, repo.get(), &tree_id) < 0) {
    2060           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
    2061           0 :         return {};
    2062             :     }
    2063         491 :     GitTree tree {tree_ptr};
    2064             : 
    2065             :     git_oid commit_id;
    2066         491 :     if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
    2067           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2068           0 :         return {};
    2069             :     }
    2070             : 
    2071         491 :     git_commit* head_ptr = nullptr;
    2072         491 :     if (git_commit_lookup(&head_ptr, repo.get(), &commit_id) < 0) {
    2073           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", accountId_, id_);
    2074           0 :         return {};
    2075             :     }
    2076         491 :     GitCommit head_commit {head_ptr};
    2077             : 
    2078         491 :     git_buf to_sign = {};
    2079             :     // The last argument of git_commit_create_buffer is of type
    2080             :     // 'const git_commit **' in all versions of libgit2 except 1.8.0,
    2081             :     // 1.8.1 and 1.8.3, in which it is of type 'git_commit *const *'.
    2082             : #if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 \
    2083             :     && (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
    2084         491 :     git_commit* const head_ref[1] = {head_commit.get()};
    2085             : #else
    2086             :     const git_commit* head_ref[1] = {head_commit.get()};
    2087             : #endif
    2088         982 :     if (git_commit_create_buffer(
    2089         982 :             &to_sign, repo.get(), sig.get(), sig.get(), nullptr, msg.c_str(), tree.get(), 1, &head_ref[0])
    2090         491 :         < 0) {
    2091           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to create commit buffer", accountId_, id_);
    2092           0 :         return {};
    2093             :     }
    2094             : 
    2095             :     // git commit -S
    2096         491 :     auto to_sign_vec = std::vector<uint8_t>(to_sign.ptr, to_sign.ptr + to_sign.size);
    2097         491 :     auto signed_buf = account->identity().first->sign(to_sign_vec);
    2098         491 :     std::string signed_str = base64::encode(signed_buf);
    2099         491 :     if (git_commit_create_with_signature(&commit_id, repo.get(), to_sign.ptr, signed_str.c_str(), "signature") < 0) {
    2100           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
    2101           0 :         git_buf_dispose(&to_sign);
    2102           0 :         return {};
    2103             :     }
    2104         491 :     git_buf_dispose(&to_sign);
    2105             : 
    2106             :     // Move commit to main branch
    2107         491 :     git_reference* ref_ptr = nullptr;
    2108         491 :     if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) {
    2109           0 :         const git_error* err = giterr_last();
    2110           0 :         if (err) {
    2111           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to move commit to main: {}",
    2112             :                        accountId_,
    2113             :                        id_,
    2114             :                        err->message);
    2115           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, ECOMMIT, err->message);
    2116             :         }
    2117           0 :         return {};
    2118             :     }
    2119         491 :     git_reference_free(ref_ptr);
    2120             : 
    2121         491 :     auto commit_str = git_oid_tostr_s(&commit_id);
    2122         491 :     if (commit_str) {
    2123        1964 :         JAMI_LOG("[Account {}] [Conversation {}] New message added with id: {}", accountId_, id_, commit_str);
    2124             :     }
    2125         491 :     return commit_str ? commit_str : "";
    2126         491 : }
    2127             : 
    2128             : ConversationMode
    2129        7421 : ConversationRepository::Impl::mode() const
    2130             : {
    2131             :     // If already retrieved, return it, else get it from first commit
    2132        7421 :     if (mode_ != std::nullopt)
    2133        7006 :         return *mode_;
    2134             : 
    2135         414 :     LogOptions options;
    2136         414 :     options.from = id_;
    2137         414 :     options.nbOfCommits = 1;
    2138         414 :     auto lastMsg = log(options);
    2139         413 :     if (lastMsg.size() == 0) {
    2140           1 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No initial commit");
    2141           1 :         throw std::logic_error("Unable to retrieve first commit");
    2142             :     }
    2143         412 :     auto commitMsg = lastMsg[0].commit_msg;
    2144             : 
    2145         413 :     Json::Value root;
    2146         413 :     if (!json::parse(commitMsg, root)) {
    2147           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No initial commit");
    2148           0 :         throw std::logic_error("Unable to retrieve first commit");
    2149             :     }
    2150         413 :     if (!root.isMember("mode")) {
    2151           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_, id_, EINVALIDMODE, "No mode detected");
    2152           0 :         throw std::logic_error("No mode detected for initial commit");
    2153             :     }
    2154         413 :     int mode = root["mode"].asInt();
    2155             : 
    2156         413 :     switch (mode) {
    2157         134 :     case 0:
    2158         134 :         mode_ = ConversationMode::ONE_TO_ONE;
    2159         134 :         break;
    2160           6 :     case 1:
    2161           6 :         mode_ = ConversationMode::ADMIN_INVITES_ONLY;
    2162           6 :         break;
    2163         273 :     case 2:
    2164         273 :         mode_ = ConversationMode::INVITES_ONLY;
    2165         273 :         break;
    2166           0 :     case 3:
    2167           0 :         mode_ = ConversationMode::PUBLIC;
    2168           0 :         break;
    2169           0 :     default:
    2170           0 :         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2171           0 :                                                                      id_,
    2172             :                                                                      EINVALIDMODE,
    2173             :                                                                      "Incorrect mode detected");
    2174           0 :         throw std::logic_error("Incorrect mode detected");
    2175             :     }
    2176         413 :     return *mode_;
    2177         415 : }
    2178             : 
    2179             : std::string
    2180        2821 : ConversationRepository::Impl::diffStats(const std::string& newId, const std::string& oldId) const
    2181             : {
    2182        2821 :     if (auto repo = repository()) {
    2183        2821 :         if (auto d = diff(repo.get(), newId, oldId))
    2184        2821 :             return diffStats(d);
    2185        2820 :     }
    2186           1 :     return {};
    2187             : }
    2188             : 
    2189             : GitDiff
    2190        2820 : ConversationRepository::Impl::diff(git_repository* repo, const std::string& idNew, const std::string& idOld) const
    2191             : {
    2192        2820 :     if (!repo) {
    2193           0 :         JAMI_ERROR("Unable to get reference for HEAD");
    2194           0 :         return nullptr;
    2195             :     }
    2196             : 
    2197             :     // Retrieve tree for commit new
    2198             :     git_oid oid;
    2199        2820 :     git_commit* commitNew = nullptr;
    2200        2820 :     if (idNew == "HEAD") {
    2201         907 :         if (git_reference_name_to_id(&oid, repo, "HEAD") < 0) {
    2202           0 :             JAMI_ERROR("Unable to get reference for HEAD");
    2203           0 :             return nullptr;
    2204             :         }
    2205             : 
    2206         907 :         if (git_commit_lookup(&commitNew, repo, &oid) < 0) {
    2207           0 :             JAMI_ERROR("Unable to look up HEAD commit");
    2208           0 :             return nullptr;
    2209             :         }
    2210             :     } else {
    2211        1913 :         if (git_oid_fromstr(&oid, idNew.c_str()) < 0 || git_commit_lookup(&commitNew, repo, &oid) < 0) {
    2212           0 :             GitCommit new_commit {commitNew};
    2213           0 :             JAMI_WARNING("Failed to look up commit {}", idNew);
    2214           0 :             return nullptr;
    2215           0 :         }
    2216             :     }
    2217        2821 :     GitCommit new_commit {commitNew};
    2218             : 
    2219        2821 :     git_tree* tNew = nullptr;
    2220        2821 :     if (git_commit_tree(&tNew, new_commit.get()) < 0) {
    2221           0 :         JAMI_ERROR("Unable to look up initial tree");
    2222           0 :         return nullptr;
    2223             :     }
    2224        2821 :     GitTree treeNew {tNew};
    2225             : 
    2226        2821 :     git_diff* diff_ptr = nullptr;
    2227        2821 :     if (idOld.empty()) {
    2228         188 :         if (git_diff_tree_to_tree(&diff_ptr, repo, nullptr, treeNew.get(), {}) < 0) {
    2229           0 :             JAMI_ERROR("Unable to get diff to empty repository");
    2230           0 :             return nullptr;
    2231             :         }
    2232         188 :         return GitDiff(diff_ptr);
    2233             :     }
    2234             : 
    2235             :     // Retrieve tree for commit old
    2236        2633 :     git_commit* commitOld = nullptr;
    2237        2633 :     if (git_oid_fromstr(&oid, idOld.c_str()) < 0 || git_commit_lookup(&commitOld, repo, &oid) < 0) {
    2238           4 :         JAMI_WARNING("Failed to look up commit {}", idOld);
    2239           1 :         return nullptr;
    2240             :     }
    2241        2632 :     GitCommit old_commit {commitOld};
    2242             : 
    2243        2632 :     git_tree* tOld = nullptr;
    2244        2632 :     if (git_commit_tree(&tOld, old_commit.get()) < 0) {
    2245           0 :         JAMI_ERROR("Unable to look up initial tree");
    2246           0 :         return nullptr;
    2247             :     }
    2248        2632 :     GitTree treeOld {tOld};
    2249             : 
    2250             :     // Calc diff
    2251        2631 :     if (git_diff_tree_to_tree(&diff_ptr, repo, treeOld.get(), treeNew.get(), {}) < 0) {
    2252           0 :         JAMI_ERROR("Unable to get diff between {} and {}", idOld, idNew);
    2253           0 :         return nullptr;
    2254             :     }
    2255        2632 :     return GitDiff(diff_ptr);
    2256        2821 : }
    2257             : 
    2258             : std::vector<ConversationCommit>
    2259        1773 : ConversationRepository::Impl::behind(const std::string& from) const
    2260             : {
    2261             :     git_oid oid_local, oid_head, oid_remote;
    2262        1773 :     auto repo = repository();
    2263        1773 :     if (!repo)
    2264           0 :         return {};
    2265        1773 :     if (git_reference_name_to_id(&oid_local, repo.get(), "HEAD") < 0) {
    2266           0 :         JAMI_ERROR("Unable to get reference for HEAD");
    2267           0 :         return {};
    2268             :     }
    2269        1773 :     oid_head = oid_local;
    2270        1773 :     std::string head = git_oid_tostr_s(&oid_head);
    2271        1773 :     if (git_oid_fromstr(&oid_remote, from.c_str()) < 0) {
    2272           0 :         JAMI_ERROR("Unable to get reference for commit {}", from);
    2273           0 :         return {};
    2274             :     }
    2275             : 
    2276             :     git_oidarray bases;
    2277        1773 :     if (git_merge_bases(&bases, repo.get(), &oid_local, &oid_remote) != 0) {
    2278           0 :         JAMI_ERROR("Unable to get any merge base for commit {} and {}", from, head);
    2279           0 :         return {};
    2280             :     }
    2281        3491 :     for (std::size_t i = 0; i < bases.count; ++i) {
    2282        1773 :         std::string oid = git_oid_tostr_s(&bases.ids[i]);
    2283        1773 :         if (oid != head) {
    2284          55 :             oid_local = bases.ids[i];
    2285          55 :             break;
    2286             :         }
    2287        1773 :     }
    2288        1773 :     git_oidarray_free(&bases);
    2289        1773 :     std::string to = git_oid_tostr_s(&oid_local);
    2290        1773 :     if (to == from)
    2291         849 :         return {};
    2292        1848 :     return log(LogOptions {from, to});
    2293        1773 : }
    2294             : 
    2295             : void
    2296       19062 : ConversationRepository::Impl::forEachCommit(PreConditionCb&& preCondition,
    2297             :                                             std::function<void(ConversationCommit&&)>&& emplaceCb,
    2298             :                                             PostConditionCb&& postCondition,
    2299             :                                             const std::string& from,
    2300             :                                             bool logIfNotFound) const
    2301             : {
    2302             :     git_oid oid, oidFrom, oidMerge;
    2303             : 
    2304             :     // NOTE! Start from head to get all merge possibilities and correct linearized parent.
    2305       19062 :     auto repo = repository();
    2306       19063 :     if (!repo or git_reference_name_to_id(&oid, repo.get(), "HEAD") < 0) {
    2307           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2308           0 :         return;
    2309             :     }
    2310             : 
    2311       19066 :     if (from != "" && git_oid_fromstr(&oidFrom, from.c_str()) == 0) {
    2312       16659 :         auto isMergeBase = git_merge_base(&oidMerge, repo.get(), &oid, &oidFrom) == 0
    2313       16660 :                            && git_oid_equal(&oidMerge, &oidFrom);
    2314       16660 :         if (!isMergeBase) {
    2315             :             // We're logging a non merged branch, so, take this one instead of HEAD
    2316        6023 :             oid = oidFrom;
    2317             :         }
    2318             :     }
    2319             : 
    2320       19064 :     git_revwalk* walker_ptr = nullptr;
    2321       19064 :     if (git_revwalk_new(&walker_ptr, repo.get()) < 0 || git_revwalk_push(walker_ptr, &oid) < 0) {
    2322        3035 :         GitRevWalker walker {walker_ptr};
    2323             :         // This fail can be permitted in the case we check if a commit exists before pulling (so can fail
    2324             :         // there). Only log if the fail is unwanted.
    2325        3035 :         if (logIfNotFound)
    2326        6512 :             JAMI_DEBUG("[Account {}] [Conversation {}] Unable to init revwalker from {}", accountId_, id_, from);
    2327        3035 :         return;
    2328        3035 :     }
    2329             : 
    2330       16031 :     GitRevWalker walker {walker_ptr};
    2331       16026 :     git_revwalk_sorting(walker.get(), GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME);
    2332             : 
    2333       39687 :     for (auto idx = 0u; !git_revwalk_next(&oid, walker.get()); ++idx) {
    2334       38250 :         git_commit* commit_ptr = nullptr;
    2335       38250 :         std::string id = git_oid_tostr_s(&oid);
    2336       38242 :         if (git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    2337           0 :             JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, id);
    2338           0 :             break;
    2339             :         }
    2340       38259 :         GitCommit commit {commit_ptr};
    2341             : 
    2342       38248 :         const git_signature* sig = git_commit_author(commit.get());
    2343       38243 :         GitAuthor author;
    2344       38240 :         author.name = sig->name;
    2345       38246 :         author.email = sig->email;
    2346             : 
    2347       38252 :         std::vector<std::string> parents;
    2348       38237 :         auto parentsCount = git_commit_parentcount(commit.get());
    2349       74674 :         for (unsigned int p = 0; p < parentsCount; ++p) {
    2350       36432 :             std::string parent {};
    2351       36416 :             const git_oid* pid = git_commit_parent_id(commit.get(), p);
    2352       36422 :             if (pid) {
    2353       36423 :                 parent = git_oid_tostr_s(pid);
    2354       36441 :                 parents.emplace_back(parent);
    2355             :             }
    2356       36418 :         }
    2357             : 
    2358       38242 :         auto result = preCondition(id, author, commit);
    2359       38240 :         if (result == CallbackResult::Skip)
    2360        1136 :             continue;
    2361       37104 :         else if (result == CallbackResult::Break)
    2362       14314 :             break;
    2363             : 
    2364       22790 :         ConversationCommit cc;
    2365       22795 :         cc.id = id;
    2366       22807 :         cc.commit_msg = git_commit_message(commit.get());
    2367       22808 :         cc.author = std::move(author);
    2368       22793 :         cc.parents = std::move(parents);
    2369       22797 :         git_buf signature = {}, signed_data = {};
    2370       22797 :         if (git_commit_extract_signature(&signature, &signed_data, repo.get(), &oid, "signature") < 0) {
    2371           4 :             JAMI_WARNING("[Account {}] [Conversation {}] Unable to extract signature for commit {}",
    2372             :                          accountId_,
    2373             :                          id_,
    2374             :                          id);
    2375             :         } else {
    2376       22810 :             cc.signature = base64::decode(std::string_view(signature.ptr, signature.size));
    2377       22801 :             cc.signed_content = std::vector<uint8_t>(signed_data.ptr, signed_data.ptr + signed_data.size);
    2378             :         }
    2379       22798 :         git_buf_dispose(&signature);
    2380       22805 :         git_buf_dispose(&signed_data);
    2381       22807 :         cc.timestamp = git_commit_time(commit.get());
    2382             : 
    2383       22800 :         auto post = postCondition(id, cc.author, cc);
    2384       22802 :         emplaceCb(std::move(cc));
    2385             : 
    2386       22800 :         if (post)
    2387         272 :             break;
    2388       85675 :     }
    2389       19066 : }
    2390             : 
    2391             : std::vector<ConversationCommit>
    2392       17548 : ConversationRepository::Impl::log(const LogOptions& options) const
    2393             : {
    2394       17548 :     std::vector<ConversationCommit> commits {};
    2395       17548 :     auto startLogging = options.from == "";
    2396       17548 :     auto breakLogging = false;
    2397       17544 :     forEachCommit(
    2398       30085 :         [&](const auto& id, const auto& author, const auto& commit) {
    2399       30085 :             if (!commits.empty()) {
    2400             :                 // Set linearized parent
    2401       14470 :                 commits.rbegin()->linearized_parent = id;
    2402             :             }
    2403       30089 :             if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) {
    2404           0 :                 return CallbackResult::Skip;
    2405             :             }
    2406       30094 :             if ((options.nbOfCommits != 0 && commits.size() == options.nbOfCommits))
    2407       12700 :                 return CallbackResult::Break; // Stop logging
    2408       17393 :             if (breakLogging)
    2409           0 :                 return CallbackResult::Break; // Stop logging
    2410       17393 :             if (id == options.to) {
    2411         924 :                 if (options.includeTo)
    2412           0 :                     breakLogging = true; // For the next commit
    2413             :                 else
    2414         924 :                     return CallbackResult::Break; // Stop logging
    2415             :             }
    2416             : 
    2417       16471 :             if (!startLogging && options.from != "" && options.from == id)
    2418       12512 :                 startLogging = true;
    2419       16467 :             if (!startLogging)
    2420        1110 :                 return CallbackResult::Skip; // Start logging after this one
    2421             : 
    2422       15357 :             if (options.fastLog) {
    2423           0 :                 if (options.authorUri != "") {
    2424           0 :                     if (options.authorUri == uriFromDevice(author.email)) {
    2425           0 :                         return CallbackResult::Break; // Found author, stop
    2426             :                     }
    2427             :                 }
    2428             :                 // Used to only count commit
    2429           0 :                 commits.emplace(commits.end(), ConversationCommit {});
    2430           0 :                 return CallbackResult::Skip;
    2431             :             }
    2432             : 
    2433       15357 :             return CallbackResult::Ok; // Continue
    2434             :         },
    2435       15357 :         [&](auto&& cc) { commits.emplace(commits.end(), std::forward<decltype(cc)>(cc)); },
    2436       15362 :         [](auto, auto, auto) { return false; },
    2437       17548 :         options.from,
    2438       17548 :         options.logIfNotFound);
    2439       35096 :     return commits;
    2440           0 : }
    2441             : 
    2442             : GitObject
    2443       11798 : ConversationRepository::Impl::fileAtTree(const std::string& path, const GitTree& tree) const
    2444             : {
    2445       11798 :     git_object* blob_ptr = nullptr;
    2446       11798 :     if (git_object_lookup_bypath(&blob_ptr, reinterpret_cast<git_object*>(tree.get()), path.c_str(), GIT_OBJECT_BLOB)
    2447       11798 :         != 0) {
    2448        3232 :         return GitObject(nullptr);
    2449             :     }
    2450        8566 :     return GitObject(blob_ptr);
    2451             : }
    2452             : 
    2453             : GitObject
    2454        1724 : ConversationRepository::Impl::memberCertificate(std::string_view memberUri, const GitTree& tree) const
    2455             : {
    2456        1724 :     auto blob = fileAtTree(fmt::format("members/{}.crt", memberUri), tree);
    2457        1724 :     if (not blob)
    2458         888 :         blob = fileAtTree(fmt::format("admins/{}.crt", memberUri), tree);
    2459        1724 :     return blob;
    2460           0 : }
    2461             : 
    2462             : GitTree
    2463        5089 : ConversationRepository::Impl::treeAtCommit(git_repository* repo, const std::string& commitId) const
    2464             : {
    2465             :     git_oid oid;
    2466        5089 :     git_commit* commit = nullptr;
    2467        5089 :     if (git_oid_fromstr(&oid, commitId.c_str()) < 0 || git_commit_lookup(&commit, repo, &oid) < 0) {
    2468           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_, commitId);
    2469           0 :         return GitTree(nullptr);
    2470             :     }
    2471        5091 :     GitCommit gc {commit};
    2472        5091 :     git_tree* tree = nullptr;
    2473        5091 :     if (git_commit_tree(&tree, gc.get()) < 0) {
    2474           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
    2475           0 :         return GitTree(nullptr);
    2476             :     }
    2477        5090 :     return GitTree {tree};
    2478        5091 : }
    2479             : 
    2480             : std::vector<std::string>
    2481         198 : ConversationRepository::Impl::getInitialMembers() const
    2482             : {
    2483         198 :     auto acc = account_.lock();
    2484         198 :     if (!acc)
    2485           0 :         return {};
    2486         198 :     LogOptions options;
    2487         197 :     options.from = id_;
    2488         198 :     options.nbOfCommits = 1;
    2489         198 :     auto firstCommit = log(options);
    2490         198 :     if (firstCommit.size() == 0) {
    2491           0 :         return {};
    2492             :     }
    2493         198 :     auto commit = firstCommit[0];
    2494             : 
    2495         198 :     auto authorDevice = commit.author.email;
    2496         198 :     auto cert = acc->certStore().getCertificate(authorDevice);
    2497         198 :     if (!cert || !cert->issuer)
    2498           2 :         return {};
    2499         196 :     auto authorId = cert->issuer->getId().toString();
    2500         196 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    2501         196 :         Json::Value root;
    2502         196 :         if (!json::parse(commit.commit_msg, root)) {
    2503           0 :             return {authorId};
    2504             :         }
    2505         196 :         if (root.isMember("invited") && root["invited"].asString() != authorId)
    2506         585 :             return {authorId, root["invited"].asString()};
    2507         196 :     }
    2508           2 :     return {authorId};
    2509         198 : }
    2510             : 
    2511             : bool
    2512           1 : ConversationRepository::Impl::resolveConflicts(git_index* index, const std::string& other_id)
    2513             : {
    2514           1 :     git_index_conflict_iterator* conflict_iterator = nullptr;
    2515           1 :     const git_index_entry* ancestor_out = nullptr;
    2516           1 :     const git_index_entry* our_out = nullptr;
    2517           1 :     const git_index_entry* their_out = nullptr;
    2518             : 
    2519           1 :     git_index_conflict_iterator_new(&conflict_iterator, index);
    2520           1 :     GitIndexConflictIterator ci {conflict_iterator};
    2521             : 
    2522             :     git_oid head_commit_id;
    2523           1 :     auto repo = repository();
    2524           1 :     if (!repo || git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
    2525           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
    2526           0 :         return false;
    2527             :     }
    2528           1 :     auto commit_str = git_oid_tostr_s(&head_commit_id);
    2529           1 :     if (!commit_str)
    2530           0 :         return false;
    2531           1 :     auto useRemote = (other_id > commit_str); // Choose by commit version
    2532             : 
    2533             :     // NOTE: for now, only authorize conflicts on "profile.vcf"
    2534           1 :     std::vector<git_index_entry> new_entries;
    2535           2 :     while (git_index_conflict_next(&ancestor_out, &our_out, &their_out, ci.get()) != GIT_ITEROVER) {
    2536           1 :         if (ancestor_out && ancestor_out->path && our_out && our_out->path && their_out && their_out->path) {
    2537           1 :             if (std::string_view(ancestor_out->path) == "profile.vcf"sv) {
    2538             :                 // Checkout the wanted version. Copy the index_entry.
    2539           1 :                 git_index_entry resolution = useRemote ? *their_out : *our_out;
    2540           1 :                 resolution.flags &= GIT_INDEX_STAGE_NORMAL;
    2541           1 :                 if (!(resolution.flags & GIT_IDXENTRY_VALID))
    2542           1 :                     resolution.flags |= GIT_IDXENTRY_VALID;
    2543             :                 // NOTE: do no git_index_add yet, wait for after full conflict checks
    2544           1 :                 new_entries.push_back(resolution);
    2545           1 :                 continue;
    2546           1 :             }
    2547           0 :             JAMI_ERROR("Conflict detected on a file that is not authorized: {}", ancestor_out->path);
    2548           0 :             return false;
    2549             :         }
    2550           0 :         return false;
    2551             :     }
    2552             : 
    2553           2 :     for (auto& entry : new_entries)
    2554           1 :         git_index_add(index, &entry);
    2555           1 :     git_index_conflict_cleanup(index);
    2556             : 
    2557             :     // Checkout and clean up
    2558             :     git_checkout_options opt;
    2559           1 :     git_checkout_options_init(&opt, GIT_CHECKOUT_OPTIONS_VERSION);
    2560           1 :     opt.checkout_strategy |= GIT_CHECKOUT_FORCE;
    2561           1 :     opt.checkout_strategy |= GIT_CHECKOUT_ALLOW_CONFLICTS;
    2562           1 :     if (other_id > commit_str)
    2563           1 :         opt.checkout_strategy |= GIT_CHECKOUT_USE_THEIRS;
    2564             :     else
    2565           0 :         opt.checkout_strategy |= GIT_CHECKOUT_USE_OURS;
    2566             : 
    2567           1 :     if (git_checkout_index(repo.get(), index, &opt) < 0) {
    2568           0 :         const git_error* err = giterr_last();
    2569           0 :         if (err)
    2570           0 :             JAMI_ERROR("Unable to checkout index: {}", err->message);
    2571           0 :         return false;
    2572             :     }
    2573             : 
    2574           1 :     return true;
    2575           1 : }
    2576             : 
    2577             : void
    2578        1186 : ConversationRepository::Impl::initMembers()
    2579             : {
    2580             :     using std::filesystem::path;
    2581        1186 :     auto repo = repository();
    2582        1186 :     if (!repo)
    2583           0 :         throw std::logic_error("Invalid Git repository");
    2584             : 
    2585        1186 :     std::vector<std::string> uris;
    2586        1186 :     std::lock_guard lk(membersMtx_);
    2587        1186 :     members_.clear();
    2588        1186 :     path repoPath = git_repository_workdir(repo.get());
    2589             : 
    2590           0 :     static const std::vector<std::pair<MemberRole, path>> paths = {{MemberRole::ADMIN, MemberPath::ADMINS},
    2591           0 :                                                                    {MemberRole::MEMBER, MemberPath::MEMBERS},
    2592           0 :                                                                    {MemberRole::INVITED, MemberPath::INVITED},
    2593           0 :                                                                    {MemberRole::BANNED,
    2594          26 :                                                                     MemberPath::BANNED / MemberPath::MEMBERS},
    2595           0 :                                                                    {MemberRole::BANNED,
    2596        1264 :                                                                     MemberPath::BANNED / MemberPath::INVITED}};
    2597             : 
    2598        1186 :     std::error_code ec;
    2599        7115 :     for (const auto& [role, p] : paths) {
    2600       30494 :         for (const auto& f : std::filesystem::directory_iterator(repoPath / p, ec)) {
    2601       12705 :             auto uri = f.path().stem().string();
    2602       12704 :             if (std::find(uris.begin(), uris.end(), uri) == uris.end()) {
    2603       12703 :                 members_.emplace_back(ConversationMember {uri, role});
    2604       12702 :                 uris.emplace_back(uri);
    2605             :             }
    2606       18634 :         }
    2607             :     }
    2608             : 
    2609        1186 :     if (mode() == ConversationMode::ONE_TO_ONE) {
    2610         496 :         for (const auto& member : getInitialMembers()) {
    2611         329 :             if (std::find(uris.begin(), uris.end(), member) == uris.end()) {
    2612             :                 // If member is in the initial commit, but not in invited, this means that user left.
    2613           0 :                 members_.emplace_back(ConversationMember {member, MemberRole::LEFT});
    2614             :             }
    2615         167 :         }
    2616             :     }
    2617        1185 :     saveMembers();
    2618        1189 : }
    2619             : 
    2620             : std::optional<std::map<std::string, std::string>>
    2621       19996 : ConversationRepository::Impl::convCommitToMap(const ConversationCommit& commit) const
    2622             : {
    2623       19996 :     auto authorId = uriFromDevice(commit.author.email, commit.id);
    2624       20000 :     if (authorId.empty()) {
    2625           4 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid author ID for commit {}", accountId_, id_, commit.id);
    2626           1 :         return std::nullopt;
    2627             :     }
    2628       19999 :     std::string parents;
    2629       19998 :     auto parentsSize = commit.parents.size();
    2630       39248 :     for (std::size_t i = 0; i < parentsSize; ++i) {
    2631       19251 :         parents += commit.parents[i];
    2632       19251 :         if (i != parentsSize - 1)
    2633          35 :             parents += ",";
    2634             :     }
    2635       19997 :     std::string type {};
    2636       19997 :     if (parentsSize > 1)
    2637          35 :         type = "merge";
    2638       19997 :     std::string body {};
    2639       19998 :     std::map<std::string, std::string> message;
    2640       19996 :     if (type.empty()) {
    2641       19961 :         Json::Value cm;
    2642       19963 :         if (json::parse(commit.commit_msg, cm)) {
    2643       78490 :             for (auto const& id : cm.getMemberNames()) {
    2644       58522 :                 if (id == "type") {
    2645       19960 :                     type = cm[id].asString();
    2646       19963 :                     continue;
    2647             :                 }
    2648       38568 :                 message.insert({id, cm[id].asString()});
    2649       19958 :             }
    2650             :         }
    2651       19960 :     }
    2652       19998 :     if (type.empty()) {
    2653           0 :         return std::nullopt;
    2654       19996 :     } else if (type == "application/data-transfer+json") {
    2655             :         // Avoid the client to do the concatenation
    2656         140 :         auto tid = message["tid"];
    2657          70 :         if (not tid.empty()) {
    2658          69 :             auto extension = fileutils::getFileExtension(message["displayName"]);
    2659          69 :             if (!extension.empty())
    2660           0 :                 message["fileId"] = fmt::format("{}_{}.{}", commit.id, tid, extension);
    2661             :             else
    2662         138 :                 message["fileId"] = fmt::format("{}_{}", commit.id, tid);
    2663             :         } else {
    2664           1 :             message["fileId"] = "";
    2665             :         }
    2666          70 :     }
    2667       19994 :     message["id"] = commit.id;
    2668       19993 :     message["parents"] = parents;
    2669       19995 :     message["linearizedParent"] = commit.linearized_parent;
    2670       19994 :     message["author"] = authorId;
    2671       19999 :     message["type"] = type;
    2672       19997 :     message["timestamp"] = std::to_string(commit.timestamp);
    2673             : 
    2674       19996 :     return message;
    2675       19996 : }
    2676             : 
    2677             : std::string
    2678        2820 : ConversationRepository::Impl::diffStats(const GitDiff& diff) const
    2679             : {
    2680        2820 :     git_diff_stats* stats_ptr = nullptr;
    2681        2820 :     if (git_diff_get_stats(&stats_ptr, diff.get()) < 0) {
    2682           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get diff stats", accountId_, id_);
    2683           0 :         return {};
    2684             :     }
    2685        2820 :     GitDiffStats stats {stats_ptr};
    2686             : 
    2687        2820 :     git_diff_stats_format_t format = GIT_DIFF_STATS_FULL;
    2688        2820 :     git_buf statsBuf = {};
    2689        2820 :     if (git_diff_stats_to_buf(&statsBuf, stats.get(), format, 80) < 0) {
    2690           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to format diff stats", accountId_, id_);
    2691           0 :         return {};
    2692             :     }
    2693             : 
    2694        2820 :     auto res = std::string(statsBuf.ptr, statsBuf.ptr + statsBuf.size);
    2695        2820 :     git_buf_dispose(&statsBuf);
    2696        2820 :     return res;
    2697        2820 : }
    2698             : 
    2699             : //////////////////////////////////
    2700             : 
    2701             : std::unique_ptr<ConversationRepository>
    2702         199 : ConversationRepository::createConversation(const std::shared_ptr<JamiAccount>& account,
    2703             :                                            ConversationMode mode,
    2704             :                                            const std::string& otherMember)
    2705             : {
    2706             :     // Create temporary directory because we are unable to know the first hash for now
    2707         199 :     std::uniform_int_distribution<uint64_t> dist;
    2708         398 :     auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
    2709         199 :     dhtnet::fileutils::check_dir(conversationsPath);
    2710         398 :     auto tmpPath = conversationsPath / std::to_string(dist(account->rand));
    2711         199 :     if (std::filesystem::is_directory(tmpPath)) {
    2712           0 :         JAMI_ERROR("{} already exists. Abort create conversations", tmpPath);
    2713           0 :         return {};
    2714             :     }
    2715         199 :     if (!dhtnet::fileutils::recursive_mkdir(tmpPath, 0700)) {
    2716           0 :         JAMI_ERROR("An error occurred when creating {}. Abort create conversations.", tmpPath);
    2717           0 :         return {};
    2718             :     }
    2719         199 :     auto repo = create_empty_repository(tmpPath.string());
    2720         199 :     if (!repo) {
    2721           0 :         return {};
    2722             :     }
    2723             : 
    2724             :     // Add initial files
    2725         199 :     if (!add_initial_files(repo, account, mode, otherMember)) {
    2726           0 :         JAMI_ERROR("An error occurred while adding the initial files.");
    2727           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2728           0 :         return {};
    2729             :     }
    2730             : 
    2731             :     // Commit changes
    2732         199 :     auto id = initial_commit(repo, account, mode, otherMember);
    2733         199 :     if (id.empty()) {
    2734           0 :         JAMI_ERROR("Unable to create initial commit in {}", tmpPath);
    2735           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2736           0 :         return {};
    2737             :     }
    2738             : 
    2739             :     // Move to wanted directory
    2740         199 :     auto newPath = conversationsPath / id;
    2741         199 :     std::error_code ec;
    2742         199 :     std::filesystem::rename(tmpPath, newPath, ec);
    2743         199 :     if (ec) {
    2744           0 :         JAMI_ERROR("Unable to move {} in {}: {}", tmpPath, newPath, ec.message());
    2745           0 :         dhtnet::fileutils::removeAll(tmpPath, true);
    2746           0 :         return {};
    2747             :     }
    2748             : 
    2749         796 :     JAMI_LOG("New conversation initialized in {}", newPath);
    2750             : 
    2751         199 :     return std::make_unique<ConversationRepository>(account, id);
    2752         199 : }
    2753             : 
    2754             : std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>
    2755         191 : ConversationRepository::cloneConversation(const std::shared_ptr<JamiAccount>& account,
    2756             :                                           const std::string& deviceId,
    2757             :                                           const std::string& conversationId)
    2758             : {
    2759             :     // Verify conversationId is not empty to avoid deleting the entire conversations directory
    2760         191 :     if (conversationId.empty()) {
    2761           0 :         JAMI_ERROR("[Account {}] Clone conversation with empty conversationId", account->getAccountID());
    2762           0 :         return {};
    2763             :     }
    2764             : 
    2765         382 :     auto conversationsPath = fileutils::get_data_dir() / account->getAccountID() / "conversations";
    2766         191 :     dhtnet::fileutils::check_dir(conversationsPath);
    2767         382 :     auto path = conversationsPath / conversationId;
    2768           0 :     auto url = fmt::format("git://{}/{}", deviceId, conversationId);
    2769             : 
    2770             :     git_clone_options clone_options;
    2771         191 :     git_clone_options_init(&clone_options, GIT_CLONE_OPTIONS_VERSION);
    2772         191 :     git_fetch_options_init(&clone_options.fetch_opts, GIT_FETCH_OPTIONS_VERSION);
    2773         191 :     clone_options.fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) {
    2774             :         // Uncomment to get advancment
    2775             :         // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
    2776             :         //     JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
    2777             :         //     stats->received_bytes/1024);
    2778             :         // If a pack is more than 256Mb, it's anormal.
    2779           0 :         if (stats->received_bytes > MAX_FETCH_SIZE) {
    2780           0 :             JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
    2781             :                        stats->received_bytes,
    2782             :                        stats->received_objects,
    2783             :                        stats->total_objects);
    2784           0 :             return -1;
    2785             :         }
    2786           0 :         return 0;
    2787         191 :     };
    2788             : 
    2789         191 :     if (std::filesystem::is_directory(path)) {
    2790             :         // If a crash occurs during a previous clone, just in case
    2791           0 :         JAMI_WARNING("[Account {}] [Conversation {}] Removing non empty directory {}",
    2792             :                      account->getAccountID(),
    2793             :                      conversationId,
    2794             :                      path);
    2795           0 :         if (dhtnet::fileutils::removeAll(path, true) != 0)
    2796           0 :             return {};
    2797             :     }
    2798             : 
    2799         764 :     JAMI_DEBUG("[Account {}] [Conversation {}] Start clone of {:s} to {}",
    2800             :                account->getAccountID(),
    2801             :                conversationId,
    2802             :                url,
    2803             :                path);
    2804         191 :     git_repository* rep = nullptr;
    2805         191 :     git_clone_options opts = GIT_CLONE_OPTIONS_INIT;
    2806         191 :     opts.fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
    2807         191 :     if (auto err = git_clone(&rep, url.c_str(), path.string().c_str(), &opts)) {
    2808           1 :         if (const git_error* gerr = giterr_last())
    2809           4 :             JAMI_ERROR("[Account {}] [Conversation {}] Error when retrieving remote conversation: {:s} {}",
    2810             :                        account->getAccountID(),
    2811             :                        conversationId,
    2812             :                        gerr->message,
    2813             :                        path);
    2814             :         else
    2815           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unknown error {:d} when retrieving remote conversation",
    2816             :                        account->getAccountID(),
    2817             :                        conversationId,
    2818             :                        err);
    2819           1 :         return {};
    2820             :     }
    2821         190 :     git_repository_free(rep);
    2822         190 :     auto repo = std::make_unique<ConversationRepository>(account, conversationId);
    2823         189 :     repo->pinCertificates(true); // need to load certificates to validate unknown members
    2824         189 :     auto [commitsToValidate, valid] = repo->validClone();
    2825         189 :     if (!valid) {
    2826           4 :         repo->erase();
    2827          16 :         JAMI_ERROR("[Account {}] [Conversation {}] An error occurred while validating remote conversation.",
    2828             :                    account->getAccountID(),
    2829             :                    conversationId);
    2830           4 :         return {};
    2831             :     }
    2832         740 :     JAMI_LOG("[Account {}] [Conversation {}] New conversation cloned in {}",
    2833             :              account->getAccountID(),
    2834             :              conversationId,
    2835             :              path);
    2836         185 :     return {std::move(repo), std::move(commitsToValidate)};
    2837         193 : }
    2838             : 
    2839             : bool
    2840        1967 : ConversationRepository::Impl::validCommits(const std::vector<ConversationCommit>& commitsToValidate) const
    2841             : {
    2842        1967 :     auto repo = repository();
    2843             : 
    2844        3868 :     for (const auto& commit : commitsToValidate) {
    2845        1927 :         auto userDevice = commit.author.email;
    2846        1927 :         auto validUserAtCommit = commit.id;
    2847             : 
    2848             :         git_oid oid;
    2849        1927 :         git_commit* commit_ptr = nullptr;
    2850        1927 :         if (git_oid_fromstr(&oid, validUserAtCommit.c_str()) < 0
    2851        1927 :             || git_commit_lookup(&commit_ptr, repo.get(), &oid) < 0) {
    2852           0 :             JAMI_WARNING("Failed to look up commit {}", validUserAtCommit.c_str());
    2853             :         }
    2854        1927 :         GitBuf sig(new git_buf {});
    2855        1927 :         GitBuf sig_data(new git_buf {});
    2856             : 
    2857             :         // Extract the signature block and signature content from the commit
    2858        1927 :         int sig_extract_res = git_commit_extract_signature(sig.get(), sig_data.get(), repo.get(), &oid, "signature");
    2859        1927 :         if (sig_extract_res != 0) {
    2860           0 :             switch (sig_extract_res) {
    2861           0 :             case GIT_ERROR_INVALID:
    2862           0 :                 JAMI_ERROR("Error, the commit ID ({}) does not correspond to a commit.", validUserAtCommit);
    2863           0 :                 break;
    2864           0 :             case GIT_ERROR_OBJECT:
    2865           0 :                 JAMI_ERROR("Error, the commit ID ({}) does not have a signature.", validUserAtCommit);
    2866           0 :                 break;
    2867           0 :             default:
    2868           0 :                 JAMI_ERROR("An unknown error occurred while extracting signature for commit ID {}.", validUserAtCommit);
    2869           0 :                 break;
    2870             :             }
    2871           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2872           0 :                                                                          id_,
    2873             :                                                                          EVALIDFETCH,
    2874             :                                                                          "Malformed commit");
    2875           0 :             return false;
    2876             :         }
    2877             : 
    2878        1927 :         if (commit.parents.size() == 0) {
    2879         187 :             if (!checkInitialCommit(userDevice, commit.id, commit.commit_msg)) {
    2880           8 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed initial commit {}. Please "
    2881             :                              "ensure that you are using the latest "
    2882             :                              "version of Jami, or that one of your contacts is not performing any "
    2883             :                              "unwanted actions.",
    2884             :                              accountId_,
    2885             :                              id_,
    2886             :                              commit.id);
    2887           2 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2888           2 :                                                                              id_,
    2889             :                                                                              EVALIDFETCH,
    2890             :                                                                              "Malformed initial commit");
    2891           2 :                 return false;
    2892             :             }
    2893        1740 :         } else if (commit.parents.size() == 1) {
    2894        1731 :             std::string type = {}, editId = {};
    2895        1731 :             Json::Value cm;
    2896        1731 :             if (json::parse(commit.commit_msg, cm)) {
    2897        1731 :                 type = cm["type"].asString();
    2898        1731 :                 editId = cm["edit"].asString();
    2899             :             } else {
    2900           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2901           0 :                                                                              id_,
    2902             :                                                                              EVALIDFETCH,
    2903             :                                                                              "Malformed commit");
    2904           0 :                 return false;
    2905             :             }
    2906             : 
    2907        1731 :             if (type == "vote") {
    2908             :                 // Check that vote is valid
    2909           8 :                 if (!checkVote(userDevice, commit.id, commit.parents[0])) {
    2910           4 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed vote commit {}. Please "
    2911             :                                  "ensure that you are using the latest "
    2912             :                                  "version of Jami, or that one of your contacts is not performing "
    2913             :                                  "any unwanted actions.",
    2914             :                                  accountId_,
    2915             :                                  id_,
    2916             :                                  commit.id);
    2917             : 
    2918           1 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2919           1 :                                                                                  id_,
    2920             :                                                                                  EVALIDFETCH,
    2921             :                                                                                  "Malformed vote");
    2922           1 :                     return false;
    2923             :                 }
    2924        1723 :             } else if (type == "member") {
    2925        1558 :                 std::string action = cm["action"].asString();
    2926        1558 :                 std::string uriMember = cm["uri"].asString();
    2927             : 
    2928        1558 :                 dht::InfoHash h(uriMember);
    2929        1558 :                 if (not h) {
    2930           8 :                     JAMI_WARNING("[Account {}] [Conversation {}] Commit {} with invalid member URI {}. Please ensure "
    2931             :                                  "that you are using the latest version of Jami, or that one of your contacts is not "
    2932             :                                  "performing any unwanted actions.",
    2933             :                                  accountId_,
    2934             :                                  id_,
    2935             :                                  commit.id,
    2936             :                                  uriMember);
    2937             : 
    2938           2 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2939           2 :                                                                                  id_,
    2940             :                                                                                  EVALIDFETCH,
    2941             :                                                                                  "Invalid member URI");
    2942           2 :                     return false;
    2943             :                 }
    2944        1556 :                 if (action == "add") {
    2945         754 :                     if (!checkValidAdd(userDevice, uriMember, commit.id, commit.parents[0])) {
    2946          12 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed add commit {}. Please ensure that you "
    2947             :                                      "are using the latest version of Jami, or that one of your contacts is not "
    2948             :                                      "performing any unwanted actions.",
    2949             :                                      accountId_,
    2950             :                                      id_,
    2951             :                                      commit.id);
    2952             : 
    2953           3 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2954           3 :                                                                                      id_,
    2955             :                                                                                      EVALIDFETCH,
    2956             :                                                                                      "Malformed add member commit");
    2957           3 :                         return false;
    2958             :                     }
    2959         802 :                 } else if (action == "join") {
    2960         791 :                     if (!checkValidJoins(userDevice, uriMember, commit.id, commit.parents[0])) {
    2961          12 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed joins commit {}. "
    2962             :                                      "Please ensure that you are using the latest "
    2963             :                                      "version of Jami, or that one of your contacts is not "
    2964             :                                      "performing any unwanted actions.",
    2965             :                                      accountId_,
    2966             :                                      id_,
    2967             :                                      commit.id);
    2968             : 
    2969           3 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2970           3 :                                                                                      id_,
    2971             :                                                                                      EVALIDFETCH,
    2972             :                                                                                      "Malformed join member commit");
    2973           3 :                         return false;
    2974             :                     }
    2975          11 :                 } else if (action == "remove") {
    2976             :                     // In this case, we remove the user. So if self, the user will not be
    2977             :                     // valid for this commit. Check previous commit
    2978           1 :                     validUserAtCommit = commit.parents[0];
    2979           1 :                     if (!checkValidRemove(userDevice, uriMember, commit.id, commit.parents[0])) {
    2980           0 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
    2981             :                                      "Please ensure that you are using the latest "
    2982             :                                      "version of Jami, or that one of your contacts is not "
    2983             :                                      "performing any unwanted actions.",
    2984             :                                      accountId_,
    2985             :                                      id_,
    2986             :                                      commit.id);
    2987             : 
    2988           0 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    2989           0 :                                                                                      id_,
    2990             :                                                                                      EVALIDFETCH,
    2991             :                                                                                      "Malformed remove member commit");
    2992           0 :                         return false;
    2993             :                     }
    2994          10 :                 } else if (action == "ban" || action == "unban") {
    2995             :                     // Note device.size() == "member".size()
    2996          10 :                     if (!checkValidVoteResolution(userDevice, uriMember, commit.id, commit.parents[0], action)) {
    2997          20 :                         JAMI_WARNING("[Account {}] [Conversation {}] Malformed removes commit {}. "
    2998             :                                      "Please ensure that you are using the latest "
    2999             :                                      "version of Jami, or that one of your contacts is not "
    3000             :                                      "performing any unwanted actions.",
    3001             :                                      accountId_,
    3002             :                                      id_,
    3003             :                                      commit.id);
    3004             : 
    3005           5 :                         emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3006           5 :                                                                                      id_,
    3007             :                                                                                      EVALIDFETCH,
    3008             :                                                                                      "Malformed ban member commit");
    3009           5 :                         return false;
    3010             :                     }
    3011             :                 } else {
    3012           0 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed member commit {} with "
    3013             :                                  "action {}. Please ensure that you are using the latest "
    3014             :                                  "version of Jami, or that one of your contacts is not performing "
    3015             :                                  "any unwanted actions.",
    3016             :                                  accountId_,
    3017             :                                  id_,
    3018             :                                  commit.id,
    3019             :                                  action);
    3020             : 
    3021           0 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3022           0 :                                                                                  id_,
    3023             :                                                                                  EVALIDFETCH,
    3024             :                                                                                  "Malformed member commit");
    3025           0 :                     return false;
    3026             :                 }
    3027        1736 :             } else if (type == "application/update-profile") {
    3028           7 :                 if (!checkValidProfileUpdate(userDevice, commit.id, commit.parents[0])) {
    3029           8 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed profile updates commit "
    3030             :                                  "{}. Please ensure that you are using the latest "
    3031             :                                  "version of Jami, or that one of your contacts is not performing "
    3032             :                                  "any unwanted actions.",
    3033             :                                  accountId_,
    3034             :                                  id_,
    3035             :                                  commit.id);
    3036             : 
    3037           2 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3038           2 :                                                                                  id_,
    3039             :                                                                                  EVALIDFETCH,
    3040             :                                                                                  "Malformed profile updates commit");
    3041           2 :                     return false;
    3042             :                 }
    3043         158 :             } else if (type == "application/edited-message" || !editId.empty()) {
    3044           2 :                 if (!checkEdit(userDevice, commit)) {
    3045           4 :                     JAMI_ERROR("Commit {:s} malformed", commit.id);
    3046             : 
    3047           1 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3048           1 :                                                                                  id_,
    3049             :                                                                                  EVALIDFETCH,
    3050             :                                                                                  "Malformed edit commit");
    3051           1 :                     return false;
    3052             :                 }
    3053             :             } else {
    3054             :                 // Note: accept all mimetype here, as we can have new mimetypes
    3055             :                 // Just avoid to add weird files
    3056             :                 // Check that no weird file is added outside device cert nor removed
    3057         156 :                 if (!checkValidUserDiff(userDevice, commit.id, commit.parents[0])) {
    3058          20 :                     JAMI_WARNING("[Account {}] [Conversation {}] Malformed {} commit {}. Please "
    3059             :                                  "ensure that you are using the latest "
    3060             :                                  "version of Jami, or that one of your contacts is not performing "
    3061             :                                  "any unwanted actions.",
    3062             :                                  accountId_,
    3063             :                                  id_,
    3064             :                                  type,
    3065             :                                  commit.id);
    3066             : 
    3067           5 :                     emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3068           5 :                                                                                  id_,
    3069             :                                                                                  EVALIDFETCH,
    3070             :                                                                                  "Malformed commit");
    3071           5 :                     return false;
    3072             :                 }
    3073             :             }
    3074             :             // For all commits, check that the user is valid.
    3075             :             // So, the user certificate MUST be in /members or /admins
    3076             :             // and device cert MUST be in /devices
    3077        1709 :             if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
    3078           4 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}. Please ensure "
    3079             :                              "that you are using the latest "
    3080             :                              "version of Jami, or that one of your contacts is not performing any "
    3081             :                              "unwanted actions. {}",
    3082             :                              accountId_,
    3083             :                              id_,
    3084             :                              validUserAtCommit,
    3085             :                              commit.commit_msg);
    3086           1 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3087           1 :                                                                              id_,
    3088             :                                                                              EVALIDFETCH,
    3089             :                                                                              "Invalid user");
    3090           1 :                 return false;
    3091             :             }
    3092        1777 :         } else {
    3093             :             // For all commits, check that the user is valid.
    3094             :             // So, the user certificate MUST be in /members or /admins
    3095             :             // and device cert MUST be in /devices
    3096           9 :             if (!isValidUserAtCommit(userDevice, validUserAtCommit, *sig, *sig_data)) {
    3097           0 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed commit {}.Please ensure "
    3098             :                              "that you are using the latest "
    3099             :                              "version of Jami, or that one of your contacts is not performing any "
    3100             :                              "unwanted actions. {}",
    3101             :                              accountId_,
    3102             :                              id_,
    3103             :                              validUserAtCommit,
    3104             :                              commit.commit_msg);
    3105           0 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3106           0 :                                                                              id_,
    3107             :                                                                              EVALIDFETCH,
    3108             :                                                                              "Malformed commit");
    3109           0 :                 return false;
    3110             :             }
    3111             : 
    3112           9 :             if (!checkValidMergeCommit(commit.id, commit.parents)) {
    3113           4 :                 JAMI_WARNING("[Account {}] [Conversation {}] Malformed merge commit {}. Please "
    3114             :                              "ensure that you are using the latest "
    3115             :                              "version of Jami, or that one of your contacts is not performing "
    3116             :                              "any unwanted actions.",
    3117             :                              accountId_,
    3118             :                              id_,
    3119             :                              commit.id);
    3120             : 
    3121           1 :                 emitSignal<libjami::ConversationSignal::OnConversationError>(accountId_,
    3122           1 :                                                                              id_,
    3123             :                                                                              EVALIDFETCH,
    3124             :                                                                              "Malformed merge commit");
    3125           1 :                 return false;
    3126             :             }
    3127             :         }
    3128        7604 :         JAMI_DEBUG("[Account {}] [Conversation {}] Validate commit {}", accountId_, id_, commit.id);
    3129        2005 :     }
    3130        1941 :     return true;
    3131        1967 : }
    3132             : 
    3133             : /////////////////////////////////////////////////////////////////////////////////
    3134             : 
    3135         426 : ConversationRepository::ConversationRepository(const std::shared_ptr<JamiAccount>& account, const std::string& id)
    3136         426 :     : pimpl_ {new Impl {account, id}}
    3137         425 : {}
    3138             : 
    3139         425 : ConversationRepository::~ConversationRepository() = default;
    3140             : 
    3141             : const std::string&
    3142       16021 : ConversationRepository::id() const
    3143             : {
    3144       16021 :     return pimpl_->id_;
    3145             : }
    3146             : 
    3147             : std::string
    3148         135 : ConversationRepository::addMember(const std::string& uri)
    3149             : {
    3150         135 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3151         135 :     pimpl_->resetHard();
    3152         135 :     auto repo = pimpl_->repository();
    3153         135 :     if (not repo)
    3154           0 :         return {};
    3155             : 
    3156             :     // First, we need to add the member file to the repository if not present
    3157         135 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3158             : 
    3159         135 :     std::filesystem::path invitedPath = repoPath / MemberPath::INVITED;
    3160         135 :     if (!dhtnet::fileutils::recursive_mkdir(invitedPath, 0700)) {
    3161           0 :         JAMI_ERROR("Error when creating {}.", invitedPath);
    3162           0 :         return {};
    3163             :     }
    3164         135 :     std::filesystem::path devicePath = invitedPath / uri;
    3165         135 :     if (std::filesystem::is_regular_file(devicePath)) {
    3166           0 :         JAMI_WARNING("Member {} already present!", uri);
    3167           0 :         return {};
    3168             :     }
    3169             : 
    3170         135 :     std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    3171         135 :     if (!file.is_open()) {
    3172           0 :         JAMI_ERROR("Unable to write data to {}", devicePath);
    3173           0 :         return {};
    3174             :     }
    3175         135 :     std::string path = "invited/" + uri;
    3176         135 :     if (!pimpl_->add(path))
    3177           0 :         return {};
    3178             : 
    3179             :     {
    3180         135 :         std::lock_guard lk(pimpl_->membersMtx_);
    3181         135 :         pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::INVITED});
    3182         135 :         pimpl_->saveMembers();
    3183         135 :     }
    3184             : 
    3185         135 :     Json::Value json;
    3186         135 :     json["action"] = "add";
    3187         135 :     json["uri"] = uri;
    3188         135 :     json["type"] = "member";
    3189         270 :     return pimpl_->commit(json::toString(json));
    3190         135 : }
    3191             : 
    3192             : void
    3193         389 : ConversationRepository::onMembersChanged(OnMembersChanged&& cb)
    3194             : {
    3195         389 :     pimpl_->onMembersChanged_ = std::move(cb);
    3196         389 : }
    3197             : 
    3198             : std::string
    3199           1 : ConversationRepository::amend(const std::string& id, const std::string& msg)
    3200             : {
    3201           1 :     GitSignature sig = pimpl_->signature();
    3202           1 :     if (!sig)
    3203           0 :         return {};
    3204             : 
    3205             :     git_oid tree_id, commit_id;
    3206           1 :     git_commit* commit_ptr = nullptr;
    3207           1 :     auto repo = pimpl_->repository();
    3208           1 :     if (!repo || git_oid_fromstr(&tree_id, id.c_str()) < 0 || git_commit_lookup(&commit_ptr, repo.get(), &tree_id) < 0) {
    3209           0 :         GitCommit commit {commit_ptr};
    3210           0 :         JAMI_WARNING("Failed to look up commit {}", id);
    3211           0 :         return {};
    3212           0 :     }
    3213           1 :     GitCommit commit {commit_ptr};
    3214             : 
    3215           1 :     if (git_commit_amend(&commit_id, commit.get(), nullptr, sig.get(), sig.get(), nullptr, msg.c_str(), nullptr) < 0) {
    3216           0 :         if (const git_error* err = giterr_last())
    3217           0 :             JAMI_ERROR("Unable to amend commit: {}", err->message);
    3218           0 :         return {};
    3219             :     }
    3220             : 
    3221             :     // Move commit to main branch
    3222           1 :     git_reference* ref_ptr = nullptr;
    3223           1 :     if (git_reference_create(&ref_ptr, repo.get(), "refs/heads/main", &commit_id, true, nullptr) < 0) {
    3224           0 :         if (const git_error* err = giterr_last()) {
    3225           0 :             JAMI_ERROR("Unable to move commit to main: {}", err->message);
    3226           0 :             emitSignal<libjami::ConversationSignal::OnConversationError>(pimpl_->accountId_,
    3227           0 :                                                                          pimpl_->id_,
    3228             :                                                                          ECOMMIT,
    3229           0 :                                                                          err->message);
    3230             :         }
    3231           0 :         return {};
    3232             :     }
    3233           1 :     git_reference_free(ref_ptr);
    3234             : 
    3235           1 :     auto commit_str = git_oid_tostr_s(&commit_id);
    3236           1 :     if (commit_str) {
    3237           4 :         JAMI_DEBUG("Commit {} amended (new ID: {})", id, commit_str);
    3238           1 :         return commit_str;
    3239             :     }
    3240           0 :     return {};
    3241           1 : }
    3242             : 
    3243             : bool
    3244        1800 : ConversationRepository::fetch(const std::string& remoteDeviceId)
    3245             : {
    3246        1800 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3247        1800 :     pimpl_->resetHard();
    3248             :     // Fetch distant repository
    3249        1800 :     git_remote* remote_ptr = nullptr;
    3250             :     git_fetch_options fetch_opts;
    3251        1800 :     git_fetch_options_init(&fetch_opts, GIT_FETCH_OPTIONS_VERSION);
    3252        1800 :     fetch_opts.follow_redirects = GIT_REMOTE_REDIRECT_NONE;
    3253             : 
    3254        1800 :     LogOptions options;
    3255        1799 :     options.nbOfCommits = 1;
    3256        1799 :     auto lastMsg = log(options);
    3257        1800 :     if (lastMsg.size() == 0)
    3258           1 :         return false;
    3259        1799 :     auto lastCommit = lastMsg[0].id;
    3260             : 
    3261             :     // Assert that repository exists
    3262        1799 :     auto repo = pimpl_->repository();
    3263        1799 :     if (!repo)
    3264           0 :         return false;
    3265        1799 :     auto res = git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str());
    3266        1799 :     if (res != 0) {
    3267         726 :         if (res != GIT_ENOTFOUND) {
    3268           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up for remote {}",
    3269             :                        pimpl_->accountId_,
    3270             :                        pimpl_->id_,
    3271             :                        remoteDeviceId);
    3272           0 :             return false;
    3273             :         }
    3274         726 :         std::string channelName = fmt::format("git://{}/{}", remoteDeviceId, pimpl_->id_);
    3275         726 :         if (git_remote_create(&remote_ptr, repo.get(), remoteDeviceId.c_str(), channelName.c_str()) < 0) {
    3276           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to create remote for repository",
    3277             :                        pimpl_->accountId_,
    3278             :                        pimpl_->id_);
    3279           0 :             return false;
    3280             :         }
    3281         726 :     }
    3282        1799 :     GitRemote remote {remote_ptr};
    3283             : 
    3284       33717 :     fetch_opts.callbacks.transfer_progress = [](const git_indexer_progress* stats, void*) {
    3285             :         // Uncomment to get advancment
    3286             :         // if (stats->received_objects % 500 == 0 || stats->received_objects == stats->total_objects)
    3287             :         //     JAMI_DEBUG("{}/{} {}kb", stats->received_objects, stats->total_objects,
    3288             :         //     stats->received_bytes/1024);
    3289             :         // If a pack is more than 256Mb, it's anormal.
    3290       31918 :         if (stats->received_bytes > MAX_FETCH_SIZE) {
    3291           0 :             JAMI_ERROR("Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
    3292             :                        stats->received_bytes,
    3293             :                        stats->received_objects,
    3294             :                        stats->total_objects);
    3295           0 :             return -1;
    3296             :         }
    3297       31918 :         return 0;
    3298        1799 :     };
    3299        1799 :     if (git_remote_fetch(remote.get(), nullptr, &fetch_opts, "fetch") < 0) {
    3300          25 :         const git_error* err = giterr_last();
    3301          25 :         if (err) {
    3302         100 :             JAMI_WARNING("[Account {}] [Conversation {}] Unable to fetch remote repository: {:s}",
    3303             :                          pimpl_->accountId_,
    3304             :                          pimpl_->id_,
    3305             :                          err->message);
    3306             :         }
    3307          25 :         return false;
    3308             :     }
    3309             : 
    3310        1774 :     return true;
    3311        1800 : }
    3312             : 
    3313             : std::vector<std::map<std::string, std::string>>
    3314        1774 : ConversationRepository::mergeHistory(const std::string& uri,
    3315             :                                      std::function<void(const std::string&)>&& disconnectFromPeerCb)
    3316             : {
    3317        3548 :     auto remoteHeadRes = remoteHead(uri);
    3318        1774 :     if (remoteHeadRes.empty()) {
    3319           4 :         JAMI_WARNING("[Account {}] [Conversation {}] Unable to get HEAD of {}", pimpl_->accountId_, pimpl_->id_, uri);
    3320           1 :         return {};
    3321             :     }
    3322             : 
    3323             :     // Validate commit
    3324        1773 :     auto [newCommits, err] = validFetch(uri);
    3325        1773 :     if (newCommits.empty()) {
    3326         866 :         if (err)
    3327          68 :             JAMI_ERROR("[Account {}] [Conversation {}] Unable to validate history with {}",
    3328             :                        pimpl_->accountId_,
    3329             :                        pimpl_->id_,
    3330             :                        uri);
    3331         866 :         removeBranchWith(uri);
    3332         866 :         return {};
    3333             :     }
    3334             : 
    3335             :     // If validated, merge
    3336         907 :     auto [ok, cid] = merge(remoteHeadRes);
    3337         907 :     if (!ok) {
    3338           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to merge history with {}",
    3339             :                    pimpl_->accountId_,
    3340             :                    pimpl_->id_,
    3341             :                    uri);
    3342           0 :         removeBranchWith(uri);
    3343           0 :         return {};
    3344             :     }
    3345         907 :     if (!cid.empty()) {
    3346             :         // A merge commit was generated, should be added in new commits
    3347          19 :         auto commit = getCommit(cid);
    3348          19 :         if (commit != std::nullopt)
    3349          19 :             newCommits.emplace_back(*commit);
    3350          19 :     }
    3351             : 
    3352        3628 :     JAMI_LOG("[Account {}] [Conversation {}] Successfully merged history with {}", pimpl_->accountId_, pimpl_->id_, uri);
    3353         907 :     auto result = convCommitsToMap(newCommits);
    3354        1847 :     for (auto& commit : result) {
    3355         940 :         auto it = commit.find("type");
    3356         940 :         if (it != commit.end() && it->second == "member") {
    3357         774 :             refreshMembers();
    3358             : 
    3359         774 :             if (commit["action"] == "ban")
    3360           5 :                 disconnectFromPeerCb(commit["uri"]);
    3361             :         }
    3362             :     }
    3363         907 :     return result;
    3364        1774 : }
    3365             : 
    3366             : std::string
    3367        3547 : ConversationRepository::remoteHead(const std::string& remoteDeviceId, const std::string& branch) const
    3368             : {
    3369        3547 :     git_remote* remote_ptr = nullptr;
    3370        3547 :     auto repo = pimpl_->repository();
    3371        3547 :     if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDeviceId.c_str()) < 0) {
    3372           4 :         JAMI_WARNING("No remote found with ID: {}", remoteDeviceId);
    3373           1 :         return {};
    3374             :     }
    3375        3546 :     GitRemote remote {remote_ptr};
    3376             : 
    3377        3545 :     git_reference* head_ref_ptr = nullptr;
    3378        7090 :     std::string remoteHead = "refs/remotes/" + remoteDeviceId + "/" + branch;
    3379             :     git_oid commit_id;
    3380        3545 :     if (git_reference_name_to_id(&commit_id, repo.get(), remoteHead.c_str()) < 0) {
    3381           0 :         const git_error* err = giterr_last();
    3382           0 :         if (err)
    3383           0 :             JAMI_ERROR("failed to look up {} ref: {}", remoteHead, err->message);
    3384           0 :         return {};
    3385             :     }
    3386        3546 :     GitReference head_ref {head_ref_ptr};
    3387             : 
    3388        3545 :     auto commit_str = git_oid_tostr_s(&commit_id);
    3389        3546 :     if (!commit_str)
    3390           0 :         return {};
    3391        3546 :     return commit_str;
    3392        3547 : }
    3393             : 
    3394             : void
    3395         350 : ConversationRepository::Impl::addUserDevice()
    3396             : {
    3397         350 :     auto account = account_.lock();
    3398         350 :     if (!account)
    3399           0 :         return;
    3400             : 
    3401             :     // First, we need to add device file to the repository if not present
    3402         350 :     auto repo = repository();
    3403         350 :     if (!repo)
    3404           0 :         return;
    3405             :     // NOTE: libgit2 uses / for files
    3406         350 :     std::string path = fmt::format("devices/{}.crt", deviceId_);
    3407         350 :     std::filesystem::path devicePath = git_repository_workdir(repo.get()) + path;
    3408         350 :     if (!std::filesystem::is_regular_file(devicePath)) {
    3409         161 :         std::ofstream file(devicePath, std::ios::trunc | std::ios::binary);
    3410         161 :         if (!file.is_open()) {
    3411           0 :             JAMI_ERROR("Unable to write data to {}", devicePath);
    3412           0 :             return;
    3413             :         }
    3414         161 :         auto cert = account->identity().second;
    3415         161 :         auto deviceCert = cert->toString(false);
    3416         161 :         file << deviceCert;
    3417         161 :         file.close();
    3418             : 
    3419         161 :         if (!add(path))
    3420           0 :             JAMI_WARNING("Unable to add file {}", devicePath);
    3421         161 :     }
    3422         350 : }
    3423             : 
    3424             : void
    3425        3234 : ConversationRepository::Impl::resetHard()
    3426             : {
    3427             : #ifdef LIBJAMI_TEST
    3428        3234 :     if (DISABLE_RESET)
    3429         465 :         return;
    3430             : #endif
    3431        2769 :     auto repo = repository();
    3432        2769 :     if (!repo)
    3433           0 :         return;
    3434        2769 :     git_object* head_commit_obj = nullptr;
    3435        2769 :     auto error = git_revparse_single(&head_commit_obj, repo.get(), "HEAD");
    3436        2769 :     if (error < 0) {
    3437           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get HEAD commit: {}", accountId_, id_, error);
    3438           0 :         return;
    3439             :     }
    3440        2769 :     GitObject target {head_commit_obj};
    3441        2769 :     git_reset(repo.get(), head_commit_obj, GIT_RESET_HARD, nullptr);
    3442        2769 : }
    3443             : 
    3444             : std::string
    3445         160 : ConversationRepository::commitMessage(const std::string& msg, bool verifyDevice)
    3446             : {
    3447         160 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3448         160 :     pimpl_->resetHard();
    3449         320 :     return pimpl_->commitMessage(msg, verifyDevice);
    3450         160 : }
    3451             : 
    3452             : std::string
    3453         350 : ConversationRepository::Impl::commitMessage(const std::string& msg, bool verifyDevice)
    3454             : {
    3455         350 :     addUserDevice();
    3456         350 :     return commit(msg, verifyDevice);
    3457             : }
    3458             : 
    3459             : std::vector<std::string>
    3460           0 : ConversationRepository::commitMessages(const std::vector<std::string>& msgs)
    3461             : {
    3462           0 :     pimpl_->addUserDevice();
    3463           0 :     std::vector<std::string> ret;
    3464           0 :     ret.reserve(msgs.size());
    3465           0 :     for (const auto& msg : msgs)
    3466           0 :         ret.emplace_back(pimpl_->commit(msg));
    3467           0 :     return ret;
    3468           0 : }
    3469             : 
    3470             : std::vector<ConversationCommit>
    3471        2001 : ConversationRepository::log(const LogOptions& options) const
    3472             : {
    3473        2001 :     return pimpl_->log(options);
    3474             : }
    3475             : 
    3476             : void
    3477        1517 : ConversationRepository::log(PreConditionCb&& preCondition,
    3478             :                             std::function<void(ConversationCommit&&)>&& emplaceCb,
    3479             :                             PostConditionCb&& postCondition,
    3480             :                             const std::string& from,
    3481             :                             bool logIfNotFound) const
    3482             : {
    3483        1517 :     pimpl_->forEachCommit(std::move(preCondition), std::move(emplaceCb), std::move(postCondition), from, logIfNotFound);
    3484        1517 : }
    3485             : 
    3486             : std::optional<ConversationCommit>
    3487       14009 : ConversationRepository::getCommit(const std::string& commitId, bool logIfNotFound) const
    3488             : {
    3489       14009 :     return pimpl_->getCommit(commitId, logIfNotFound);
    3490             : }
    3491             : 
    3492             : std::pair<bool, std::string>
    3493         910 : ConversationRepository::merge(const std::string& merge_id, bool force)
    3494             : {
    3495         910 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3496         910 :     pimpl_->resetHard();
    3497             :     // First, the repository must be in a clean state
    3498         910 :     auto repo = pimpl_->repository();
    3499         910 :     if (!repo) {
    3500           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to merge without repo", pimpl_->accountId_, pimpl_->id_);
    3501           0 :         return {false, ""};
    3502             :     }
    3503         910 :     int state = git_repository_state(repo.get());
    3504         910 :     if (state != GIT_REPOSITORY_STATE_NONE) {
    3505           0 :         pimpl_->resetHard();
    3506           0 :         int state = git_repository_state(repo.get());
    3507           0 :         if (state != GIT_REPOSITORY_STATE_NONE) {
    3508           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository is in unexpected state {}",
    3509             :                        pimpl_->accountId_,
    3510             :                        pimpl_->id_,
    3511             :                        state);
    3512           0 :             return {false, ""};
    3513             :         }
    3514             :     }
    3515             :     // Checkout main (to do a `git_merge branch`)
    3516         910 :     if (git_repository_set_head(repo.get(), "refs/heads/main") < 0) {
    3517           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to checkout main branch",
    3518             :                    pimpl_->accountId_,
    3519             :                    pimpl_->id_);
    3520           0 :         return {false, ""};
    3521             :     }
    3522             : 
    3523             :     // Then check that merge_id exists
    3524             :     git_oid commit_id;
    3525         910 :     if (git_oid_fromstr(&commit_id, merge_id.c_str()) < 0) {
    3526           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}",
    3527             :                    pimpl_->accountId_,
    3528             :                    pimpl_->id_,
    3529             :                    merge_id);
    3530           0 :         return {false, ""};
    3531             :     }
    3532         910 :     git_annotated_commit* annotated_ptr = nullptr;
    3533         910 :     if (git_annotated_commit_lookup(&annotated_ptr, repo.get(), &commit_id) < 0) {
    3534           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: unable to look up commit {}",
    3535             :                    pimpl_->accountId_,
    3536             :                    pimpl_->id_,
    3537             :                    merge_id);
    3538           0 :         return {false, ""};
    3539             :     }
    3540         910 :     GitAnnotatedCommit annotated {annotated_ptr};
    3541             : 
    3542             :     // Now, we can analyze the type of merge required
    3543             :     git_merge_analysis_t analysis;
    3544             :     git_merge_preference_t preference;
    3545         910 :     const git_annotated_commit* const_annotated = annotated.get();
    3546         910 :     if (git_merge_analysis(&analysis, &preference, repo.get(), &const_annotated, 1) < 0) {
    3547           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Merge operation aborted: repository analysis failed",
    3548             :                    pimpl_->accountId_,
    3549             :                    pimpl_->id_);
    3550           0 :         return {false, ""};
    3551             :     }
    3552             : 
    3553             :     // Handle easy merges
    3554         910 :     if (analysis & GIT_MERGE_ANALYSIS_UP_TO_DATE) {
    3555           0 :         JAMI_LOG("Already up-to-date");
    3556           0 :         return {true, ""};
    3557         910 :     } else if (analysis & GIT_MERGE_ANALYSIS_UNBORN
    3558         910 :                || (analysis & GIT_MERGE_ANALYSIS_FASTFORWARD && !(preference & GIT_MERGE_PREFERENCE_NO_FASTFORWARD))) {
    3559         889 :         if (analysis & GIT_MERGE_ANALYSIS_UNBORN)
    3560           0 :             JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Unborn", pimpl_->accountId_, pimpl_->id_);
    3561             :         else
    3562        3556 :             JAMI_LOG("[Account {}] [Conversation {}] Merge analysis result: Fast-forward",
    3563             :                      pimpl_->accountId_,
    3564             :                      pimpl_->id_);
    3565         889 :         const auto* target_oid = git_annotated_commit_id(annotated.get());
    3566             : 
    3567         889 :         if (!pimpl_->mergeFastforward(target_oid, (analysis & GIT_MERGE_ANALYSIS_UNBORN))) {
    3568           0 :             const git_error* err = giterr_last();
    3569           0 :             if (err)
    3570           0 :                 JAMI_ERROR("[Account {}] [Conversation {}] Fast forward merge failed: {}",
    3571             :                            pimpl_->accountId_,
    3572             :                            pimpl_->id_,
    3573             :                            err->message);
    3574           0 :             return {false, ""};
    3575             :         }
    3576         889 :         return {true, ""}; // fast forward so no commit generated;
    3577             :     }
    3578             : 
    3579          21 :     if (!pimpl_->validateDevice() && !force) {
    3580           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Invalid device. Not migrated?", pimpl_->accountId_, pimpl_->id_);
    3581           0 :         return {false, ""};
    3582             :     }
    3583             : 
    3584             :     // Else we want to check for conflicts
    3585             :     git_oid head_commit_id;
    3586          21 :     if (git_reference_name_to_id(&head_commit_id, repo.get(), "HEAD") < 0) {
    3587           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to get reference for HEAD", pimpl_->accountId_, pimpl_->id_);
    3588           0 :         return {false, ""};
    3589             :     }
    3590             : 
    3591          21 :     git_commit* head_ptr = nullptr;
    3592          21 :     if (git_commit_lookup(&head_ptr, repo.get(), &head_commit_id) < 0) {
    3593           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
    3594           0 :         return {false, ""};
    3595             :     }
    3596          21 :     GitCommit head_commit {head_ptr};
    3597             : 
    3598          21 :     git_commit* other__ptr = nullptr;
    3599          21 :     if (git_commit_lookup(&other__ptr, repo.get(), &commit_id) < 0) {
    3600           0 :         JAMI_ERROR("[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
    3601           0 :         return {false, ""};
    3602             :     }
    3603          21 :     GitCommit other_commit {other__ptr};
    3604             : 
    3605             :     git_merge_options merge_opts;
    3606          21 :     git_merge_options_init(&merge_opts, GIT_MERGE_OPTIONS_VERSION);
    3607          21 :     merge_opts.recursion_limit = 2;
    3608          21 :     git_index* index_ptr = nullptr;
    3609          21 :     if (git_merge_commits(&index_ptr, repo.get(), head_commit.get(), other_commit.get(), &merge_opts) < 0) {
    3610           0 :         const git_error* err = giterr_last();
    3611           0 :         if (err)
    3612           0 :             JAMI_ERROR("[Account {}] [Conversation {}] Git merge failed: {}",
    3613             :                        pimpl_->accountId_,
    3614             :                        pimpl_->id_,
    3615             :                        err->message);
    3616           0 :         return {false, ""};
    3617             :     }
    3618          21 :     GitIndex index {index_ptr};
    3619          21 :     if (git_index_has_conflicts(index.get())) {
    3620           4 :         JAMI_LOG("Some conflicts were detected during the merge operations. Resolution phase.");
    3621           1 :         if (!pimpl_->resolveConflicts(index.get(), merge_id) or !git_add_all(repo.get())) {
    3622           0 :             JAMI_ERROR("Merge operation aborted; Unable to automatically resolve conflicts");
    3623           0 :             return {false, ""};
    3624             :         }
    3625             :     }
    3626          21 :     auto result = pimpl_->createMergeCommit(index.get(), merge_id);
    3627          84 :     JAMI_LOG("Merge done between {} and main", merge_id);
    3628             : 
    3629          21 :     return {!result.empty(), result};
    3630         910 : }
    3631             : 
    3632             : std::string
    3633         909 : ConversationRepository::diffStats(const std::string& newId, const std::string& oldId) const
    3634             : {
    3635         909 :     return pimpl_->diffStats(newId, oldId);
    3636             : }
    3637             : 
    3638             : std::vector<std::string>
    3639        2821 : ConversationRepository::changedFiles(std::string_view diffStats)
    3640             : {
    3641        2821 :     static const std::regex re(" +\\| +[0-9]+.*");
    3642        2821 :     std::vector<std::string> changedFiles;
    3643        2821 :     std::string_view line;
    3644       10959 :     while (jami::getline(diffStats, line)) {
    3645        8139 :         std::svmatch match;
    3646        8139 :         if (!std::regex_search(line, match, re) && match.size() == 0)
    3647        2820 :             continue;
    3648        5319 :         changedFiles.emplace_back(std::regex_replace(std::string {line}, re, "").substr(1));
    3649        8139 :     }
    3650        5642 :     return changedFiles;
    3651           0 : }
    3652             : 
    3653             : std::string
    3654         186 : ConversationRepository::join()
    3655             : {
    3656         186 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3657         186 :     pimpl_->resetHard();
    3658             :     // Check that not already member
    3659         186 :     auto repo = pimpl_->repository();
    3660         186 :     if (!repo)
    3661           0 :         return {};
    3662         186 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3663         186 :     auto account = pimpl_->account_.lock();
    3664         186 :     if (!account)
    3665           0 :         return {};
    3666         186 :     auto cert = account->identity().second;
    3667         186 :     auto parentCert = cert->issuer;
    3668         186 :     if (!parentCert) {
    3669           0 :         JAMI_ERROR("Parent cert is null!");
    3670           0 :         return {};
    3671             :     }
    3672         186 :     auto uri = parentCert->getId().toString();
    3673         186 :     auto membersPath = repoPath / MemberPath::MEMBERS;
    3674         372 :     auto memberFile = membersPath / (uri + ".crt");
    3675         372 :     auto adminsPath = repoPath / MemberPath::ADMINS / (uri + ".crt");
    3676         186 :     if (std::filesystem::is_regular_file(memberFile) or std::filesystem::is_regular_file(adminsPath)) {
    3677             :         // Already member, nothing to commit
    3678          30 :         return {};
    3679             :     }
    3680             :     // Remove invited/uri.crt
    3681         156 :     auto invitedPath = repoPath / MemberPath::INVITED;
    3682         156 :     dhtnet::fileutils::remove(fileutils::getFullPath(invitedPath, uri));
    3683             :     // Add members/uri.crt
    3684         156 :     if (!dhtnet::fileutils::recursive_mkdir(membersPath, 0700)) {
    3685           0 :         JAMI_ERROR("Error when creating {}. Abort create conversations", membersPath);
    3686           0 :         return {};
    3687             :     }
    3688         156 :     std::ofstream file(memberFile, std::ios::trunc | std::ios::binary);
    3689         156 :     if (!file.is_open()) {
    3690           0 :         JAMI_ERROR("Unable to write data to {}", memberFile);
    3691           0 :         return {};
    3692             :     }
    3693         156 :     file << parentCert->toString(true);
    3694         156 :     file.close();
    3695             :     // git add -A
    3696         156 :     if (!git_add_all(repo.get())) {
    3697           0 :         return {};
    3698             :     }
    3699         156 :     Json::Value json;
    3700         156 :     json["action"] = "join";
    3701         156 :     json["uri"] = uri;
    3702         156 :     json["type"] = "member";
    3703             : 
    3704             :     {
    3705         156 :         std::lock_guard lk(pimpl_->membersMtx_);
    3706         156 :         auto updated = false;
    3707             : 
    3708         631 :         for (auto& member : pimpl_->members_) {
    3709         631 :             if (member.uri == uri) {
    3710         156 :                 updated = true;
    3711         156 :                 member.role = MemberRole::MEMBER;
    3712         156 :                 break;
    3713             :             }
    3714             :         }
    3715         156 :         if (!updated)
    3716           0 :             pimpl_->members_.emplace_back(ConversationMember {uri, MemberRole::MEMBER});
    3717         156 :         pimpl_->saveMembers();
    3718         156 :     }
    3719             : 
    3720         312 :     return pimpl_->commitMessage(json::toString(json));
    3721         186 : }
    3722             : 
    3723             : std::string
    3724           7 : ConversationRepository::leave()
    3725             : {
    3726           7 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3727           7 :     pimpl_->resetHard();
    3728             :     // TODO: simplify
    3729           7 :     auto account = pimpl_->account_.lock();
    3730           7 :     auto repo = pimpl_->repository();
    3731           7 :     if (!account || !repo)
    3732           0 :         return {};
    3733             : 
    3734             :     // Remove related files
    3735           7 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3736           7 :     auto crt = fmt::format("{}.crt", pimpl_->userId_);
    3737          14 :     auto adminFile = repoPath / MemberPath::ADMINS / crt;
    3738          14 :     auto memberFile = repoPath / MemberPath::MEMBERS / crt;
    3739           7 :     auto crlsPath = repoPath / "CRLs";
    3740           7 :     std::error_code ec;
    3741             : 
    3742           7 :     if (std::filesystem::is_regular_file(adminFile, ec)) {
    3743           7 :         std::filesystem::remove(adminFile, ec);
    3744             :     }
    3745             : 
    3746           7 :     if (std::filesystem::is_regular_file(memberFile, ec)) {
    3747           0 :         std::filesystem::remove(memberFile, ec);
    3748             :     }
    3749             : 
    3750             :     // /CRLs
    3751           7 :     for (const auto& crl : account->identity().second->getRevocationLists()) {
    3752           0 :         if (!crl)
    3753           0 :             continue;
    3754           0 :         auto crlPath = crlsPath / pimpl_->deviceId_ / fmt::format("{}.crl", dht::toHex(crl->getNumber()));
    3755           0 :         if (std::filesystem::is_regular_file(crlPath, ec)) {
    3756           0 :             std::filesystem::remove(crlPath, ec);
    3757             :         }
    3758           7 :     }
    3759             : 
    3760             :     // Devices
    3761          29 :     for (const auto& certificate : std::filesystem::directory_iterator(repoPath / "devices", ec)) {
    3762           8 :         if (certificate.is_regular_file(ec)) {
    3763             :             try {
    3764          16 :                 crypto::Certificate cert(fileutils::loadFile(certificate.path()));
    3765           8 :                 if (cert.getIssuerUID() == pimpl_->userId_)
    3766           7 :                     std::filesystem::remove(certificate.path(), ec);
    3767           8 :             } catch (...) {
    3768           0 :                 continue;
    3769           0 :             }
    3770             :         }
    3771           7 :     }
    3772             : 
    3773           7 :     if (!git_add_all(repo.get())) {
    3774           0 :         return {};
    3775             :     }
    3776             : 
    3777           7 :     Json::Value json;
    3778           7 :     json["action"] = "remove";
    3779           7 :     json["uri"] = pimpl_->userId_;
    3780           7 :     json["type"] = "member";
    3781             : 
    3782             :     {
    3783           7 :         std::lock_guard lk(pimpl_->membersMtx_);
    3784          21 :         pimpl_->members_.erase(std::remove_if(pimpl_->members_.begin(),
    3785           7 :                                               pimpl_->members_.end(),
    3786           9 :                                               [&](auto& member) { return member.uri == pimpl_->userId_; }),
    3787           7 :                                pimpl_->members_.end());
    3788           7 :         pimpl_->saveMembers();
    3789           7 :     }
    3790             : 
    3791          14 :     return pimpl_->commit(json::toString(json), false);
    3792           7 : }
    3793             : 
    3794             : void
    3795          25 : ConversationRepository::erase()
    3796             : {
    3797             :     // First, we need to add the member file to the repository if not present
    3798          25 :     if (auto repo = pimpl_->repository()) {
    3799          25 :         std::string repoPath = git_repository_workdir(repo.get());
    3800         100 :         JAMI_LOG("Erasing {}", repoPath);
    3801          25 :         dhtnet::fileutils::removeAll(repoPath, true);
    3802          50 :     }
    3803          25 : }
    3804             : 
    3805             : ConversationMode
    3806        5095 : ConversationRepository::mode() const
    3807             : {
    3808        5095 :     return pimpl_->mode();
    3809             : }
    3810             : 
    3811             : std::string
    3812          13 : ConversationRepository::voteKick(const std::string& uri, const std::string& type)
    3813             : {
    3814          13 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3815          13 :     pimpl_->resetHard();
    3816          13 :     auto repo = pimpl_->repository();
    3817          13 :     auto account = pimpl_->account_.lock();
    3818          13 :     if (!account || !repo)
    3819           0 :         return {};
    3820          13 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3821          13 :     auto cert = account->identity().second;
    3822          13 :     if (!cert || !cert->issuer)
    3823           0 :         return {};
    3824          13 :     auto adminUri = cert->issuer->getId().toString();
    3825          13 :     if (adminUri == uri) {
    3826           4 :         JAMI_WARNING("Admin tried to ban theirself");
    3827           1 :         return {};
    3828             :     }
    3829             : 
    3830          24 :     auto oldFile = repoPath / type / (uri + (type != "invited" ? ".crt" : ""));
    3831          12 :     if (!std::filesystem::is_regular_file(oldFile)) {
    3832           0 :         JAMI_WARNING("Didn't found file for {} with type {}", uri, type);
    3833           0 :         return {};
    3834             :     }
    3835             : 
    3836           0 :     auto relativeVotePath = fmt::format("votes/ban/{}/{}", type, uri);
    3837          12 :     auto voteDirectory = repoPath / relativeVotePath;
    3838          12 :     if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
    3839           0 :         JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
    3840           0 :         return {};
    3841             :     }
    3842          12 :     auto votePath = fileutils::getFullPath(voteDirectory, adminUri);
    3843          12 :     std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
    3844          12 :     if (!voteFile.is_open()) {
    3845           0 :         JAMI_ERROR("Unable to write data to {}", votePath);
    3846           0 :         return {};
    3847             :     }
    3848          12 :     voteFile.close();
    3849             : 
    3850           0 :     auto toAdd = fmt::format("{}/{}", relativeVotePath, adminUri);
    3851          12 :     if (!pimpl_->add(toAdd))
    3852           0 :         return {};
    3853             : 
    3854          12 :     Json::Value json;
    3855          12 :     json["uri"] = uri;
    3856          12 :     json["type"] = "vote";
    3857          24 :     return pimpl_->commitMessage(json::toString(json));
    3858          13 : }
    3859             : 
    3860             : std::string
    3861           1 : ConversationRepository::voteUnban(const std::string& uri, const std::string_view type)
    3862             : {
    3863           1 :     std::lock_guard lkOp(pimpl_->opMtx_);
    3864           1 :     pimpl_->resetHard();
    3865           1 :     auto repo = pimpl_->repository();
    3866           1 :     auto account = pimpl_->account_.lock();
    3867           1 :     if (!account || !repo)
    3868           0 :         return {};
    3869           1 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3870           1 :     auto cert = account->identity().second;
    3871           1 :     if (!cert || !cert->issuer)
    3872           0 :         return {};
    3873           1 :     auto adminUri = cert->issuer->getId().toString();
    3874             : 
    3875           0 :     auto relativeVotePath = fmt::format("votes/unban/{}/{}", type, uri);
    3876           1 :     auto voteDirectory = repoPath / relativeVotePath;
    3877           1 :     if (!dhtnet::fileutils::recursive_mkdir(voteDirectory, 0700)) {
    3878           0 :         JAMI_ERROR("Error when creating {}. Abort vote", voteDirectory);
    3879           0 :         return {};
    3880             :     }
    3881           1 :     auto votePath = voteDirectory / adminUri;
    3882           1 :     std::ofstream voteFile(votePath, std::ios::trunc | std::ios::binary);
    3883           1 :     if (!voteFile.is_open()) {
    3884           0 :         JAMI_ERROR("Unable to write data to {}", votePath);
    3885           0 :         return {};
    3886             :     }
    3887           1 :     voteFile.close();
    3888             : 
    3889           2 :     auto toAdd = fileutils::getFullPath(relativeVotePath, adminUri).string();
    3890           1 :     if (!pimpl_->add(toAdd.c_str()))
    3891           0 :         return {};
    3892             : 
    3893           1 :     Json::Value json;
    3894           1 :     json["uri"] = uri;
    3895           1 :     json["type"] = "vote";
    3896           2 :     return pimpl_->commitMessage(json::toString(json));
    3897           1 : }
    3898             : 
    3899             : bool
    3900          12 : ConversationRepository::Impl::resolveBan(const std::string_view type, const std::string& uri)
    3901             : {
    3902          12 :     auto repo = repository();
    3903          12 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3904          12 :     auto bannedPath = repoPath / "banned";
    3905          12 :     auto devicesPath = repoPath / "devices";
    3906             :     // Move from device or members file into banned
    3907          12 :     auto crtStr = uri + (type != "invited" ? ".crt" : "");
    3908          24 :     auto originFilePath = repoPath / type / crtStr;
    3909             : 
    3910          12 :     auto destPath = bannedPath / type;
    3911          12 :     auto destFilePath = destPath / crtStr;
    3912          12 :     if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
    3913           0 :         JAMI_ERROR("An error occurred while creating the {} directory. Abort resolving vote.", destPath);
    3914           0 :         return false;
    3915             :     }
    3916             : 
    3917          12 :     std::error_code ec;
    3918          12 :     std::filesystem::rename(originFilePath, destFilePath, ec);
    3919          12 :     if (ec) {
    3920           0 :         JAMI_ERROR("An error occurred while moving the {} origin file path to the {} destination "
    3921             :                    "file path. Abort resolving vote.",
    3922             :                    originFilePath,
    3923             :                    destFilePath);
    3924           0 :         return false;
    3925             :     }
    3926             : 
    3927             :     // If members, remove related devices and mark as banned
    3928          12 :     if (type != "devices") {
    3929          12 :         std::error_code ec;
    3930          64 :         for (const auto& certificate : std::filesystem::directory_iterator(devicesPath, ec)) {
    3931          28 :             auto certPath = certificate.path();
    3932             :             try {
    3933          56 :                 crypto::Certificate cert(fileutils::loadFile(certPath));
    3934          28 :                 if (auto issuer = cert.issuer)
    3935           0 :                     if (issuer->getPublicKey().getId().to_view() == uri)
    3936          28 :                         dhtnet::fileutils::remove(certPath, true);
    3937          28 :             } catch (...) {
    3938           0 :                 continue;
    3939           0 :             }
    3940          40 :         }
    3941          12 :         std::lock_guard lk(membersMtx_);
    3942          12 :         auto updated = false;
    3943             : 
    3944          28 :         for (auto& member : members_) {
    3945          28 :             if (member.uri == uri) {
    3946          12 :                 updated = true;
    3947          12 :                 member.role = MemberRole::BANNED;
    3948          12 :                 break;
    3949             :             }
    3950             :         }
    3951          12 :         if (!updated)
    3952           0 :             members_.emplace_back(ConversationMember {uri, MemberRole::BANNED});
    3953          12 :         saveMembers();
    3954          12 :     }
    3955          12 :     return true;
    3956          12 : }
    3957             : 
    3958             : bool
    3959           1 : ConversationRepository::Impl::resolveUnban(const std::string_view type, const std::string& uri)
    3960             : {
    3961           1 :     auto repo = repository();
    3962           1 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    3963           1 :     auto bannedPath = repoPath / "banned";
    3964           1 :     auto crtStr = uri + (type != "invited" ? ".crt" : "");
    3965           2 :     auto originFilePath = bannedPath / type / crtStr;
    3966           1 :     auto destPath = repoPath / type;
    3967           1 :     auto destFilePath = destPath / crtStr;
    3968           1 :     if (!dhtnet::fileutils::recursive_mkdir(destPath, 0700)) {
    3969           0 :         JAMI_ERROR("An error occurred while creating the {} destination path. Abort resolving vote.", destPath);
    3970           0 :         return false;
    3971             :     }
    3972           1 :     std::error_code ec;
    3973           1 :     std::filesystem::rename(originFilePath, destFilePath, ec);
    3974           1 :     if (ec) {
    3975           0 :         JAMI_ERROR("Error when moving {} to {}. Abort resolving vote.", originFilePath, destFilePath);
    3976           0 :         return false;
    3977             :     }
    3978             : 
    3979           1 :     std::lock_guard lk(membersMtx_);
    3980           1 :     auto updated = false;
    3981             : 
    3982           1 :     auto role = MemberRole::MEMBER;
    3983           1 :     if (type == "invited")
    3984           0 :         role = MemberRole::INVITED;
    3985           1 :     else if (type == "admins")
    3986           0 :         role = MemberRole::ADMIN;
    3987             : 
    3988           2 :     for (auto& member : members_) {
    3989           2 :         if (member.uri == uri) {
    3990           1 :             updated = true;
    3991           1 :             member.role = role;
    3992           1 :             break;
    3993             :         }
    3994             :     }
    3995           1 :     if (!updated)
    3996           0 :         members_.emplace_back(ConversationMember {uri, role});
    3997           1 :     saveMembers();
    3998           1 :     return true;
    3999           1 : }
    4000             : 
    4001             : std::string
    4002          13 : ConversationRepository::resolveVote(const std::string& uri, const std::string_view type, const std::string& voteType)
    4003             : {
    4004          13 :     std::lock_guard lkOp(pimpl_->opMtx_);
    4005          13 :     pimpl_->resetHard();
    4006             :     // Count ratio admin/votes
    4007          13 :     auto nbAdmins = 0, nbVotes = 0;
    4008             :     // For each admin, check if voted
    4009          13 :     auto repo = pimpl_->repository();
    4010          13 :     if (!repo)
    4011           0 :         return {};
    4012          13 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4013          13 :     auto adminsPath = repoPath / MemberPath::ADMINS;
    4014          26 :     auto voteDirectory = repoPath / "votes" / voteType / type / uri;
    4015          26 :     for (const auto& certificate : dhtnet::fileutils::readDirectory(adminsPath)) {
    4016          13 :         if (certificate.find(".crt") == std::string::npos) {
    4017           0 :             JAMI_WARNING("Incorrect file found: {}/{}", adminsPath, certificate);
    4018           0 :             continue;
    4019           0 :         }
    4020          26 :         auto adminUri = certificate.substr(0, certificate.size() - std::string(".crt").size());
    4021          13 :         nbAdmins += 1;
    4022          13 :         if (std::filesystem::is_regular_file(fileutils::getFullPath(voteDirectory, adminUri)))
    4023          13 :             nbVotes += 1;
    4024          26 :     }
    4025             : 
    4026          13 :     if (nbAdmins > 0 && (static_cast<double>(nbVotes) / static_cast<double>(nbAdmins)) > .5) {
    4027          52 :         JAMI_WARNING("More than half of the admins voted to ban {}, applying the ban.", uri);
    4028             : 
    4029             :         // Remove vote directory
    4030          13 :         dhtnet::fileutils::removeAll(voteDirectory, true);
    4031             : 
    4032          13 :         if (voteType == "ban") {
    4033          12 :             if (!pimpl_->resolveBan(type, uri))
    4034           0 :                 return {};
    4035           1 :         } else if (voteType == "unban") {
    4036           1 :             if (!pimpl_->resolveUnban(type, uri))
    4037           0 :                 return {};
    4038             :         }
    4039             : 
    4040             :         // Commit
    4041          13 :         if (!git_add_all(repo.get()))
    4042           0 :             return {};
    4043             : 
    4044          13 :         Json::Value json;
    4045          13 :         json["action"] = voteType;
    4046          13 :         json["uri"] = uri;
    4047          13 :         json["type"] = "member";
    4048          26 :         return pimpl_->commitMessage(json::toString(json));
    4049          13 :     }
    4050             : 
    4051             :     // If vote nok
    4052           0 :     return {};
    4053          13 : }
    4054             : 
    4055             : std::pair<std::vector<ConversationCommit>, bool>
    4056        1773 : ConversationRepository::validFetch(const std::string& remoteDevice) const
    4057             : {
    4058        3546 :     auto newCommit = remoteHead(remoteDevice);
    4059        1773 :     if (not pimpl_ or newCommit.empty())
    4060           0 :         return {{}, false};
    4061        1773 :     auto commitsToValidate = pimpl_->behind(newCommit);
    4062        1773 :     std::reverse(std::begin(commitsToValidate), std::end(commitsToValidate));
    4063        1773 :     auto isValid = pimpl_->validCommits(commitsToValidate);
    4064        1773 :     if (isValid)
    4065        1756 :         return {commitsToValidate, false};
    4066          17 :     return {{}, true};
    4067        1773 : }
    4068             : 
    4069             : std::pair<std::vector<ConversationCommit>, bool>
    4070         189 : ConversationRepository::validClone() const
    4071             : {
    4072         189 :     auto commits = log({});
    4073         189 :     if (!pimpl_->validCommits(commits))
    4074           4 :         return {{}, false};
    4075         185 :     return {std::move(commits), true};
    4076         189 : }
    4077             : 
    4078             : bool
    4079           2 : ConversationRepository::isValidUserAtCommit(const std::string& userDevice,
    4080             :                                             const std::string& commitId,
    4081             :                                             const git_buf& sig,
    4082             :                                             const git_buf& sig_data) const
    4083             : {
    4084           2 :     return pimpl_->isValidUserAtCommit(userDevice, commitId, sig, sig_data);
    4085             : }
    4086             : 
    4087             : bool
    4088           5 : ConversationRepository::validCommits(const std::vector<ConversationCommit>& commitsToValidate) const
    4089             : {
    4090           5 :     return pimpl_->validCommits(commitsToValidate);
    4091             : }
    4092             : 
    4093             : void
    4094         866 : ConversationRepository::removeBranchWith(const std::string& remoteDevice)
    4095             : {
    4096         866 :     git_remote* remote_ptr = nullptr;
    4097         866 :     auto repo = pimpl_->repository();
    4098         866 :     if (!repo || git_remote_lookup(&remote_ptr, repo.get(), remoteDevice.c_str()) < 0) {
    4099           0 :         JAMI_WARNING("No remote found with id: {}", remoteDevice);
    4100           0 :         return;
    4101             :     }
    4102         866 :     GitRemote remote {remote_ptr};
    4103             : 
    4104         866 :     git_remote_prune(remote.get(), nullptr);
    4105         866 : }
    4106             : 
    4107             : std::vector<std::string>
    4108          30 : ConversationRepository::getInitialMembers() const
    4109             : {
    4110          30 :     return pimpl_->getInitialMembers();
    4111             : }
    4112             : 
    4113             : std::vector<ConversationMember>
    4114        1357 : ConversationRepository::members() const
    4115             : {
    4116        1357 :     return pimpl_->members();
    4117             : }
    4118             : 
    4119             : std::set<std::string>
    4120        2945 : ConversationRepository::memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
    4121             : {
    4122        2945 :     return pimpl_->memberUris(filter, filteredRoles);
    4123             : }
    4124             : 
    4125             : std::map<std::string, std::vector<DeviceId>>
    4126         514 : ConversationRepository::devices(bool ignoreExpired) const
    4127             : {
    4128         514 :     return pimpl_->devices(ignoreExpired);
    4129             : }
    4130             : 
    4131             : void
    4132         774 : ConversationRepository::refreshMembers() const
    4133             : {
    4134             :     try {
    4135         774 :         pimpl_->initMembers();
    4136           0 :     } catch (...) {
    4137           0 :     }
    4138         774 : }
    4139             : 
    4140             : void
    4141         189 : ConversationRepository::pinCertificates(bool blocking)
    4142             : {
    4143         189 :     auto acc = pimpl_->account_.lock();
    4144         189 :     auto repo = pimpl_->repository();
    4145         189 :     if (!repo or !acc)
    4146           0 :         return;
    4147             : 
    4148         189 :     std::string repoPath = git_repository_workdir(repo.get());
    4149         189 :     std::vector<std::string> paths = {repoPath + MemberPath::ADMINS.string(),
    4150         378 :                                       repoPath + MemberPath::MEMBERS.string(),
    4151        1323 :                                       repoPath + MemberPath::DEVICES.string()};
    4152             : 
    4153         756 :     for (const auto& path : paths) {
    4154         567 :         if (blocking) {
    4155         567 :             std::promise<bool> p;
    4156         567 :             std::future<bool> f = p.get_future();
    4157        1134 :             acc->certStore().pinCertificatePath(path, [&](auto /* certs */) { p.set_value(true); });
    4158         567 :             f.wait();
    4159         567 :         } else {
    4160           0 :             acc->certStore().pinCertificatePath(path, {});
    4161             :         }
    4162             :     }
    4163         189 : }
    4164             : 
    4165             : std::string
    4166       13428 : ConversationRepository::uriFromDevice(const std::string& deviceId) const
    4167             : {
    4168       13428 :     return pimpl_->uriFromDevice(deviceId);
    4169             : }
    4170             : 
    4171             : std::string
    4172           9 : ConversationRepository::updateInfos(const std::map<std::string, std::string>& profile)
    4173             : {
    4174           9 :     std::lock_guard lkOp(pimpl_->opMtx_);
    4175           9 :     pimpl_->resetHard();
    4176           9 :     auto valid = false;
    4177             :     {
    4178           9 :         std::lock_guard lk(pimpl_->membersMtx_);
    4179          10 :         for (const auto& member : pimpl_->members_) {
    4180          10 :             if (member.uri == pimpl_->userId_) {
    4181           9 :                 valid = member.role <= pimpl_->updateProfilePermLvl_;
    4182           9 :                 break;
    4183             :             }
    4184             :         }
    4185           9 :     }
    4186           9 :     if (!valid) {
    4187           4 :         JAMI_ERROR("Insufficient permission to update information.");
    4188           1 :         emitSignal<libjami::ConversationSignal::OnConversationError>(pimpl_->accountId_,
    4189           1 :                                                                      pimpl_->id_,
    4190             :                                                                      EUNAUTHORIZED,
    4191             :                                                                      "Insufficient permission to update information.");
    4192           1 :         return {};
    4193             :     }
    4194             : 
    4195           8 :     auto infosMap = infos();
    4196          19 :     for (const auto& [k, v] : profile) {
    4197          11 :         infosMap[k] = v;
    4198             :     }
    4199           8 :     auto repo = pimpl_->repository();
    4200           8 :     if (!repo)
    4201           0 :         return {};
    4202           8 :     std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4203           8 :     auto profilePath = repoPath / "profile.vcf";
    4204           8 :     std::ofstream file(profilePath, std::ios::trunc | std::ios::binary);
    4205           8 :     if (!file.is_open()) {
    4206           0 :         JAMI_ERROR("Unable to write data to {}", profilePath);
    4207           0 :         return {};
    4208             :     }
    4209             : 
    4210          32 :     auto addKey = [&](auto property, auto key) {
    4211          32 :         auto it = infosMap.find(std::string(key));
    4212          32 :         if (it != infosMap.end()) {
    4213          11 :             file << property;
    4214          11 :             file << ":";
    4215          11 :             file << it->second;
    4216          11 :             file << vCard::Delimiter::END_LINE_TOKEN;
    4217             :         }
    4218          32 :     };
    4219             : 
    4220           8 :     file << vCard::Delimiter::BEGIN_TOKEN;
    4221           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4222           8 :     file << vCard::Property::VCARD_VERSION;
    4223           8 :     file << ":2.1";
    4224           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4225           8 :     addKey(vCard::Property::FORMATTED_NAME, vCard::Value::TITLE);
    4226           8 :     addKey(vCard::Property::DESCRIPTION, vCard::Value::DESCRIPTION);
    4227           8 :     file << vCard::Property::PHOTO;
    4228           8 :     file << vCard::Delimiter::SEPARATOR_TOKEN;
    4229           8 :     file << vCard::Property::BASE64;
    4230           8 :     auto avatarIt = infosMap.find(std::string(vCard::Value::AVATAR));
    4231           8 :     if (avatarIt != infosMap.end()) {
    4232             :         // TODO: type=png? store another way?
    4233           0 :         file << ":";
    4234           0 :         file << avatarIt->second;
    4235             :     }
    4236           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4237           8 :     addKey(vCard::Property::RDV_ACCOUNT, vCard::Value::RDV_ACCOUNT);
    4238           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4239           8 :     addKey(vCard::Property::RDV_DEVICE, vCard::Value::RDV_DEVICE);
    4240           8 :     file << vCard::Delimiter::END_LINE_TOKEN;
    4241           8 :     file << vCard::Delimiter::END_TOKEN;
    4242           8 :     file.close();
    4243             : 
    4244           8 :     if (!pimpl_->add("profile.vcf"))
    4245           0 :         return {};
    4246           8 :     Json::Value json;
    4247           8 :     json["type"] = "application/update-profile";
    4248          16 :     return pimpl_->commitMessage(json::toString(json));
    4249           9 : }
    4250             : 
    4251             : std::map<std::string, std::string>
    4252         337 : ConversationRepository::infos() const
    4253             : {
    4254         337 :     if (auto repo = pimpl_->repository()) {
    4255             :         try {
    4256         337 :             std::filesystem::path repoPath = git_repository_workdir(repo.get());
    4257         337 :             auto profilePath = repoPath / "profile.vcf";
    4258         337 :             std::map<std::string, std::string> result;
    4259         337 :             std::error_code ec;
    4260         337 :             if (std::filesystem::is_regular_file(profilePath, ec)) {
    4261          25 :                 auto content = fileutils::loadFile(profilePath);
    4262          50 :                 result = ConversationRepository::infosFromVCard(
    4263          75 :                     vCard::utils::toMap(std::string_view {(const char*) content.data(), content.size()}));
    4264          25 :             }
    4265         337 :             result["mode"] = std::to_string(static_cast<int>(mode()));
    4266         337 :             return result;
    4267         337 :         } catch (...) {
    4268           0 :         }
    4269         337 :     }
    4270           0 :     return {};
    4271             : }
    4272             : 
    4273             : std::map<std::string, std::string>
    4274         100 : ConversationRepository::infosFromVCard(vCard::utils::VCardData&& details)
    4275             : {
    4276         100 :     std::map<std::string, std::string> result;
    4277         216 :     for (auto&& [k, v] : details) {
    4278         116 :         if (k == vCard::Property::FORMATTED_NAME) {
    4279          15 :             result["title"] = std::move(v);
    4280         101 :         } else if (k == vCard::Property::DESCRIPTION) {
    4281           1 :             result["description"] = std::move(v);
    4282         100 :         } else if (k.find(vCard::Property::PHOTO) == 0) {
    4283           0 :             result["avatar"] = std::move(v);
    4284         100 :         } else if (k.find(vCard::Property::RDV_ACCOUNT) == 0) {
    4285          11 :             result["rdvAccount"] = std::move(v);
    4286          89 :         } else if (k.find(vCard::Property::RDV_DEVICE) == 0) {
    4287          11 :             result["rdvDevice"] = std::move(v);
    4288             :         }
    4289             :     }
    4290         100 :     return result;
    4291           0 : }
    4292             : 
    4293             : std::string
    4294        1774 : ConversationRepository::getHead() const
    4295             : {
    4296        1774 :     if (auto repo = pimpl_->repository()) {
    4297             :         git_oid commit_id;
    4298        1774 :         if (git_reference_name_to_id(&commit_id, repo.get(), "HEAD") < 0) {
    4299           0 :             JAMI_ERROR("Unable to get reference for HEAD");
    4300        1774 :             return {};
    4301             :         }
    4302        1774 :         if (auto commit_str = git_oid_tostr_s(&commit_id))
    4303        1774 :             return commit_str;
    4304        1774 :     }
    4305           0 :     return {};
    4306             : }
    4307             : 
    4308             : std::optional<std::map<std::string, std::string>>
    4309       17766 : ConversationRepository::convCommitToMap(const ConversationCommit& commit) const
    4310             : {
    4311       17766 :     return pimpl_->convCommitToMap(commit);
    4312             : }
    4313             : 
    4314             : std::vector<std::map<std::string, std::string>>
    4315        1388 : ConversationRepository::convCommitsToMap(const std::vector<ConversationCommit>& commits) const
    4316             : {
    4317        1388 :     std::vector<std::map<std::string, std::string>> result = {};
    4318        1388 :     result.reserve(commits.size());
    4319        3610 :     for (const auto& commit : commits) {
    4320        2222 :         if (auto message = pimpl_->convCommitToMap(commit))
    4321        2222 :             result.emplace_back(*message);
    4322             :     }
    4323        1388 :     return result;
    4324           0 : }
    4325             : 
    4326             : } // namespace jami

Generated by: LCOV version 1.14