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