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