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