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