LCOV - code coverage report
Current view: top level - foo/src/jamidht - conversationrepository.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 1742 2520 69.1 %
Date: 2025-08-24 09:11:10 Functions: 177 599 29.5 %

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

Generated by: LCOV version 1.14