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