LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1891 2564 73.8 %
Date: 2024-04-20 08:06:59 Functions: 212 580 36.6 %

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

Generated by: LCOV version 1.14