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