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