37using namespace std::string_view_literals;
44bool ConversationRepository::DISABLE_RESET =
false;
49inline std::string_view
55inline std::string_view
64 Impl(
const std::shared_ptr<JamiAccount>&
account,
const std::string&
id)
72 /
"conversation_data" /
id_;
85 throw std::logic_error(
"Invalid git repository");
90 auto indexPath = std::filesystem::path(repoPath /
"index.lock");
98 auto refPath = std::filesystem::path(repoPath /
"refs" /
"heads" /
"main.lock");
99 if (std::filesystem::exists(
refPath)) {
106 auto remotePath = std::filesystem::path(repoPath /
"refs" /
"remotes");
109 if (std::filesystem::exists(
refPath,
ec)) {
129 msgpack::object_handle
oh = msgpack::unpack((
const char*)
file.data(),
file.size());
132 }
catch (
const std::exception&
e) {
157 +
"conversations" +
"/" +
id_;
172 auto name = shared->getDisplayName();
215 bool add(
const std::string& path);
229 std::vector<ConversationCommit>
behind(
const std::string& from)
const;
233 const std::string& from =
"",
234 bool logIfNotFound =
true)
const;
244 bool resolveBan(
const std::string_view type,
const std::string& uri);
245 bool resolveUnban(
const std::string_view type,
const std::string& uri);
252 mutable std::optional<ConversationMode>
mode_ {};
258 std::vector<ConversationMember>
members()
const
279 auto cert = std::make_shared<dht::crypto::Certificate>(
286 if (!acc->certStore().getCertificate(
issuerUid)) {
288 auto memberFile = fmt::format(
"{}members/{}.crt",
291 auto adminFile = fmt::format(
"{}admins/{}.crt",
294 auto parentCert = std::make_shared<dht::crypto::Certificate>(
295 dhtnet::fileutils::loadFile(
299 ||
parentCert->getExpiration() < std::chrono::system_clock::now()))
300 acc->certStore().pinCertificate(
303 if (!acc->certStore().getCertificate(
cert->getPublicKey().getLongId().toString())) {
305 .pinCertificate(
cert,
310 }
catch (
const std::exception&) {
317 bool logIfNotFound =
true)
const
322 options.logIfNotFound = logIfNotFound;
335 std::set<std::string>
ret;
369 auto cert = acc->certStore().getCertificate(deviceId);
383 / fmt::format(
"{}.crt", deviceId);
384 if (!std::filesystem::is_regular_file(
deviceFile))
388 }
catch (
const std::exception&) {
415 auto deviceFile = fmt::format(
"devices/{}.crt", deviceId);
418 JAMI_ERROR(
"{} announced but not found", deviceId);
452 JAMI_ERROR(
"Device certificate with a bad issuer {}",
453 cert.getId().toString());
458 JAMI_ERROR(
"Certificate with a bad Id {}",
cert.getId().toString());
462 JAMI_ERROR(
"Certificate with a bad Id {}",
cert.getId().toString());
473 JAMI_ERROR(
"Device certificate with a bad issuer {}",
cert.getId().toString());
477 JAMI_ERROR(
"Certificate with a bad Id {}",
cert.getId().toString());
501 opts.initial_head =
"main";
503 JAMI_ERROR(
"Unable to create a git repository in {}", path);
519 JAMI_ERROR(
"Unable to open repository index");
538 const std::shared_ptr<JamiAccount>&
account,
542 auto deviceId =
account->currentDeviceId();
547 auto crlsPath = repoPath /
"CRLs" / deviceId;
549 if (!dhtnet::fileutils::recursive_mkdir(
adminsPath, 0700)) {
564 std::ofstream
file(
adminPath, std::ios::trunc | std::ios::binary);
565 if (!
file.is_open()) {
572 if (!dhtnet::fileutils::recursive_mkdir(
devicesPath, 0700)) {
579 file = std::ofstream(
devicePath, std::ios::trunc | std::ios::binary);
580 if (!
file.is_open()) {
587 if (!dhtnet::fileutils::recursive_mkdir(
crlsPath, 0700)) {
593 for (
const auto&
crl :
account->identity().second->getRevocationLists()) {
597 std::ofstream
file(
crlPath, std::ios::trunc | std::ios::binary);
598 if (!
file.is_open()) {
608 if (!dhtnet::fileutils::recursive_mkdir(
invitedPath, 0700)) {
619 if (!
file.is_open()) {
629 JAMI_LOG(
"Initial files added in {}", repoPath);
643 const std::shared_ptr<JamiAccount>&
account,
647 auto deviceId = std::string(
account->currentDeviceId());
648 auto name =
account->getDisplayName();
662 JAMI_ERROR(
"Unable to create a commit signature.");
669 JAMI_ERROR(
"Unable to open repository index");
675 JAMI_ERROR(
"Unable to write initial tree from index");
686 json[
"mode"] =
static_cast<int>(mode);
690 json[
"type"] =
"initial";
703 JAMI_ERROR(
"Unable to create initial buffer");
745 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to create a commit signature: no name set",
accountId_,
id_);
765 if (!validateDevice()) {
766 JAMI_ERROR(
"[Account {}] [Conversation {}] Invalid device. Not migrated?", accountId_, id_);
771 auto repo = repository();
773 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get HEAD reference", accountId_, id_);
799 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to peel HEAD reference", accountId_, id_);
809 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to lookup commit {}", accountId_, id_,
wanted_ref);
814 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to lookup commit {}", accountId_, id_,
wanted_ref);
825 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to write index: {}", accountId_, id_,
err->message);
829 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to lookup tree", accountId_, id_);
839#if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 && \
840 (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
857 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to create commit buffer: {}", accountId_, id_,
err->message);
861 auto account = account_.lock();
876 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
883 JAMI_LOG(
"[Account {}] [Conversation {}] New merge commit added with id: {}", accountId_, id_,
commit_str);
890 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to move commit to main: {}", accountId_, id_,
err->message);
923 auto repo = repository();
925 JAMI_ERROR(
"[Account {}] [Conversation {}] No repository found", accountId_, id_);
932 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to lookup HEAD ref", accountId_, id_);
945 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to create main reference: {}", accountId_, id_,
err->message);
951 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to get HEAD reference", accountId_, id_);
970 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to checkout HEAD reference: {}", accountId_, id_,
err->message);
972 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to checkout HEAD reference: unknown error", accountId_, id_);
979 JAMI_ERROR(
"[Account {}] [Conversation {}] failed to move HEAD reference", accountId_, id_);
990 auto repo = repository();
995 JAMI_ERROR(
"Unable to open repository index");
1014 auto repo = repository();
1074 auto repo = repository();
1101 if (
editedCommitMap->at(
"type") ==
"application/data-transfer+json") {
1143 auto repo = repository();
1164 "votes.(\\w+).(members|devices|admins|invited).(\\w+).(\\w+)");
1177 std::string_view type = svsub_match_view(
base_match[2]);
1201 if (type !=
"devices") {
1237 auto repo = repository();
1306 std::string
bannedFile = std::string(
"banned") +
"/" +
"members" +
"/" +
uriMember +
".crt";
1335 auto repo = repository();
1397 auto repo = repository();
1414 static const std::regex
regex_devices(
"devices.(\\w+)\\.crt");
1425 JAMI_ERROR(
"Unwanted changed file detected: {}",
f);
1458 auto repo = repository();
1472 std::vector<std::string>
voters;
1478 +
".(members|devices|admins|invited).(\\w+).(\\w+)");
1479 static const std::regex
regex_devices(
"devices.(\\w+)\\.crt");
1480 static const std::regex
regex_banned(
"banned.(members|devices|admins).(\\w+)\\.crt");
1506 JAMI_ERROR(
"Invalid banned file detected : {}",
f);
1510 JAMI_ERROR(
"Unwanted changed file detected: {}",
f);
1553 for (
const auto& certificate : dhtnet::fileutils::readDirectory(repoPath +
"admins")) {
1554 if (certificate.find(
".crt") == std::string::npos) {
1559 auto adminUri = certificate.substr(0, certificate.size() - std::string(
".crt").size());
1580 auto repo = repository();
1614 if (
f ==
"profile.vcf") {
1628 JAMI_ERROR(
"Unwanted changed file detected: {}",
f);
1639 auto acc = account_.lock();
1644 auto repo = repository();
1706 acc->certStore().pinCertificate(std::move(
deviceCert));
1707 acc->certStore().pinCertificate(std::move(
parentCert));
1717 auto account = account_.lock();
1718 auto repo = repository();
1789 auto repo = repository();
1790 auto account = account_.lock();
1792 JAMI_WARNING(
"[Account {}] [Conversation {}] Invalid repository detected", accountId_, id_);
1795 auto path = fmt::format(
"devices/{}.crt", deviceId_);
1798 if (!std::filesystem::is_regular_file(
devicePath)) {
1807 }
catch (
const std::exception&) {
1811 JAMI_WARNING(
"[Account {}] [Conversation {}] Device certificate is no longer valid. Attempting to update certificate.", accountId_, id_);
1815 JAMI_ERROR(
"[Account {}] [Conversation {}] Current device's certificate is invalid. A migration is needed", accountId_, id_);
1818 std::ofstream
file(
devicePath, std::ios::trunc | std::ios::binary);
1819 if (!
file.is_open()) {
1820 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to write data to {}", accountId_, id_,
devicePath);
1826 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to add file {}", accountId_, id_,
devicePath);
1832 auto adminPath = fmt::format(
"admins/{}.crt", userId_);
1833 auto memberPath = fmt::format(
"members/{}.crt", userId_);
1842 JAMI_ERROR(
"[Account {}] [Conversation {}] Invalid parent path (not in members or admins)", accountId_, id_);
1849 }
catch (
const std::exception&) {
1853 JAMI_WARNING(
"[Account {}] [Conversation {}] Account certificate is no longer valid. Attempting to update certificate.", accountId_, id_);
1857 std::ofstream
file(
parentPath, std::ios::trunc | std::ios::binary);
1858 if (!
file.is_open()) {
1859 JAMI_ERROR(
"Unable to write data to {}", path);
1878 JAMI_ERROR(
"[Account {}] [Conversation {}] commit failed: Invalid device", accountId_, id_);
1883 JAMI_ERROR(
"[Account {}] [Conversation {}] commit failed: Unable to generate signature", accountId_, id_);
1886 auto account = account_.lock();
1890 auto repo = repository();
1894 JAMI_ERROR(
"[Account {}] [Conversation {}] commit failed: Unable to open repository index", accountId_, id_);
1901 JAMI_ERROR(
"[Account {}] [Conversation {}] commit failed: Unable to write initial tree from index", accountId_, id_);
1907 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
1914 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
1920 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to look up HEAD commit", accountId_, id_);
1929#if LIBGIT2_VER_MAJOR == 1 && LIBGIT2_VER_MINOR == 8 && \
1930 (LIBGIT2_VER_REVISION == 0 || LIBGIT2_VER_REVISION == 1 || LIBGIT2_VER_REVISION == 3)
1945 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to create commit buffer", accountId_, id_);
1959 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to sign commit", accountId_, id_);
1971 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to move commit to main: {}", accountId_, id_,
err->message);
1983 JAMI_LOG(
"[Account {}] [Conversation {}] New message added with id: {}", accountId_, id_,
commit_str);
1992 if (mode_ != std::nullopt)
2003 "No initial commit");
2004 throw std::logic_error(
"Unable to retrieve first commit");
2013 "No initial commit");
2014 throw std::logic_error(
"Unable to retrieve first commit");
2016 if (!
root.isMember(
"mode")) {
2020 "No mode detected");
2021 throw std::logic_error(
"No mode detected for initial commit");
2042 "Incorrect mode detected");
2043 throw std::logic_error(
"Incorrect mode detected");
2051 if (
auto repo = repository()) {
2060 const std::string&
idNew,
2061 const std::string&
idOld)
const
2064 JAMI_ERROR(
"Unable to get reference for HEAD");
2071 if (
idNew ==
"HEAD") {
2073 JAMI_ERROR(
"Unable to get reference for HEAD");
2093 JAMI_ERROR(
"Unable to look up initial tree");
2099 if (
idOld.empty()) {
2101 JAMI_ERROR(
"Unable to get diff to empty repository");
2117 JAMI_ERROR(
"Unable to look up initial tree");
2130std::vector<ConversationCommit>
2134 auto repo = repository();
2138 JAMI_ERROR(
"Unable to get reference for HEAD");
2144 JAMI_ERROR(
"Unable to get reference for commit {}", from);
2150 JAMI_ERROR(
"Unable to get any merge base for commit {} and {}", from,
head);
2153 for (std::size_t
i = 0;
i <
bases.count; ++
i) {
2171 const std::string& from,
2172 bool logIfNotFound)
const
2177 auto repo = repository();
2179 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
2198 JAMI_DEBUG(
"[Account {}] [Conversation {}] Unable to init revwalker", accountId_, id_);
2209 JAMI_WARNING(
"[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_,
id);
2219 std::vector<std::string> parents;
2226 parents.emplace_back(
parent);
2239 cc.
author = std::move(author);
2240 cc.
parents = std::move(parents);
2244 JAMI_WARNING(
"[Account {}] [Conversation {}] Unable to extract signature for commit {}", accountId_, id_,
id);
2247 std::string(signature.ptr, signature.ptr + signature.size));
2263std::vector<ConversationCommit>
2266 std::vector<ConversationCommit>
commits {};
2270 [&](
const auto&
id,
const auto& author,
const auto& commit) {
2273 commits.rbegin()->linearized_parent = id;
2276 return CallbackResult::Skip;
2295 if (
options.authorUri !=
"") {
2307 [&](
auto&& cc) {
commits.emplace(
commits.end(), std::forward<decltype(cc)>(cc)); },
2308 [](
auto,
auto,
auto) {
return false; },
2344 JAMI_WARNING(
"[Account {}] [Conversation {}] Failed to look up commit {}", accountId_, id_,
commitId);
2350 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to look up initial tree", accountId_, id_);
2356std::vector<std::string>
2359 auto acc = account_.lock();
2399 auto repo = repository();
2401 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get reference for HEAD", accountId_, id_);
2457 using std::filesystem::path;
2458 auto repo = repository();
2460 throw std::logic_error(
"Invalid git repository");
2462 std::vector<std::string>
uris;
2463 std::lock_guard
lk(membersMtx_);
2467 static const std::vector<std::pair<MemberRole, path>>
paths = {
2476 for (
const auto& [role, p] :
paths) {
2477 for (
const auto&
f : std::filesystem::directory_iterator(repoPath / p,
ec)) {
2478 auto uri =
f.path().stem().string();
2479 if (std::find(
uris.begin(),
uris.end(), uri) ==
uris.end()) {
2481 uris.emplace_back(uri);
2497std::optional<std::map<std::string, std::string>>
2502 JAMI_ERROR(
"[Account {}] [Conversation {}] Invalid author id for commit {}", accountId_, id_, commit.
id);
2503 return std::nullopt;
2505 std::string parents;
2512 std::string type {};
2515 std::string body {};
2516 std::map<std::string, std::string> message;
2520 for (
auto const&
id :
cm.getMemberNames()) {
2522 type =
cm[
id].asString();
2525 message.insert({
id,
cm[
id].asString()});
2530 return std::nullopt;
2531 }
else if (type ==
"application/data-transfer+json") {
2533 auto tid = message[
"tid"];
2537 message[
"fileId"] = fmt::format(
"{}_{}.{}", commit.
id,
tid,
extension);
2539 message[
"fileId"] = fmt::format(
"{}_{}", commit.
id,
tid);
2541 message[
"fileId"] =
"";
2544 message[
"id"] = commit.
id;
2545 message[
"parents"] = parents;
2548 message[
"type"] = type;
2549 message[
"timestamp"] = std::to_string(commit.
timestamp);
2559 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get diff stats", accountId_, id_);
2567 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to format diff stats", accountId_, id_);
2578std::unique_ptr<ConversationRepository>
2584 std::uniform_int_distribution<uint64_t>
dist;
2588 if (std::filesystem::is_directory(
tmpPath)) {
2592 if (!dhtnet::fileutils::recursive_mkdir(
tmpPath, 0700)) {
2603 JAMI_ERROR(
"Error when adding initial files");
2604 dhtnet::fileutils::removeAll(
tmpPath,
true);
2612 dhtnet::fileutils::removeAll(
tmpPath,
true);
2622 dhtnet::fileutils::removeAll(
tmpPath,
true);
2628 return std::make_unique<ConversationRepository>(
account,
id);
2631std::unique_ptr<ConversationRepository>
2633 const std::shared_ptr<JamiAccount>&
account,
2634 const std::string& deviceId,
2635 const std::string& conversationId,
2636 std::function<
void(std::vector<ConversationCommit>)>&&
checkCommitCb)
2639 if (conversationId.empty()) {
2640 JAMI_ERROR(
"[Account {}] Clone conversation with empty conversationId",
account->getAccountID());
2647 auto url = fmt::format(
"git://{}/{}", deviceId, conversationId);
2660 JAMI_ERROR(
"Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
2661 stats->received_bytes,
2662 stats->received_objects,
2663 stats->total_objects);
2669 if (std::filesystem::is_directory(path)) {
2671 JAMI_WARNING(
"Removing existing directory {} (the dir exists and non empty)", path);
2672 if (dhtnet::fileutils::removeAll(path,
true) != 0)
2676 JAMI_DEBUG(
"[Account {}] [Conversation {}] Start clone of {:s} to {}",
account->getAccountID(), conversationId, url, path);
2682 JAMI_ERROR(
"[Account {}] [Conversation {}] Error when retrieving remote conversation: {:s} {}",
account->getAccountID(), conversationId,
gerr->message, path);
2684 JAMI_ERROR(
"[Account {}] [Conversation {}] Unknown error {:d} when retrieving remote conversation",
account->getAccountID(), conversationId,
err);
2688 auto repo = std::make_unique<ConversationRepository>(
account, conversationId);
2689 repo->pinCertificates(
true);
2692 JAMI_ERROR(
"[Account {}] [Conversation {}] error when validating remote conversation",
account->getAccountID(), conversationId);
2695 JAMI_LOG(
"[Account {}] [Conversation {}] New conversation cloned in {}",
account->getAccountID(), conversationId, path);
2706 if (commit.parents.size() == 0) {
2707 if (!checkInitialCommit(
userDevice, commit.id, commit.commit_msg)) {
2708 JAMI_WARNING(
"[Account {}] [Conversation {}] Malformed initial commit {}. Please check you use the latest "
2709 "version of Jami, or that your contact is not doing unwanted stuff.",
2710 accountId_, id_, commit.id);
2712 accountId_, id_,
EVALIDFETCH,
"Malformed initial commit");
2715 }
else if (commit.parents.size() == 1) {
2716 std::string type = {},
editId = {};
2719 type =
cm[
"type"].asString();
2723 accountId_, id_,
EVALIDFETCH,
"Malformed commit");
2727 if (type ==
"vote") {
2729 if (!checkVote(
userDevice, commit.id, commit.parents[0])) {
2731 "[Account {}] [Conversation {}] Malformed vote commit {}. Please check you use the latest version "
2732 "of Jami, or that your contact is not doing unwanted stuff.",
2733 accountId_, id_, commit.id);
2739 }
else if (type ==
"member") {
2740 std::string
action =
cm[
"action"].asString();
2745 "[Account {}] [Conversation {}] Malformed add commit {}. Please check you use the latest version "
2746 "of Jami, or that your contact is not doing unwanted stuff.",
2747 accountId_, id_, commit.id);
2753 "Malformed add member commit");
2756 }
else if (
action ==
"join") {
2759 "[Account {}] [Conversation {}] Malformed joins commit {}. Please check you use the latest version "
2760 "of Jami, or that your contact is not doing unwanted stuff.",
2761 accountId_, id_, commit.id);
2767 "Malformed join member commit");
2770 }
else if (
action ==
"remove") {
2776 "[Account {}] [Conversation {}] Malformed removes commit {}. Please check you use the latest version "
2777 "of Jami, or that your contact is not doing unwanted stuff.",
2778 accountId_, id_, commit.id);
2784 "Malformed remove member commit");
2795 "[Account {}] [Conversation {}] Malformed removes commit {}. Please check you use the latest version "
2796 "of Jami, or that your contact is not doing unwanted stuff.",
2797 accountId_, id_, commit.id);
2803 "Malformed ban member commit");
2808 "[Account {}] [Conversation {}] Malformed member commit {} with action {}. Please check you use the "
2810 "version of Jami, or that your contact is not doing unwanted stuff.",
2811 accountId_, id_, commit.id,
2815 accountId_, id_,
EVALIDFETCH,
"Malformed member commit");
2818 }
else if (type ==
"application/update-profile") {
2819 if (!checkValidProfileUpdate(
userDevice, commit.id, commit.parents[0])) {
2820 JAMI_WARNING(
"[Account {}] [Conversation {}] Malformed profile updates commit {}. Please check you use the "
2822 "of Jami, or that your contact is not doing unwanted stuff.",
2823 accountId_, id_, commit.id);
2829 "Malformed profile updates commit");
2832 }
else if (type ==
"application/edited-message" || !
editId.empty()) {
2834 JAMI_ERROR(
"Commit {:s} malformed", commit.id);
2837 accountId_, id_,
EVALIDFETCH,
"Malformed edit commit");
2844 if (!checkValidUserDiff(
userDevice, commit.id, commit.parents[0])) {
2846 "[Account {}] [Conversation {}] Malformed {} commit {}. Please check you use the latest "
2847 "version of Jami, or that your contact is not doing unwanted stuff.",
2848 accountId_, id_, type, commit.id);
2851 accountId_, id_,
EVALIDFETCH,
"Malformed commit");
2861 "[Account {}] [Conversation {}] Malformed commit {}. Please check you use the latest version of Jami, or "
2862 "that your contact is not doing unwanted stuff. {}", accountId_, id_,
2866 accountId_, id_,
EVALIDFETCH,
"Malformed commit");
2873 "[Account {}] [Conversation {}] Malformed merge commit {}. Please check you use the latest version of "
2874 "Jami, or that your contact is not doing unwanted stuff.", accountId_, id_,
2877 accountId_, id_,
EVALIDFETCH,
"Malformed commit");
2881 JAMI_DEBUG(
"[Account {}] [Conversation {}] Validate commit {}", accountId_, id_, commit.id);
2888ConversationRepository::ConversationRepository(
const std::shared_ptr<JamiAccount>&
account,
2889 const std::string&
id)
2904 std::lock_guard
lkOp(pimpl_->opMtx_);
2905 pimpl_->resetHard();
2906 auto repo = pimpl_->repository();
2913 std::filesystem::path
invitedPath = repoPath /
"invited";
2914 if (!dhtnet::fileutils::recursive_mkdir(
invitedPath, 0700)) {
2919 if (std::filesystem::is_regular_file(
devicePath)) {
2924 std::ofstream
file(
devicePath, std::ios::trunc | std::ios::binary);
2925 if (!
file.is_open()) {
2929 std::string path =
"invited/" + uri;
2930 if (!pimpl_->add(path))
2934 std::lock_guard
lk(pimpl_->membersMtx_);
2936 pimpl_->saveMembers();
2940 json[
"action"] =
"add";
2942 json[
"type"] =
"member";
2949 pimpl_->onMembersChanged_ = std::move(
cb);
2961 auto repo = pimpl_->repository();
2971 &
commit_id, commit.get(),
nullptr,
sig.get(),
sig.get(),
nullptr,
msg.c_str(),
nullptr)
2985 JAMI_ERROR(
"Unable to move commit to main: {}",
err->message);
3006 std::lock_guard
lkOp(pimpl_->opMtx_);
3007 pimpl_->resetHard();
3022 auto repo = pimpl_->repository();
3028 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to lookup for remote {}", pimpl_->accountId_, pimpl_->id_,
remoteDeviceId);
3034 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to create remote for repository", pimpl_->accountId_, pimpl_->id_);
3047 JAMI_ERROR(
"Abort fetching repository, the fetch is too big: {} bytes ({}/{})",
3048 stats->received_bytes,
3049 stats->received_objects,
3050 stats->total_objects);
3058 JAMI_WARNING(
"[Account {}] [Conversation {}] Unable to fetch remote repository: {:s}",
3071 const std::string&
branch)
const
3074 auto repo = pimpl_->repository();
3110 std::string path = fmt::format(
"devices/{}.crt",
deviceId_);
3112 if (!std::filesystem::is_regular_file(
devicePath)) {
3113 std::ofstream
file(
devicePath, std::ios::trunc | std::ios::binary);
3114 if (!
file.is_open()) {
3135 auto repo = repository();
3141 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get HEAD commit: {}", accountId_, id_, error);
3151 std::lock_guard
lkOp(pimpl_->opMtx_);
3152 pimpl_->resetHard();
3163std::vector<std::string>
3166 pimpl_->addUserDevice();
3167 std::vector<std::string>
ret;
3170 ret.emplace_back(pimpl_->commit(
msg));
3174std::vector<ConversationCommit>
3184 const std::string& from,
3185 bool logIfNotFound)
const
3194std::optional<ConversationCommit>
3197 return pimpl_->getCommit(
commitId, logIfNotFound);
3200std::pair<bool, std::string>
3203 std::lock_guard
lkOp(pimpl_->opMtx_);
3204 pimpl_->resetHard();
3206 auto repo = pimpl_->repository();
3208 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to merge without repo", pimpl_->accountId_, pimpl_->id_);
3213 pimpl_->resetHard();
3216 JAMI_ERROR(
"[Account {}] [Conversation {}] Merge operation aborted: repository is in unexpected state {}", pimpl_->accountId_, pimpl_->id_, state);
3222 JAMI_ERROR(
"[Account {}] [Conversation {}] Merge operation aborted: unable to checkout main branch", pimpl_->accountId_, pimpl_->id_);
3229 JAMI_ERROR(
"[Account {}] [Conversation {}] Merge operation aborted: unable to lookup commit {}", pimpl_->accountId_, pimpl_->id_,
merge_id);
3234 JAMI_ERROR(
"[Account {}] [Conversation {}] Merge operation aborted: unable to lookup commit {}", pimpl_->accountId_, pimpl_->id_,
merge_id);
3244 JAMI_ERROR(
"[Account {}] [Conversation {}] Merge operation aborted: repository analysis failed", pimpl_->accountId_, pimpl_->id_);
3256 JAMI_LOG(
"[Account {}] [Conversation {}] Merge analysis result: Unborn", pimpl_->accountId_, pimpl_->id_);
3258 JAMI_LOG(
"[Account {}] [Conversation {}] Merge analysis result: Fast-forward", pimpl_->accountId_, pimpl_->id_);
3264 JAMI_ERROR(
"[Account {}] [Conversation {}] Fast forward merge failed: {}", pimpl_->accountId_, pimpl_->id_,
err->message);
3270 if (!pimpl_->validateDevice() && !
force) {
3271 JAMI_ERROR(
"[Account {}] [Conversation {}] Invalid device. Not migrated?", pimpl_->accountId_, pimpl_->id_);
3278 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to get reference for HEAD", pimpl_->accountId_, pimpl_->id_);
3284 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
3291 JAMI_ERROR(
"[Account {}] [Conversation {}] Unable to look up HEAD commit", pimpl_->accountId_, pimpl_->id_);
3304 JAMI_ERROR(
"[Account {}] [Conversation {}] Git merge failed: {}", pimpl_->accountId_, pimpl_->id_,
err->message);
3309 JAMI_LOG(
"Some conflicts were detected during the merge operations. Resolution phase.");
3311 JAMI_ERROR(
"Merge operation aborted; Unable to automatically resolve conflicts");
3315 auto result = pimpl_->createMergeCommit(index.get(),
merge_id);
3318 return {!result.empty(), result};
3324 if (
auto repo = pimpl_->repository()) {
3341std::vector<std::string>
3344 static const std::regex
re(
" +\\| +[0-9]+.*");
3346 std::string_view
line;
3359 std::lock_guard
lkOp(pimpl_->opMtx_);
3360 pimpl_->resetHard();
3362 auto repo = pimpl_->repository();
3366 auto account = pimpl_->account_.lock();
3378 auto adminsPath = repoPath /
"admins" / (uri +
".crt");
3379 if (std::filesystem::is_regular_file(
memberFile)
3388 if (!dhtnet::fileutils::recursive_mkdir(
membersPath, 0700)) {
3392 std::ofstream
file(
memberFile, std::ios::trunc | std::ios::binary);
3393 if (!
file.is_open()) {
3404 json[
"action"] =
"join";
3406 json[
"type"] =
"member";
3409 std::lock_guard
lk(pimpl_->membersMtx_);
3412 for (
auto&
member : pimpl_->members_) {
3421 pimpl_->saveMembers();
3430 std::lock_guard
lkOp(pimpl_->opMtx_);
3431 pimpl_->resetHard();
3433 auto account = pimpl_->account_.lock();
3434 auto repo = pimpl_->repository();
3440 auto crt = fmt::format(
"{}.crt", pimpl_->userId_);
3446 if (std::filesystem::is_regular_file(
adminFile,
ec)) {
3450 if (std::filesystem::is_regular_file(
memberFile,
ec)) {
3455 for (
const auto&
crl :
account->identity().second->getRevocationLists()) {
3458 auto crlPath =
crlsPath / pimpl_->deviceId_ / fmt::format(
"{}.crl", dht::toHex(
crl->getNumber()));
3459 if (std::filesystem::is_regular_file(
crlPath,
ec)) {
3465 for (
const auto& certificate : std::filesystem::directory_iterator(repoPath /
"devices",
ec)) {
3466 if (certificate.is_regular_file(
ec)) {
3469 if (
cert.getIssuerUID() == pimpl_->userId_)
3470 std::filesystem::remove(certificate.path(),
ec);
3482 json[
"action"] =
"remove";
3483 json[
"uri"] = pimpl_->userId_;
3484 json[
"type"] =
"member";
3487 std::lock_guard
lk(pimpl_->membersMtx_);
3488 pimpl_->members_.erase(std::remove_if(pimpl_->members_.begin(), pimpl_->members_.end(), [&](
auto&
member) {
3489 return member.uri == pimpl_->userId_;
3490 }), pimpl_->members_.end());
3491 pimpl_->saveMembers();
3501 if (
auto repo = pimpl_->repository()) {
3504 dhtnet::fileutils::removeAll(repoPath,
true);
3511 return pimpl_->mode();
3517 std::lock_guard
lkOp(pimpl_->opMtx_);
3518 pimpl_->resetHard();
3519 auto repo = pimpl_->repository();
3520 auto account = pimpl_->account_.lock();
3533 auto oldFile = repoPath / type / (uri + (type !=
"invited" ?
".crt" :
""));
3534 if (!std::filesystem::is_regular_file(
oldFile)) {
3535 JAMI_WARNING(
"Didn't found file for {} with type {}", uri, type);
3541 if (!dhtnet::fileutils::recursive_mkdir(
voteDirectory, 0700)) {
3554 if (!pimpl_->add(
toAdd))
3559 json[
"type"] =
"vote";
3566 std::lock_guard
lkOp(pimpl_->opMtx_);
3567 pimpl_->resetHard();
3568 auto repo = pimpl_->repository();
3569 auto account = pimpl_->account_.lock();
3580 if (!dhtnet::fileutils::recursive_mkdir(
voteDirectory, 0700)) {
3593 if (!pimpl_->add(
toAdd.c_str()))
3598 json[
"type"] =
"vote";
3605 auto repo = repository();
3610 auto crtStr = uri + (type !=
"invited" ?
".crt" :
"");
3615 if (!dhtnet::fileutils::recursive_mkdir(
destPath, 0700)) {
3628 if (type !=
"devices") {
3630 for (
const auto& certificate : std::filesystem::directory_iterator(
devicesPath,
ec)) {
3631 auto certPath = certificate.path();
3635 if (
issuer->getPublicKey().getId().to_view() == uri)
3636 dhtnet::fileutils::remove(
certPath,
true);
3641 std::lock_guard
lk(membersMtx_);
3644 for (
auto&
member : members_) {
3661 auto repo = repository();
3664 auto crtStr = uri + (type !=
"invited" ?
".crt" :
"");
3668 if (!dhtnet::fileutils::recursive_mkdir(
destPath, 0700)) {
3679 std::lock_guard
lk(membersMtx_);
3683 if (type ==
"invited")
3685 else if (type ==
"admins")
3688 for (
auto&
member : members_) {
3703 const std::string_view type,
3706 std::lock_guard
lkOp(pimpl_->opMtx_);
3707 pimpl_->resetHard();
3711 auto repo = pimpl_->repository();
3717 for (
const auto& certificate : dhtnet::fileutils::readDirectory(
adminsPath)) {
3718 if (certificate.find(
".crt") == std::string::npos) {
3722 auto adminUri = certificate.substr(0, certificate.size() - std::string(
".crt").size());
3729 JAMI_WARNING(
"More than half of the admins voted to ban {}, apply the ban", uri);
3735 if (!pimpl_->resolveBan(type, uri))
3738 if (!pimpl_->resolveUnban(type, uri))
3749 json[
"type"] =
"member";
3757std::pair<std::vector<ConversationCommit>,
bool>
3773 std::function<
void(std::vector<ConversationCommit>)>&&
checkCommitCb)
const
3788 auto repo = pimpl_->repository();
3798std::vector<std::string>
3801 return pimpl_->getInitialMembers();
3804std::vector<ConversationMember>
3807 return pimpl_->members();
3810std::set<std::string>
3817std::map<std::string, std::vector<DeviceId>>
3827 pimpl_->initMembers();
3835 auto acc = pimpl_->account_.lock();
3836 auto repo = pimpl_->repository();
3841 std::vector<std::string>
paths = {repoPath +
"admins",
3842 repoPath +
"members",
3843 repoPath +
"devices"};
3845 for (
const auto& path :
paths) {
3847 std::promise<bool> p;
3848 std::future<bool>
f = p.get_future();
3849 acc->certStore().pinCertificatePath(path, [&](
auto ) { p.set_value(
true); });
3852 acc->certStore().pinCertificatePath(path, {});
3860 return pimpl_->uriFromDevice(deviceId);
3866 std::lock_guard
lkOp(pimpl_->opMtx_);
3867 pimpl_->resetHard();
3870 std::lock_guard
lk(pimpl_->membersMtx_);
3871 for (
const auto&
member : pimpl_->members_) {
3872 if (
member.uri == pimpl_->userId_) {
3873 valid =
member.role <= pimpl_->updateProfilePermLvl_;
3879 JAMI_ERROR(
"Insufficient permission to update information");
3884 "Insufficient permission to update information");
3889 for (
const auto& [
k, v] :
profile) {
3892 auto repo = pimpl_->repository();
3896 auto profilePath = repoPath /
"profile.vcf";
3897 std::ofstream
file(profilePath, std::ios::trunc | std::ios::binary);
3898 if (!
file.is_open()) {
3899 JAMI_ERROR(
"Unable to write data to {}", profilePath);
3937 if (!pimpl_->add(
"profile.vcf"))
3940 json[
"type"] =
"application/update-profile";
3944std::map<std::string, std::string>
3947 if (
auto repo = pimpl_->repository()) {
3950 auto profilePath = repoPath /
"profile.vcf";
3951 std::map<std::string, std::string> result;
3953 if (std::filesystem::is_regular_file(profilePath,
ec)) {
3956 std::string_view {(
const char*)
content.data(),
content.size()}));
3958 result[
"mode"] = std::to_string(
static_cast<int>(
mode()));
3966std::map<std::string, std::string>
3969 std::map<std::string, std::string> result;
3972 result[
"title"] = std::move(v);
3974 result[
"description"] = std::move(v);
3976 result[
"avatar"] = std::move(v);
3978 result[
"rdvAccount"] = std::move(v);
3980 result[
"rdvDevice"] = std::move(v);
3989 if (
auto repo = pimpl_->repository()) {
3992 JAMI_ERROR(
"Unable to get reference for HEAD");
4001std::optional<std::map<std::string, std::string>>
4004 return pimpl_->convCommitToMap(commit);
4007std::vector<std::map<std::string, std::string>>
4010 std::vector<std::map<std::string, std::string>> result = {};
4011 result.reserve(
commits.size());
4012 for (
const auto& commit :
commits) {
4013 auto message = pimpl_->convCommitToMap(commit);
4014 if (message == std::nullopt)
4016 result.emplace_back(*message);
std::weak_ptr< JamiAccount > account_
std::optional< ConversationCommit > getCommit(const std::string &commitId, bool logIfNotFound=true) const
const std::string userId_
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
const std::string deviceId_
const std::string accountId_
std::string commit(const std::string &msg, bool verifyDevice=true)
ConversationMode mode() const
OnMembersChanged onMembersChanged_
std::string uriFromDevice(const std::string &deviceId, const std::string &commitId="") const
Retrieve the user related to a device using the account's certificate store.
std::map< std::string, std::vector< DeviceId > > devices(bool ignoreExpired=true) const
std::string diffStats(const std::string &newId, const std::string &oldId) const
std::map< std::string, std::string > deviceToUri_
bool isValidUserAtCommit(const std::string &userDevice, const std::string &commitId) const
std::string commitMessage(const std::string &msg, bool verifyDevice=true)
std::vector< ConversationCommit > behind(const std::string &from) const
GitTree treeAtCommit(git_repository *repo, const std::string &commitId) const
bool validCommits(const std::vector< ConversationCommit > &commits) const
GitObject memberCertificate(std::string_view memberUri, const GitTree &tree) const
bool checkInitialCommit(const std::string &userDevice, const std::string &commitId, const std::string &commitMsg) const
std::set< std::string > memberUris(std::string_view filter, const std::set< MemberRole > &filteredRoles) const
std::filesystem::path conversationDataPath_
void forEachCommit(PreConditionCb &&preCondition, std::function< void(ConversationCommit &&)> &&emplaceCb, PostConditionCb &&postCondition, const std::string &from="", bool logIfNotFound=true) const
bool add(const std::string &path)
bool checkValidVoteResolution(const std::string &userDevice, const std::string &uriMember, const std::string &commitId, const std::string &parentId, const std::string &voteType) const
std::filesystem::path membersCache_
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.
MemberRole updateProfilePermLvl_
std::vector< ConversationCommit > log(const LogOptions &options) const
std::string uriFromDeviceAtCommit(const std::string &deviceId, const std::string &commitId) const
Retrieve the user related to a device using certificate directly from the repository at a specific co...
std::string createMergeCommit(git_index *index, const std::string &wanted_ref)
Impl(const std::shared_ptr< JamiAccount > &account, const std::string &id)
std::string getDisplayName() const
GitDiff diff(git_repository *repo, const std::string &idNew, const std::string &idOld) const
std::optional< std::map< std::string, std::string > > convCommitToMap(const ConversationCommit &commit) const
std::vector< ConversationMember > members() const
bool resolveBan(const std::string_view type, const std::string &uri)
bool checkValidAdd(const std::string &userDevice, const std::string &uriMember, const std::string &commitid, const std::string &parentId) const
bool checkValidJoins(const std::string &userDevice, const std::string &uriMember, const std::string &commitid, const std::string &parentId) const
bool resolveConflicts(git_index *index, const std::string &other_id)
std::mutex deviceToUriMtx_
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_
GitRepository repository() const
bool mergeFastforward(const git_oid *target_oid, int is_unborn)
bool checkValidUserDiff(const std::string &userDevice, const std::string &commitId, const std::string &parentId) const
std::map< std::string, std::vector< DeviceId > > devices(bool ignoreExpired=true) const
Get conversation's devices.
std::string leave()
Erase self from repository.
static LIBJAMI_TEST_EXPORT std::unique_ptr< ConversationRepository > createConversation(const std::shared_ptr< JamiAccount > &account, ConversationMode mode=ConversationMode::INVITES_ONLY, const std::string &otherMember="")
Creates a new repository, with initial files, where the first commit hash is the conversation id.
std::vector< std::string > commitMessages(const std::vector< std::string > &msgs)
std::string addMember(const std::string &uri)
Write the certificate in /members and commit the change.
void removeBranchWith(const std::string &remoteDevice)
Delete branch with remote.
std::string mergeBase(const std::string &from, const std::string &to) const
Get the common parent between two branches.
bool fetch(const std::string &remoteDeviceId)
Fetch a remote repository via the given socket.
std::optional< ConversationCommit > getCommit(const std::string &commitId, bool logIfNotFound=true) const
void erase()
Erase repository.
std::string getHead() const
Get current HEAD hash.
std::string commitMessage(const std::string &msg, bool verifyDevice=true)
Add a new commit to the conversation.
std::map< std::string, std::string > infos() const
Retrieve current infos (title, description, avatar, mode)
bool validClone(std::function< void(std::vector< ConversationCommit >)> &&checkCommitCb) const
std::set< std::string > memberUris(std::string_view filter, const std::set< MemberRole > &filteredRoles) const
static std::map< std::string, std::string > infosFromVCard(std::map< std::string, std::string > &&details)
static std::vector< std::string > changedFiles(std::string_view diffStats)
Get changed files from a git diff.
ConversationMode mode() const
Get conversation's mode.
static LIBJAMI_TEST_EXPORT std::unique_ptr< ConversationRepository > cloneConversation(const std::shared_ptr< JamiAccount > &account, const std::string &deviceId, const std::string &conversationId, std::function< void(std::vector< ConversationCommit >)> &&checkCommitCb={})
Clones a conversation on a remote device.
std::string updateInfos(const std::map< std::string, std::string > &map)
Change repository's infos.
std::string amend(const std::string &id, const std::string &msg)
Amend a commit message.
void refreshMembers() const
To use after a merge with member's events, refresh members knowledge.
std::string voteKick(const std::string &uri, const std::string &type)
The voting system is divided in two parts.
std::vector< std::map< std::string, std::string > > convCommitsToMap(const std::vector< ConversationCommit > &commits) const
Convert ConversationCommit to MapStringString for the client.
std::string join()
Join a repository.
~ConversationRepository()
void onMembersChanged(OnMembersChanged &&cb)
const std::string & id() const
Return the conversation id.
std::string remoteHead(const std::string &remoteDeviceId, const std::string &branch="main") const
Retrieve remote head.
std::vector< ConversationCommit > log(const LogOptions &options={}) const
Get commits depending on the options we pass.
std::string resolveVote(const std::string &uri, const std::string_view type, const std::string &voteType)
Validate if a vote is finished.
std::string voteUnban(const std::string &uri, const std::string_view type)
Add a vote to re-add a user.
std::pair< std::vector< ConversationCommit >, bool > validFetch(const std::string &remoteDevice) const
Validate a fetch with remote device.
std::string uriFromDevice(const std::string &deviceId) const
Retrieve the uri from a deviceId.
void pinCertificates(bool blocking=false)
Because conversations can contains non contacts certificates, this methods loads certificates in conv...
std::vector< ConversationMember > members() const
Get conversation's members.
std::vector< std::string > getInitialMembers() const
One to one util, get initial members.
std::pair< bool, std::string > merge(const std::string &merge_id, bool force=false)
Merge another branch into the main branch.
std::optional< std::map< std::string, std::string > > convCommitToMap(const ConversationCommit &commit) const
std::string diffStats(const std::string &newId, const std::string &oldId="") const
Get current diff stats between two commits.
constexpr auto DIFF_REGEX
constexpr size_t MAX_FETCH_SIZE
std::unique_ptr< git_object, decltype(&git_object_free)> GitObject
std::unique_ptr< git_commit, decltype(&git_commit_free)> GitCommit
std::unique_ptr< git_index_conflict_iterator, decltype(&git_index_conflict_iterator_free)> GitIndexConflictIterator
std::unique_ptr< git_repository, decltype(&git_repository_free)> GitRepository
std::unique_ptr< git_index, decltype(&git_index_free)> GitIndex
std::unique_ptr< git_signature, decltype(&git_signature_free)> GitSignature
std::unique_ptr< git_diff_stats, decltype(&git_diff_stats_free)> GitDiffStats
std::unique_ptr< git_revwalk, decltype(&git_revwalk_free)> GitRevWalker
std::unique_ptr< git_remote, decltype(&git_remote_free)> GitRemote
std::unique_ptr< git_tree, decltype(&git_tree_free)> GitTree
std::unique_ptr< git_reference, decltype(&git_reference_free)> GitReference
std::unique_ptr< git_diff, decltype(&git_diff_free)> GitDiff
std::unique_ptr< git_annotated_commit, decltype(&git_annotated_commit_free)> GitAnnotatedCommit
#define JAMI_ERROR(formatstr,...)
#define JAMI_DEBUG(formatstr,...)
#define JAMI_WARNING(formatstr,...)
#define JAMI_LOG(formatstr,...)
std::vector< uint8_t > decode(std::string_view str)
std::string encode(std::string_view dat)
const std::filesystem::path & get_data_dir()
std::vector< uint8_t > loadFile(const std::filesystem::path &path, const std::filesystem::path &default_dir)
Read the full content of a file at path.
std::string_view getFileExtension(std::string_view filename)
std::filesystem::path getFullPath(const std::filesystem::path &base, const std::filesystem::path &path)
If path is relative, it is appended to base.
std::string toString(const Json::Value &jsonVal)
bool parse(std::string_view jsonStr, Json::Value &jsonVal)
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)
std::function< bool(const std::string &, const GitAuthor &, ConversationCommit &)> PostConditionCb
constexpr auto EINVALIDMODE
bool add_initial_files(GitRepository &repo, const std::shared_ptr< JamiAccount > &account, ConversationMode mode, const std::string &otherMember="")
Adds initial files.
constexpr auto EVALIDFETCH
bool git_add_all(git_repository *repo)
Add all files to index.
constexpr auto EUNAUTHORIZED
bool getline(std::string_view &str, std::string_view &line, char delim='\n')
Similar to @getline_full but skips empty results.
static const std::regex regex_display_name("<|>")
std::string_view as_view(const git_blob *blob)
std::function< CallbackResult(const std::string &, const GitAuthor &, const GitCommit &)> PreConditionCb
std::function< void(const std::set< std::string > &)> OnMembersChanged
bool regex_match(string_view sv, svmatch &m, const regex &e, regex_constants::match_flag_type flags=regex_constants::match_default)
match_results< string_view::const_iterator > svmatch
bool regex_search(string_view sv, svmatch &m, const regex &e, regex_constants::match_flag_type flags=regex_constants::match_default)
std::map< std::string, std::string > toMap(std::string_view content)
Payload to vCard.
std::vector< uint8_t > signed_content
std::vector< std::string > parents
std::vector< uint8_t > signature
std::string linearized_parent
static constexpr std::string_view END_LINE_TOKEN
static constexpr std::string_view BEGIN_TOKEN
static constexpr std::string_view END_TOKEN
static constexpr std::string_view SEPARATOR_TOKEN
static constexpr std::string_view RDV_DEVICE
static constexpr std::string_view BASE64
static constexpr std::string_view FORMATTED_NAME
static constexpr std::string_view DESCRIPTION
static constexpr std::string_view PHOTO
static constexpr std::string_view VCARD_VERSION
static constexpr std::string_view RDV_ACCOUNT
static constexpr std::string_view RDV_DEVICE
static constexpr std::string_view RDV_ACCOUNT
static constexpr std::string_view DESCRIPTION
static constexpr std::string_view TITLE
static constexpr std::string_view AVATAR