LCOV - code coverage report
Current view: top level - src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1874 2530 74.1 %
Date: 2024-12-21 08:56:24 Functions: 212 588 36.1 %

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

Generated by: LCOV version 1.14