Ring Daemon
Loading...
Searching...
No Matches
conversation.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18#include "conversation.h"
19
20#include "account_const.h"
21#include "jamiaccount.h"
22#include "client/jami_signal.h"
23#include "swarm/swarm_manager.h"
25#ifdef ENABLE_PLUGIN
26#include "manager.h"
28#include "plugin/streamdata.h"
29#endif
32
33#include "fileutils.h"
34#include "json_utils.h"
35#include "logger.h"
36#include "presence_manager.h"
37
38#include <opendht/thread_pool.h>
39#include <opendht/infohash.h>
40#include <fmt/compile.h>
41#include <asio/error_code.hpp>
42
43#include <algorithm>
44#include <memory>
45#include <charconv>
46#include <string_view>
47#include <tuple>
48#include <optional>
49#include <utility>
50#include <deque>
51#include <chrono>
52#include <ctime>
53#include <functional>
54#include <atomic>
55#include <regex>
56
57namespace jami {
58
59static const char* const LAST_MODIFIED = "lastModified";
60
61ConvInfo::ConvInfo(const Json::Value& json)
62{
63 id = json[ConversationMapKeys::ID].asString();
64 created = json[ConversationMapKeys::CREATED].asLargestUInt();
65 removed = json[ConversationMapKeys::REMOVED].asLargestUInt();
66 erased = json[ConversationMapKeys::ERASED].asLargestUInt();
67 for (const auto& v : json[ConversationMapKeys::MEMBERS]) {
68 members.emplace(v["uri"].asString());
69 }
71}
72
73Json::Value
75{
76 Json::Value json;
78 json[ConversationMapKeys::CREATED] = Json::Int64(created);
79 if (removed) {
80 json[ConversationMapKeys::REMOVED] = Json::Int64(removed);
81 }
82 if (erased) {
83 json[ConversationMapKeys::ERASED] = Json::Int64(erased);
84 }
85 for (const auto& m : members) {
86 Json::Value member;
87 member["uri"] = m;
89 }
91 return json;
92}
93
94// ConversationRequest
96{
97 received = json[ConversationMapKeys::RECEIVED].asLargestUInt();
98 declined = json[ConversationMapKeys::DECLINED].asLargestUInt();
99 from = json[ConversationMapKeys::FROM].asString();
102 for (const auto& member : md.getMemberNames()) {
103 metadatas.emplace(member, md[member].asString());
104 }
105}
106
107Json::Value
109{
110 Json::Value json;
113 json[ConversationMapKeys::RECEIVED] = static_cast<uint32_t>(received);
114 if (declined)
115 json[ConversationMapKeys::DECLINED] = static_cast<uint32_t>(declined);
116 for (const auto& [key, value] : metadatas) {
117 json[ConversationMapKeys::METADATAS][key] = value;
118 }
119 return json;
120}
121
122std::map<std::string, std::string>
124{
125 auto result = metadatas;
128 if (declined)
129 result[ConversationMapKeys::DECLINED] = std::to_string(declined);
130 result[ConversationMapKeys::RECEIVED] = std::to_string(received);
131 return result;
132}
133
134using MessageList = std::list<std::shared_ptr<libjami::SwarmMessage>>;
135
137{
138 // While loading the history, we need to avoid:
139 // - reloading history (can just be ignored)
140 // - adding new commits (should wait for history to be loaded)
141 std::mutex mutex {};
142 std::condition_variable cv {};
143 bool loading {false};
145 std::map<std::string, std::shared_ptr<libjami::SwarmMessage>> quickAccess {};
146 std::map<std::string, std::list<std::shared_ptr<libjami::SwarmMessage>>> pendingEditions {};
147 std::map<std::string, std::list<std::map<std::string, std::string>>> pendingReactions {};
148};
149
151{
152private:
153 Impl(std::unique_ptr<ConversationRepository>&& repository,
154 const std::shared_ptr<JamiAccount>& account,
155 std::vector<ConversationCommit>&& commits = {})
156 : repository_(std::move(repository ? repository : throw std::logic_error("Invalid repository")))
158 , accountId_(account->getAccountID())
159 , userId_(account->getUsername())
160 , deviceId_(account->currentDeviceId())
161 , swarmManager_(std::make_shared<SwarmManager>(NodeId(deviceId_),
162 Manager::instance().getSeededRandomEngine(),
163 [account = account_](const DeviceId& deviceId) {
164 if (auto acc = account.lock()) {
165 return acc->isConnectedWith(deviceId);
166 }
167 return false;
168 }))
169 , transferManager_(std::make_shared<TransferManager>(accountId_,
170 "",
171 repository_->id(),
173 , repoPath_(fileutils::get_data_dir() / accountId_ / "conversations" / repository_->id())
174 , conversationDataPath_(fileutils::get_data_dir() / accountId_ / "conversation_data" / repository_->id())
181 , ioContext_(Manager::instance().ioContext())
182 , typers_(std::make_shared<Typers>(account, repository_->id()))
183 {
184 if (!commits.empty())
185 initActiveCalls(repository_->convCommitsToMap(commits));
186 loadStatus();
188 }
189
190 Impl(std::pair<std::unique_ptr<ConversationRepository>, std::vector<ConversationCommit>>&& repoAndCommits,
191 const std::shared_ptr<JamiAccount>& account)
192 : Impl(std::move(repoAndCommits.first), account, std::move(repoAndCommits.second))
193 {}
194
195public:
196 Impl(const std::shared_ptr<JamiAccount>& account, ConversationMode mode, const std::string& otherMember = "")
197 : Impl(ConversationRepository::createConversation(account, mode, otherMember), account)
198 {}
199
200 Impl(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId)
202 {}
203
204 Impl(const std::shared_ptr<JamiAccount>& account, const std::string& remoteDevice, const std::string& conversationId)
205 : Impl(ConversationRepository::cloneConversation(account, remoteDevice, conversationId), account)
206 {}
207
208 std::string toString() const
209 {
210 return fmt::format(FMT_COMPILE("[Account {}] [Conversation {}]"), accountId_, repository_->id());
211 }
212
213 std::vector<std::map<std::string, std::string>> getConnectivity() const
214 {
215 return swarmManager_->getRoutingTableInfo();
216 }
217
218 mutable std::string fmtStr_;
219
221
227 std::vector<std::string> commitsEndedCalls();
228 bool isAdmin() const;
229
230 void announce(const std::string& commitId, bool commitFromSelf = false)
231 {
232 std::vector<std::string> vec;
233 if (!commitId.empty())
234 vec.emplace_back(commitId);
236 }
237
238 void announce(const std::vector<std::string>& commits, bool commitFromSelf = false)
239 {
240 std::vector<ConversationCommit> convcommits;
241 convcommits.reserve(commits.size());
242 for (const auto& cid : commits) {
243 if (auto commit = repository_->getCommit(cid)) {
244 convcommits.emplace_back(*commit);
245 }
246 }
247 announce(repository_->convCommitsToMap(convcommits), commitFromSelf);
248 }
249
254 void initActiveCalls(const std::vector<std::map<std::string, std::string>>& commits) const
255 {
256 std::unordered_set<std::string> invalidHostUris;
257 std::unordered_set<std::string> invalidCallIds;
258
259 std::lock_guard lk(activeCallsMtx_);
260 for (const auto& commit : commits) {
261 if (commit.at("type") == "member") {
262 // Each commit of type "member" has an "action" field whose value can be one
263 // of the following: "add", "join", "remove", "ban", "unban"
264 // In the case of "remove" and "ban", we need to add the member's URI to
265 // invalidHostUris to ensure that any call they may have started in the past
266 // is no longer considered active.
267 // For the other actions, there's no harm in adding the member's URI anyway,
268 // since it's not possible to start hosting a call before joining the swarm (or
269 // before getting unbanned in the case of previously banned members).
270 invalidHostUris.emplace(commit.at("uri"));
271 } else if (commit.find("confId") != commit.end() && commit.find("uri") != commit.end()
272 && commit.find("device") != commit.end()) {
273 // The commit indicates either the end or the beginning of a call
274 // (depending on whether there is a "duration" field or not).
275 auto convId = repository_->id();
276 auto confId = commit.at("confId");
277 auto uri = commit.at("uri");
278 auto device = commit.at("device");
279
280 if (commit.find("duration") == commit.end() && invalidCallIds.find(confId) == invalidCallIds.end()
281 && invalidHostUris.find(uri) == invalidHostUris.end()) {
282 std::map<std::string, std::string> activeCall;
283 activeCall["id"] = confId;
284 activeCall["uri"] = uri;
285 activeCall["device"] = device;
286 activeCalls_.emplace_back(activeCall);
287 fmt::print("swarm:{} new active call detected: {} (on device {}, account {})\n",
288 convId,
289 confId,
290 device,
291 uri);
292 }
293 // Even if the call was active, we still add its ID to invalidCallIds to make sure it
294 // doesn't get added a second time. (This shouldn't happen normally, but in practice
295 // there are sometimes multiple commits indicating the beginning of the same call.)
296 invalidCallIds.emplace(confId);
297 }
298 }
301 }
302
310 void updateActiveCalls(const std::map<std::string, std::string>& commit,
311 bool eraseOnly = false,
312 bool emitSig = true) const
313 {
314 if (!repository_)
315 return;
316 if (commit.at("type") == "member") {
317 // In this case, we need to check if we are not removing a hosting member or device
318 std::lock_guard lk(activeCallsMtx_);
319 auto it = activeCalls_.begin();
320 auto updateActives = false;
321 while (it != activeCalls_.end()) {
322 if (it->at("uri") == commit.at("uri") || it->at("device") == commit.at("uri")) {
323 JAMI_DEBUG("Removing {:s} from the active calls, because {:s} left", it->at("id"), commit.at("uri"));
324 it = activeCalls_.erase(it);
325 updateActives = true;
326 } else {
327 ++it;
328 }
329 }
330 if (updateActives) {
332 if (emitSig)
334 repository_->id(),
336 }
337 return;
338 }
339 // Else, it's a call information
340 if (commit.find("confId") != commit.end() && commit.find("uri") != commit.end()
341 && commit.find("device") != commit.end()) {
342 auto convId = repository_->id();
343 auto confId = commit.at("confId");
344 auto uri = commit.at("uri");
345 auto device = commit.at("device");
346 std::lock_guard lk(activeCallsMtx_);
347 auto itActive = std::find_if(activeCalls_.begin(), activeCalls_.end(), [&](const auto& value) {
348 return value.at("id") == confId && value.at("uri") == uri && value.at("device") == device;
349 });
350 if (commit.find("duration") == commit.end()) {
351 if (itActive == activeCalls_.end() && !eraseOnly) {
352 JAMI_DEBUG("swarm:{:s} new current call detected: {:s} on device {:s}, account {:s}",
353 convId,
354 confId,
355 device,
356 uri);
357 activeCalls_.emplace_back(std::map<std::string, std::string> {
358 {"id", confId},
359 {"uri", uri},
360 {"device", device},
361 });
363 if (emitSig)
365 repository_->id(),
367 }
368 } else {
369 if (itActive != activeCalls_.end()) {
371 // Unlikely, but we must ensure that no duplicate exists
372 while (itActive != activeCalls_.end()) {
373 itActive = std::find_if(itActive, activeCalls_.end(), [&](const auto& value) {
374 return value.at("id") == confId && value.at("uri") == uri && value.at("device") == device;
375 });
376 if (itActive != activeCalls_.end()) {
377 JAMI_ERROR("Duplicate call found. (This is a bug)");
379 }
380 }
381
382 if (eraseOnly) {
383 JAMI_WARNING("previous swarm:{:s} call finished detected: {:s} on device "
384 "{:s}, account {:s}",
385 convId,
386 confId,
387 device,
388 uri);
389 } else {
390 JAMI_DEBUG("swarm:{:s} call finished: {:s} on device {:s}, account {:s}",
391 convId,
392 confId,
393 device,
394 uri);
395 }
396 }
398 if (emitSig)
400 repository_->id(),
402 }
403 }
404 }
405
406 void announce(const std::vector<std::map<std::string, std::string>>& commits, bool commitFromSelf = false)
407 {
408 if (!repository_)
409 return;
410 auto convId = repository_->id();
411 auto ok = !commits.empty();
412 auto lastId = ok ? commits.rbegin()->at(ConversationMapKeys::ID) : "";
414 if (ok) {
415 bool announceMember = false;
416 for (const auto& c : commits) {
417 // Announce member events
418 if (c.at("type") == "member") {
419 if (c.find("uri") != c.end() && c.find("action") != c.end()) {
420 const auto& uri = c.at("uri");
421 const auto& actionStr = c.at("action");
422 auto action = -1;
423 if (actionStr == "add")
424 action = 0;
425 else if (actionStr == "join")
426 action = 1;
427 else if (actionStr == "remove")
428 action = 2;
429 else if (actionStr == "ban")
430 action = 3;
431 else if (actionStr == "unban")
432 action = 4;
433 if (actionStr == "ban" || actionStr == "remove") {
434 // In this case, a potential host was removed during a call.
436 typers_->removeTyper(uri);
437 }
438 if (action != -1) {
439 announceMember = true;
441 convId,
442 uri,
443 action);
444 }
445 }
446 } else if (c.at("type") == "application/call-history+json") {
448 }
449#ifdef ENABLE_PLUGIN
450 auto& pluginChatManager = Manager::instance().getJamiPluginManager().getChatServicesManager();
451 if (pluginChatManager.hasHandlers()) {
452 auto cm = std::make_shared<JamiMessage>(accountId_, convId, c.at("author") != userId_, c, false);
453 cm->isSwarm = true;
454 pluginChatManager.publishMessage(std::move(cm));
455 }
456#endif
457 }
458
460 onMembersChanged_(repository_->memberUris("", {}));
461 }
462 }
463 }
464
466 {
467 try {
468 // read file
470 // load values
471 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
472 std::lock_guard lk {messageStatusMtx_};
473 oh.get().convert(messagesStatus_);
474 } catch (const std::exception& e) {
475 }
476 }
478 {
479 std::ofstream file(statusPath_, std::ios::trunc | std::ios::binary);
480 msgpack::pack(file, messagesStatus_);
481 }
482
483 void loadActiveCalls() const
484 {
485 try {
486 // read file
488 // load values
489 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
490 std::lock_guard lk {activeCallsMtx_};
491 oh.get().convert(activeCalls_);
492 } catch (const std::exception& e) {
493 return;
494 }
495 }
496
497 void saveActiveCalls() const
498 {
499 std::ofstream file(activeCallsPath_, std::ios::trunc | std::ios::binary);
500 msgpack::pack(file, activeCalls_);
501 }
502
503 void loadHostedCalls() const
504 {
505 try {
506 // read file
508 // load values
509 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
510 std::lock_guard lk {activeCallsMtx_};
511 oh.get().convert(hostedCalls_);
512 } catch (const std::exception& e) {
513 return;
514 }
515 }
516
517 void saveHostedCalls() const
518 {
519 std::ofstream file(hostedCallsPath_, std::ios::trunc | std::ios::binary);
520 msgpack::pack(file, hostedCalls_);
521 }
522
523 void voteUnban(const std::string& contactUri, const std::string_view type, const OnDoneCb& cb);
524
525 std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited,
526 bool includeLeft,
527 bool includeBanned) const;
528
529 std::vector<std::map<std::string, std::string>> getTrackedMembers() const;
530
531 std::string_view bannedType(const std::string& uri) const
532 {
533 auto crt = fmt::format("{}.crt", uri);
535 if (std::filesystem::is_regular_file(bannedMember))
536 return "members"sv;
538 if (std::filesystem::is_regular_file(bannedAdmin))
539 return "admins"sv;
541 if (std::filesystem::is_regular_file(bannedInvited))
542 return "invited"sv;
544 if (std::filesystem::is_regular_file(bannedDevice))
545 return "devices"sv;
546 return {};
547 }
548
549 std::shared_ptr<dhtnet::ChannelSocket> gitSocket(const DeviceId& deviceId) const
550 {
551 auto deviceSockets = gitSocketList_.find(deviceId);
552 return (deviceSockets != gitSocketList_.end()) ? deviceSockets->second : nullptr;
553 }
554
555 void addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket)
556 {
557 gitSocketList_[deviceId] = socket;
558 }
559 void removeGitSocket(const DeviceId& deviceId)
560 {
561 auto deviceSockets = gitSocketList_.find(deviceId);
562 if (deviceSockets != gitSocketList_.end())
564 }
565
571 void disconnectFromPeer(const std::string& peerUri);
572
573 std::vector<std::map<std::string, std::string>> getMembers(bool includeInvited, bool includeLeft) const;
574
575 std::function<void()> bootstrapCb_;
576 std::mutex bootstrapMtx_ {};
577#ifdef LIBJAMI_TEST
578 std::function<void(std::string, BootstrapStatus)> bootstrapCbTest_;
579#endif
580
581 std::mutex writeMtx_ {};
582 const std::unique_ptr<ConversationRepository> repository_;
583 const std::weak_ptr<JamiAccount> account_;
584 const std::string accountId_;
585 const std::string userId_;
586 const std::string deviceId_;
587 const std::shared_ptr<SwarmManager> swarmManager_;
588 std::atomic_bool isRemoving_ {false};
589 std::vector<libjami::SwarmMessage> loadMessages(const LogOptions& options, History* optHistory = nullptr);
590 void pull(const std::string& deviceId);
591
592 // Avoid multiple fetch/merges at the same time.
593 std::mutex pullcbsMtx_ {};
594 // store current remote in fetch
595 std::map<std::string, std::deque<std::pair<std::string, OnPullCb>>> fetchingRemotes_ {};
596 const std::shared_ptr<TransferManager> transferManager_ {};
597 const std::filesystem::path repoPath_ {};
598 const std::filesystem::path conversationDataPath_ {};
599 const std::filesystem::path fetchedPath_ {};
600
601 // Manage last message displayed and status
602 const std::filesystem::path sendingPath_ {};
603 const std::filesystem::path preferencesPath_ {};
604 const std::filesystem::path statusPath_ {};
605
608 {
609 std::set<DeviceId> devices;
610 std::set<DeviceId> failedDevices;
611 };
612 std::map<std::string, TrackedMember> trackedMembers_;
613 mutable std::mutex trackedMembersMtx_;
614 bool isTracking_ {false};
615 void setupMemberCallback();
616 void startTracking(std::weak_ptr<Conversation> w);
617 void stopTracking();
618 void rotateTrackedMembers(const std::string& memberUri = "", const DeviceId& deviceId = {});
619 void monitorConnection(std::weak_ptr<Conversation> w);
620 void onConnectionFailed(const DeviceId& deviceId, const std::string& memberUri = "");
621
623
624 // Manage hosted calls on this device
625 std::filesystem::path hostedCallsPath_ {};
626 mutable std::map<std::string, uint64_t /* start time */> hostedCalls_ {};
627 // Manage active calls for this conversation (can be hosted by other devices)
628 std::filesystem::path activeCallsPath_ {};
629 mutable std::mutex activeCallsMtx_ {};
630 mutable std::vector<std::map<std::string, std::string>> activeCalls_ {};
631
633
634 // Bootstrap
635 const std::shared_ptr<asio::io_context> ioContext_;
636
641 std::vector<std::shared_ptr<libjami::SwarmMessage>> addToHistory(
643 const std::vector<std::map<std::string, std::string>>& commits,
644 bool messageReceived = false,
645 bool commitFromSelf = false);
646
647 void handleReaction(History& history, const std::shared_ptr<libjami::SwarmMessage>& sharedCommit) const;
649 const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
650 bool messageReceived) const;
652 const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
653 bool messageReceived) const;
654 void rectifyStatus(const std::shared_ptr<libjami::SwarmMessage>& message, History& history) const;
664 mutable std::mutex messageStatusMtx_;
665 std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)> messageStatusCb_ {};
666 std::map<std::string, std::map<std::string, std::string>> messagesStatus_ {};
671 // Note: only store int32_t cause it's easy to pass to dbus this way
672 // memberToStatus serves as a cache for loading messages
673 std::map<std::string, int32_t> memberToStatus;
674
675 // futureStatus is used to store the status for receiving messages
676 // (because we're not sure to fetch the commit before receiving a status change for this)
677 std::map<std::string, std::map<std::string, int32_t>> futureStatus;
678 // Update internal structures regarding status
679 void updateStatus(const std::string& uri,
681 const std::string& commitId,
682 const std::string& ts,
683 bool emit = false);
684
685 std::shared_ptr<Typers> typers_;
686};
687
688void
690{
691 repository_->onMembersChanged([this](const std::set<std::string>& memberUris) {
692 {
693 std::lock_guard lk(trackedMembersMtx_);
694 if (isTracking_) {
695 if (auto acc = account_.lock()) {
696 for (auto it = trackedMembers_.begin(); it != trackedMembers_.end();) {
697 if (memberUris.find(it->first) == memberUris.end()) {
698 acc->presenceManager()->untrackBuddy(it->first);
699 it = trackedMembers_.erase(it);
700 } else {
701 ++it;
702 }
703 }
704 }
705 }
706 }
707
710 });
711}
712
713void
714Conversation::Impl::startTracking(std::weak_ptr<Conversation> w)
715{
716 auto acc = account_.lock();
717 if (!acc)
718 return;
719
720 {
721 std::lock_guard lk(trackedMembersMtx_);
722 if (isTracking_)
723 return;
724 isTracking_ = true;
725 presenceDeviceListenerToken_ = acc->presenceManager()->addDeviceListener(
726 [w](const std::string& uri, const DeviceId& deviceId, bool online) {
727 if (auto sthis = w.lock()) {
728 if (online && sthis->isMember(uri)) {
729 sthis->addKnownDevices({deviceId}, uri);
730 }
731 }
732 });
733 }
734
735 rotateTrackedMembers();
736}
737
738void
740{
741 // Collect data under trackedMembersMtx_, then release it before calling
742 // into PresenceManager to avoid lock-ordering inversion with the
743 // PresenceManager mutex (which is held when notifyListeners calls
744 // addKnownDevices, which in turn acquires trackedMembersMtx_).
745 uint64_t token = 0;
746 std::vector<std::string> urisToUntrack;
747 {
748 std::lock_guard lk(trackedMembersMtx_);
749 if (!isTracking_)
750 return;
751 isTracking_ = false;
754 for (const auto& [uri, _] : trackedMembers_) {
755 urisToUntrack.push_back(uri);
756 }
757 trackedMembers_.clear();
758 }
759
760 auto acc = account_.lock();
761 if (!acc)
762 return;
763
764 if (token) {
765 acc->presenceManager()->removeDeviceListener(token);
766 }
767
768 for (const auto& uri : urisToUntrack) {
769 acc->presenceManager()->untrackBuddy(uri);
770 }
771}
772
773void
775{
776 auto acc = account_.lock();
777 if (!acc)
778 return;
779
780 std::lock_guard lk(trackedMembersMtx_);
781 if (!isTracking_)
782 return;
783
784 if (!memberUri.empty()) {
785 if (auto it = trackedMembers_.find(memberUri); it != trackedMembers_.end()) {
786 JAMI_WARNING("{} [device {}] Rotating tracked members after connection failure", toString(), deviceId);
787 auto& info = it->second;
788 info.failedDevices.insert(deviceId);
789 if (std::includes(info.failedDevices.begin(),
790 info.failedDevices.end(),
791 info.devices.begin(),
792 info.devices.end())) {
793 acc->presenceManager()->untrackBuddy(it->first);
794 trackedMembers_.erase(it);
795 }
796 } else {
797 return;
798 }
799 }
800
801 auto members = repository_->members();
802 size_t N = members.size();
803 size_t K = std::min(N, 3 + (size_t) std::log2(N));
804
805 // If we are already trying enough devices, don't rotate
806 auto activeDevices = swarmManager_->getActiveNodesCount();
807 if (activeDevices >= 2 * K)
808 return;
809
810 JAMI_WARNING("{} Refreshing tracked members: {}/{} active ({}/{} devices)",
811 toString(),
812 trackedMembers_.size(),
813 K,
815 2 * K);
816
817 // Add new members if we have space
818 if (trackedMembers_.size() < K) {
819 std::vector<std::string> candidates;
820 candidates.reserve(N - trackedMembers_.size());
821 for (const auto& m : members) {
822 if (m.uri != memberUri && trackedMembers_.find(m.uri) == trackedMembers_.end()) {
823 candidates.push_back(m.uri);
824 }
825 }
826 if (!candidates.empty()) {
827 std::vector<std::string> chosen;
828 std::sample(candidates.begin(),
829 candidates.end(),
830 std::back_inserter(chosen),
831 K - trackedMembers_.size(),
833 for (const auto& uri : chosen) {
834 acc->presenceManager()->trackBuddy(uri);
835 trackedMembers_.emplace(uri, TrackedMember {});
836 }
837 }
838 }
839}
840
841void
843{
844 rotateTrackedMembers(memberUri, deviceId);
845}
846
847void
848Conversation::Impl::monitorConnection(std::weak_ptr<Conversation> w)
849{
850 if (!swarmManager_->isConnected()) {
851 startTracking(w);
852 }
853
854 swarmManager_->onConnectionChanged([w](bool ok) {
855 dht::ThreadPool::io().run([w, ok] {
856 if (auto sthis = w.lock()) {
857 // Check the current connection state rather than relying on the
858 // captured `ok` value, because the thread pool does not guarantee
859 // execution order. Two rapid state changes (connected then
860 // disconnected) could otherwise cause stopTracking to run after
861 // startTracking, leaving the conversation untracked while the DRT
862 // has no connected nodes.
863 if (sthis->pimpl_->swarmManager_->isConnected()) {
864 sthis->pimpl_->stopTracking();
865 } else {
866 sthis->pimpl_->startTracking(w);
867 }
868 if (ok) {
869 if (sthis->pimpl_->bootstrapCb_)
870 sthis->pimpl_->bootstrapCb_();
871 }
872#ifdef LIBJAMI_TEST
873 if (sthis->pimpl_->bootstrapCbTest_)
874 sthis->pimpl_->bootstrapCbTest_(sthis->id(),
875 ok ? BootstrapStatus::SUCCESS : BootstrapStatus::FAILED);
876#endif
877 }
878 });
879 });
880}
881
882bool
884{
885 auto adminsPath = repoPath_ / MemberPath::ADMINS;
886 return std::filesystem::is_regular_file(fileutils::getFullPath(adminsPath, userId_ + ".crt"));
887}
888
889void
890Conversation::Impl::disconnectFromPeer(const std::string& peerUri)
891{
892 // Remove nodes from swarmManager
893 const auto nodes = swarmManager_->getAllNodes();
894 std::vector<NodeId> toRemove;
895 for (const auto node : nodes)
896 if (peerUri == repository_->uriFromDevice(node.toString()))
897 toRemove.emplace_back(node);
898 swarmManager_->deleteNode(toRemove);
899
900 // Remove git sockets with this member
901 for (auto it = gitSocketList_.begin(); it != gitSocketList_.end();) {
902 if (peerUri == repository_->uriFromDevice(it->first.toString()))
903 it = gitSocketList_.erase(it);
904 else
905 ++it;
906 }
907}
908
909std::vector<std::map<std::string, std::string>>
911{
912 std::vector<std::map<std::string, std::string>> result;
913 auto members = repository_->members();
914 std::lock_guard lk(messageStatusMtx_);
915 for (const auto& member : members) {
916 if (member.role == MemberRole::BANNED && !includeBanned) {
917 continue;
918 }
920 continue;
921 if (member.role == MemberRole::LEFT && !includeLeft)
922 continue;
923 auto mm = member.map();
924 auto it = messagesStatus_.find(member.uri);
925 if (it != messagesStatus_.end()) {
926 auto readIt = it->second.find("read");
927 if (readIt != it->second.end())
929 }
930 result.emplace_back(std::move(mm));
931 }
932 return result;
933}
934
935std::vector<std::map<std::string, std::string>>
937{
938 std::vector<std::map<std::string, std::string>> result;
939 std::lock_guard lk(trackedMembersMtx_);
940 for (const auto& [uri, member] : trackedMembers_) {
941 std::map<std::string, std::string> map;
942 map["uri"] = uri;
943 std::string devicesStr;
944 for (const auto& dev : member.devices) {
945 if (!devicesStr.empty())
946 devicesStr += ";";
947 devicesStr += dev.toString();
948 }
949 map["devices"] = devicesStr;
950 result.emplace_back(std::move(map));
951 }
952 return result;
953}
954
955std::vector<std::string>
957{
958 // Handle current calls
959 std::vector<std::string> commits {};
960 std::unique_lock lk(writeMtx_);
961 std::unique_lock lkA(activeCallsMtx_);
962 for (const auto& hostedCall : hostedCalls_) {
963 // In this case, this means that we left
964 // the conference while still hosting it, so activeCalls
965 // will not be correctly updated
966 // We don't need to send notifications there, as peers will sync with presence
967 Json::Value value;
968 value["uri"] = userId_;
969 value["device"] = deviceId_;
970 value["confId"] = hostedCall.first;
971 value["type"] = "application/call-history+json";
972 auto now = std::chrono::system_clock::now();
973 auto nowConverted = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
974 value["duration"] = std::to_string((nowConverted - hostedCall.second) * 1000);
975 auto itActive = std::find_if(activeCalls_.begin(),
976 activeCalls_.end(),
977 [this, confId = hostedCall.first](const auto& value) {
978 return value.at("id") == confId && value.at("uri") == userId_
979 && value.at("device") == deviceId_;
980 });
981 if (itActive != activeCalls_.end())
982 activeCalls_.erase(itActive);
983 commits.emplace_back(repository_->commitMessage(json::toString(value)));
984
985 JAMI_DEBUG("Removing hosted conference... {:s}", hostedCall.first);
986 }
987 hostedCalls_.clear();
988 saveActiveCalls();
989 saveHostedCalls();
990 return commits;
991}
992
993std::vector<libjami::SwarmMessage>
995{
996 auto history = optHistory ? optHistory : &loadedHistory_;
997
998 // history->mutex is locked by the caller
999 if (!repository_ || history->loading) {
1000 return {};
1001 }
1002 history->loading = true;
1003
1004 // By convention, if options.nbOfCommits is zero, then we
1005 // don't impose a limit on the number of commits returned.
1006 bool limitNbOfCommits = options.nbOfCommits > 0;
1007
1008 auto startLogging = options.from == "";
1009 auto breakLogging = false;
1010 auto currentHistorySize = loadedHistory_.messageList.size();
1011 std::vector<std::string> replies;
1012 std::vector<std::shared_ptr<libjami::SwarmMessage>> msgList;
1013 repository_->log(
1014 /* preCondition */
1015 [&](const auto& id, const auto& author, const auto& commit) {
1016 if (options.skipMerge && git_commit_parentcount(commit.get()) > 1) {
1017 return CallbackResult::Skip;
1018 }
1019 if (id == options.to) {
1020 if (options.includeTo)
1021 breakLogging = true; // For the next commit
1022 }
1023 if (replies.empty()) { // This avoid load until
1024 // NOTE: in the future, we may want to add "Reply-Body" in commit to avoid to load
1025 // until this commit
1026 if ((limitNbOfCommits
1027 && (loadedHistory_.messageList.size() - currentHistorySize) == options.nbOfCommits))
1028 return CallbackResult::Break; // Stop logging
1029 if (breakLogging)
1030 return CallbackResult::Break; // Stop logging
1031 if (id == options.to && !options.includeTo) {
1032 return CallbackResult::Break; // Stop logging
1033 }
1034 }
1035
1036 if (!startLogging && options.from != "" && options.from == id)
1037 startLogging = true;
1038 if (!startLogging)
1039 return CallbackResult::Skip; // Start logging after this one
1040
1041 if (options.fastLog) {
1042 if (options.authorUri != "") {
1043 if (options.authorUri == repository_->uriFromDevice(author.email)) {
1044 return CallbackResult::Break; // Found author, stop
1045 }
1046 }
1047 }
1048
1049 return CallbackResult::Ok; // Continue
1050 },
1051 /* emplaceCb */
1052 [&](auto&& cc) {
1053 if (limitNbOfCommits && (msgList.size() == options.nbOfCommits))
1054 return;
1055 auto optMessage = repository_->convCommitToMap(cc);
1056 if (!optMessage.has_value())
1057 return;
1058 auto message = optMessage.value();
1059 if (message.find("reply-to") != message.end()) {
1060 auto it = std::find(replies.begin(), replies.end(), message.at("reply-to"));
1061 if (it == replies.end()) {
1062 replies.emplace_back(message.at("reply-to"));
1063 }
1064 }
1065 auto it = std::find(replies.begin(), replies.end(), message.at("id"));
1066 if (it != replies.end()) {
1067 replies.erase(it);
1068 }
1069 std::shared_ptr<libjami::SwarmMessage> firstMsg;
1070 if ((history == &loadedHistory_) && msgList.empty() && !loadedHistory_.messageList.empty()) {
1071 firstMsg = *loadedHistory_.messageList.rbegin();
1072 }
1073 auto added = addToHistory(*history, {message}, false, false);
1074 if (!added.empty() && firstMsg) {
1076 }
1077 msgList.insert(msgList.end(), added.begin(), added.end());
1078 },
1079 /* postCondition */
1080 [&](auto, auto, auto) {
1081 // Stop logging if there was a limit set on the number of commits
1082 // to return and we reached it. This isn't strictly necessary since
1083 // the check at the beginning of `emplaceCb` ensures that we won't
1084 // return too many messages, but it prevents us from needlessly
1085 // iterating over a (potentially) large number of commits.
1086 return limitNbOfCommits && (msgList.size() == options.nbOfCommits);
1087 },
1088 options.from,
1089 options.logIfNotFound);
1090
1091 history->loading = false;
1092 history->cv.notify_all();
1093
1094 // Convert for client (remove ptr)
1095 std::vector<libjami::SwarmMessage> ret;
1096 ret.reserve(msgList.size());
1097 for (const auto& msg : msgList) {
1098 ret.emplace_back(*msg);
1099 }
1100 return ret;
1101}
1102
1103void
1104Conversation::Impl::handleReaction(History& history, const std::shared_ptr<libjami::SwarmMessage>& sharedCommit) const
1105{
1106 auto it = history.quickAccess.find(sharedCommit->body.at("react-to"));
1107 auto peditIt = history.pendingEditions.find(sharedCommit->id);
1108 if (peditIt != history.pendingEditions.end()) {
1109 auto oldBody = sharedCommit->body;
1110 sharedCommit->body["body"] = peditIt->second.front()->body["body"];
1111 if (sharedCommit->body.at("body").empty())
1112 return;
1113 history.pendingEditions.erase(peditIt);
1114 }
1115 if (it != history.quickAccess.end()) {
1116 it->second->reactions.emplace_back(sharedCommit->body);
1118 repository_->id(),
1119 it->second->id,
1120 sharedCommit->body);
1121 } else {
1122 history.pendingReactions[sharedCommit->body.at("react-to")].emplace_back(sharedCommit->body);
1123 }
1124}
1125
1126void
1128 const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
1129 bool messageReceived) const
1130{
1131 auto editId = sharedCommit->body.at("edit");
1132 auto it = history.quickAccess.find(editId);
1133 if (it != history.quickAccess.end()) {
1134 auto baseCommit = it->second;
1135 if (baseCommit) {
1136 auto itReact = baseCommit->body.find("react-to");
1137 std::string toReplace = (baseCommit->type == "application/data-transfer+json") ? "tid" : "body";
1138 auto body = sharedCommit->body.at(toReplace);
1139 // Edit reaction
1140 if (itReact != baseCommit->body.end()) {
1141 baseCommit->body[toReplace] = body; // Replace body if pending
1142 it = history.quickAccess.find(itReact->second);
1143 auto itPending = history.pendingReactions.find(itReact->second);
1144 if (it != history.quickAccess.end()) {
1145 baseCommit = it->second; // Base commit
1146 auto itPreviousReact = std::find_if(baseCommit->reactions.begin(),
1147 baseCommit->reactions.end(),
1148 [&](const auto& reaction) {
1149 return reaction.at("id") == editId;
1150 });
1151 if (itPreviousReact != baseCommit->reactions.end()) {
1152 (*itPreviousReact)[toReplace] = body;
1153 if (body.empty()) {
1154 baseCommit->reactions.erase(itPreviousReact);
1156 repository_->id(),
1157 baseCommit->id,
1158 editId);
1159 }
1160 }
1161 } else if (itPending != history.pendingReactions.end()) {
1162 // Else edit if pending
1163 auto itReaction = std::find_if(itPending->second.begin(),
1164 itPending->second.end(),
1165 [&](const auto& reaction) { return reaction.at("id") == editId; });
1166 if (itReaction != itPending->second.end()) {
1167 (*itReaction)[toReplace] = body;
1168 if (body.empty())
1169 itPending->second.erase(itReaction);
1170 }
1171 } else {
1172 // Add to pending edtions
1173 messageReceived ? history.pendingEditions[editId].emplace_front(sharedCommit)
1174 : history.pendingEditions[editId].emplace_back(sharedCommit);
1175 }
1176 } else {
1177 // Normal message
1178 it->second->editions.emplace(it->second->editions.begin(), it->second->body);
1179 it->second->body[toReplace] = sharedCommit->body[toReplace];
1180 if (toReplace == "tid") {
1181 // Avoid to replace fileId in client
1182 it->second->body["fileId"] = "";
1183 }
1184 // Remove reactions
1185 if (sharedCommit->body.at(toReplace).empty())
1186 it->second->reactions.clear();
1187 emitSignal<libjami::ConversationSignal::SwarmMessageUpdated>(accountId_, repository_->id(), *it->second);
1188 }
1189 }
1190 } else {
1191 messageReceived ? history.pendingEditions[editId].emplace_front(sharedCommit)
1192 : history.pendingEditions[editId].emplace_back(sharedCommit);
1193 }
1194}
1195
1196bool
1198 const std::shared_ptr<libjami::SwarmMessage>& sharedCommit,
1199 bool messageReceived) const
1200{
1201 if (messageReceived) {
1202 // For a received message, we place it at the beginning of the list
1203 if (!history.messageList.empty())
1204 sharedCommit->linearizedParent = (*history.messageList.begin())->id;
1205 history.messageList.emplace_front(sharedCommit);
1206 } else {
1207 // For a loaded message, we load from newest to oldest
1208 // So we change the parent of the last message.
1209 if (!history.messageList.empty())
1210 (*history.messageList.rbegin())->linearizedParent = sharedCommit->id;
1211 history.messageList.emplace_back(sharedCommit);
1212 }
1213 // Handle pending reactions/editions
1214 auto reactIt = history.pendingReactions.find(sharedCommit->id);
1215 if (reactIt != history.pendingReactions.end()) {
1216 for (const auto& commitBody : reactIt->second)
1217 sharedCommit->reactions.emplace_back(commitBody);
1218 history.pendingReactions.erase(reactIt);
1219 }
1220 auto peditIt = history.pendingEditions.find(sharedCommit->id);
1221 if (peditIt != history.pendingEditions.end()) {
1222 auto oldBody = sharedCommit->body;
1223 if (sharedCommit->type == "application/data-transfer+json") {
1224 sharedCommit->body["tid"] = peditIt->second.front()->body["tid"];
1225 sharedCommit->body["fileId"] = "";
1226 } else {
1227 sharedCommit->body["body"] = peditIt->second.front()->body["body"];
1228 }
1229 peditIt->second.pop_front();
1230 for (const auto& commit : peditIt->second) {
1231 sharedCommit->editions.emplace_back(commit->body);
1232 }
1233 sharedCommit->editions.emplace_back(oldBody);
1234 history.pendingEditions.erase(peditIt);
1235 }
1236 // Announce to client
1237 if (messageReceived)
1239 return !messageReceived;
1240}
1241
1242void
1243Conversation::Impl::rectifyStatus(const std::shared_ptr<libjami::SwarmMessage>& message, History& history) const
1244{
1245 auto parentIt = history.quickAccess.find(message->linearizedParent);
1246 auto currentMessage = message;
1247
1248 while (parentIt != history.quickAccess.end()) {
1249 const auto& parent = parentIt->second;
1250 for (const auto& [peer, value] : message->status) {
1251 auto parentStatusIt = parent->status.find(peer);
1252 if (parentStatusIt == parent->status.end() || parentStatusIt->second < value) {
1253 parent->status[peer] = value;
1255 repository_->id(),
1256 peer,
1257 parent->id,
1258 value);
1259 } else if (parentStatusIt->second >= value) {
1260 break;
1261 }
1262 }
1264 parentIt = history.quickAccess.find(parent->linearizedParent);
1265 }
1266}
1267
1268std::vector<std::shared_ptr<libjami::SwarmMessage>>
1270 const std::vector<std::map<std::string, std::string>>& commits,
1271 bool messageReceived,
1272 bool commitFromSelf)
1273{
1274 //
1275 // NOTE: This function makes the following assumptions on its arguments:
1276 // - The messages in "history" are in reverse chronological order (newest message
1277 // first, oldest message last).
1278 // - If messageReceived is true, then the commits in "commits" are assumed to be in
1279 // chronological order (oldest to newest) and to be newer than the ones in "history".
1280 // They are therefore inserted at the beginning of the message list.
1281 // - If messageReceived is false, then the commits in "commits" are assumed to be in
1282 // reverse chronological order (newest to oldest) and to be older than the ones in
1283 // "history". They are therefore appended at the end of the message list.
1284 //
1285 auto acc = account_.lock();
1286 if (!acc)
1287 return {};
1288 auto username = acc->getUsername();
1289 if (messageReceived && (&history == &loadedHistory_ && history.loading)) {
1290 std::unique_lock lk(history.mutex);
1291 history.cv.wait(lk, [&] { return !history.loading; });
1292 }
1293
1294 // Only set messages' status on history for client
1295 bool needToSetMessageStatus = !commitFromSelf && &history == &loadedHistory_;
1296
1297 std::vector<std::shared_ptr<libjami::SwarmMessage>> sharedCommits;
1298 for (const auto& commit : commits) {
1299 auto commitId = commit.at("id");
1300 if (history.quickAccess.find(commitId) != history.quickAccess.end())
1301 continue; // Already present
1302 auto typeIt = commit.find("type");
1303 // Nothing to show for the client, skip
1304 if (typeIt != commit.end() && typeIt->second == "merge")
1305 continue;
1306
1307 auto sharedCommit = std::make_shared<libjami::SwarmMessage>();
1308 sharedCommit->fromMapStringString(commit);
1309
1311 std::lock_guard lk(messageStatusMtx_);
1312 // Check if we already have status information for the commit.
1313 auto itFuture = futureStatus.find(sharedCommit->id);
1314 if (itFuture != futureStatus.end()) {
1315 sharedCommit->status = std::move(itFuture->second);
1316 futureStatus.erase(itFuture);
1317 }
1318 }
1319 sharedCommits.emplace_back(sharedCommit);
1320 }
1321
1323 constexpr int32_t SENDING = static_cast<int32_t>(libjami::Account::MessageStates::SENDING);
1324 constexpr int32_t SENT = static_cast<int32_t>(libjami::Account::MessageStates::SENT);
1325 constexpr int32_t DISPLAYED = static_cast<int32_t>(libjami::Account::MessageStates::DISPLAYED);
1326
1327 std::lock_guard lk(messageStatusMtx_);
1328 for (const auto& member : repository_->members()) {
1329 // For each member, we iterate over the commits to add in reverse chronological
1330 // order (i.e. from newest to oldest) and set their status from the point of view
1331 // of that member (as best we can given the information we have).
1332 //
1333 // The key assumption made in order to compute the status is that it can never decrease
1334 // (with respect to the ordering SENDING < SENT < DISPLAYED) as we go back in time. We
1335 // therefore start by setting the "status" variable below to the lowest possible value,
1336 // and increase it when we encounter a commit for which it is justified to do so.
1337 //
1338 // If messageReceived is true, then the commits we are adding are the most recent in the
1339 // conversation history, so the lowest possible value is SENDING.
1340 //
1341 // If messageReceived is false, then the commits we are adding are older than the ones
1342 // that are already in the history, so the lowest possible value is the status of the
1343 // oldest message in the history so far, which is stored in memberToStatus.
1344 auto status = SENDING;
1345 if (!messageReceived) {
1346 auto cache = memberToStatus[member.uri];
1347 if (cache > status)
1348 status = cache;
1349 }
1350 auto& messagesStatus = messagesStatus_[member.uri];
1351
1352 for (auto it = sharedCommits.rbegin(); it != sharedCommits.rend(); it++) {
1353 auto sharedCommit = *it;
1354 auto previousStatus = status;
1355 auto& commitStatus = sharedCommit->status[member.uri];
1356
1357 // Compute status for the current commit.
1358 if (status < SENT && messagesStatus["fetched"] == sharedCommit->id) {
1359 status = SENT;
1360 }
1361 if (messagesStatus["read"] == sharedCommit->id) {
1362 status = DISPLAYED;
1363 }
1364 if (member.uri == sharedCommit->body.at("author")) {
1365 status = DISPLAYED;
1366 }
1367 if (status < commitStatus) {
1368 status = commitStatus;
1369 }
1370
1371 // Store computed value.
1372 commitStatus = status;
1373
1374 // Update messagesStatus_ if needed.
1375 if (previousStatus == SENDING && status >= SENT) {
1376 messagesStatus["fetched"] = sharedCommit->id;
1377 }
1378 if (previousStatus <= SENT && status == DISPLAYED) {
1379 messagesStatus["read"] = sharedCommit->id;
1380 }
1381 }
1382
1383 if (!messageReceived) {
1384 // Update memberToStatus with the status of the last (i.e. oldest) added commit.
1385 memberToStatus[member.uri] = status;
1386 }
1387 }
1388 }
1389
1390 std::vector<std::shared_ptr<libjami::SwarmMessage>> messages;
1391 for (const auto& sharedCommit : sharedCommits) {
1392 history.quickAccess[sharedCommit->id] = sharedCommit;
1393
1394 auto reactToIt = sharedCommit->body.find("react-to");
1395 auto editIt = sharedCommit->body.find("edit");
1396 if (reactToIt != sharedCommit->body.end() && !reactToIt->second.empty()) {
1397 handleReaction(history, sharedCommit);
1398 } else if (editIt != sharedCommit->body.end() && !editIt->second.empty()) {
1399 handleEdition(history, sharedCommit, messageReceived);
1400 } else if (handleMessage(history, sharedCommit, messageReceived)) {
1401 messages.emplace_back(sharedCommit);
1402 }
1403 rectifyStatus(sharedCommit, history);
1404 }
1405
1406 return messages;
1407}
1408
1409Conversation::Conversation(const std::shared_ptr<JamiAccount>& account,
1411 const std::string& otherMember)
1412 : pimpl_ {new Impl {account, mode, otherMember}}
1413{}
1414
1415Conversation::Conversation(const std::shared_ptr<JamiAccount>& account, const std::string& conversationId)
1416 : pimpl_ {new Impl {account, conversationId}}
1417{}
1418
1419Conversation::Conversation(const std::shared_ptr<JamiAccount>& account,
1420 const std::string& remoteDevice,
1421 const std::string& conversationId)
1422 : pimpl_ {new Impl {account, remoteDevice, conversationId}}
1423{}
1424
1426
1427std::string
1429{
1430 return pimpl_->repository_ ? pimpl_->repository_->id() : "";
1431}
1432
1433void
1435{
1436 try {
1438 // Only authorize to add left members
1440 auto it = std::find(initialMembers.begin(), initialMembers.end(), contactUri);
1441 if (it == initialMembers.end()) {
1442 JAMI_WARN("Unable to add new member in one to one conversation");
1443 cb(false, "");
1444 return;
1445 }
1446 }
1447 } catch (const std::exception& e) {
1448 JAMI_WARN("Unable to get mode: %s", e.what());
1449 cb(false, "");
1450 return;
1451 }
1452 if (isMember(contactUri, true)) {
1453 JAMI_WARN("Unable to add member %s because it's already a member", contactUri.c_str());
1454 cb(false, "");
1455 return;
1456 }
1457 if (isBanned(contactUri)) {
1458 if (pimpl_->isAdmin()) {
1459 dht::ThreadPool::io().run([w = weak(), contactUri = std::move(contactUri), cb = std::move(cb)] {
1460 if (auto sthis = w.lock()) {
1461 auto members = sthis->pimpl_->repository_->members();
1462 auto type = sthis->pimpl_->bannedType(contactUri);
1463 if (type.empty()) {
1464 cb(false, {});
1465 return;
1466 }
1467 sthis->pimpl_->voteUnban(contactUri, type, cb);
1468 }
1469 });
1470 } else {
1471 JAMI_WARN("Unable to add member %s because this member is blocked", contactUri.c_str());
1472 cb(false, "");
1473 }
1474 return;
1475 }
1476
1477 dht::ThreadPool::io().run([w = weak(), contactUri = std::move(contactUri), cb = std::move(cb)] {
1478 if (auto sthis = w.lock()) {
1479 // Add member files and commit
1480 std::unique_lock lk(sthis->pimpl_->writeMtx_);
1481 auto commit = sthis->pimpl_->repository_->addMember(contactUri);
1482 if (not commit.empty())
1483 sthis->pimpl_->announce(commit, true);
1484 lk.unlock();
1485 if (cb)
1486 cb(!commit.empty(), commit);
1487 }
1488 });
1489}
1490
1491std::shared_ptr<dhtnet::ChannelSocket>
1492Conversation::gitSocket(const DeviceId& deviceId) const
1493{
1494 return pimpl_->gitSocket(deviceId);
1495}
1496
1497void
1498Conversation::addGitSocket(const DeviceId& deviceId, const std::shared_ptr<dhtnet::ChannelSocket>& socket)
1499{
1500 pimpl_->addGitSocket(deviceId, socket);
1501}
1502
1503void
1505{
1506 pimpl_->removeGitSocket(deviceId);
1507}
1508
1509void
1511{
1512 pimpl_->gitSocketList_.clear();
1513 if (pimpl_->swarmManager_)
1514 pimpl_->swarmManager_->shutdown();
1515}
1516
1517void
1519{
1520 if (pimpl_->swarmManager_)
1521 pimpl_->swarmManager_->maintainBuckets();
1522}
1523
1524std::vector<jami::DeviceId>
1526{
1527 return pimpl_->swarmManager_->getAllNodes();
1528}
1529
1530std::shared_ptr<Typers>
1532{
1533 return pimpl_->typers_;
1534}
1535
1536std::vector<std::map<std::string, std::string>>
1538{
1539 return pimpl_->getConnectivity();
1540}
1541
1542bool
1543Conversation::hasSwarmChannel(const std::string& deviceId)
1544{
1545 if (!pimpl_->swarmManager_)
1546 return false;
1547 return pimpl_->swarmManager_->isConnectedWith(DeviceId(deviceId));
1548}
1549
1550void
1551Conversation::Impl::voteUnban(const std::string& contactUri, const std::string_view type, const OnDoneCb& cb)
1552{
1553 // Check if admin
1554 if (!isAdmin()) {
1555 JAMI_WARN("You're not an admin of this repo. Unable to unblock %s", contactUri.c_str());
1556 cb(false, {});
1557 return;
1558 }
1559
1560 // Vote for removal
1561 std::unique_lock lk(writeMtx_);
1562 auto voteCommit = repository_->voteUnban(contactUri, type);
1563 if (voteCommit.empty()) {
1564 JAMI_WARN("Unbanning %s failed", contactUri.c_str());
1565 cb(false, "");
1566 return;
1567 }
1568
1569 auto lastId = voteCommit;
1570 std::vector<std::string> commits;
1571 commits.emplace_back(voteCommit);
1572
1573 // If admin, check vote
1574 auto resolveCommit = repository_->resolveVote(contactUri, type, "unban");
1575 if (!resolveCommit.empty()) {
1576 commits.emplace_back(resolveCommit);
1577 lastId = resolveCommit;
1578 JAMI_WARN("Vote solved for unbanning %s.", contactUri.c_str());
1579 }
1580 announce(commits, true);
1581 lk.unlock();
1582 if (cb)
1583 cb(!lastId.empty(), lastId);
1584}
1585
1586void
1588{
1589 dht::ThreadPool::io().run(
1590 [w = weak(), contactUri = std::move(contactUri), isDevice = std::move(isDevice), cb = std::move(cb)] {
1591 if (auto sthis = w.lock()) {
1592 // Check if admin
1593 if (!sthis->pimpl_->isAdmin()) {
1594 JAMI_WARN("You're not an admin of this repo. Unable to block %s", contactUri.c_str());
1595 cb(false, {});
1596 return;
1597 }
1598
1599 // Get current user type
1600 std::string type;
1601 if (isDevice) {
1602 type = "devices";
1603 } else {
1604 auto members = sthis->pimpl_->repository_->members();
1605 for (const auto& member : members) {
1606 if (member.uri == contactUri) {
1607 if (member.role == MemberRole::INVITED) {
1608 type = "invited";
1609 } else if (member.role == MemberRole::ADMIN) {
1610 type = "admins";
1611 } else if (member.role == MemberRole::MEMBER) {
1612 type = "members";
1613 }
1614 break;
1615 }
1616 }
1617 if (type.empty()) {
1618 cb(false, {});
1619 return;
1620 }
1621 }
1622
1623 // Vote for removal
1624 std::unique_lock lk(sthis->pimpl_->writeMtx_);
1625 auto voteCommit = sthis->pimpl_->repository_->voteKick(contactUri, type);
1626 if (voteCommit.empty()) {
1627 JAMI_WARN("Kicking %s failed", contactUri.c_str());
1628 cb(false, "");
1629 return;
1630 }
1631
1632 auto lastId = voteCommit;
1633 std::vector<std::string> commits;
1634 commits.emplace_back(voteCommit);
1635
1636 // If admin, check vote
1637 auto resolveCommit = sthis->pimpl_->repository_->resolveVote(contactUri, type, "ban");
1638 if (!resolveCommit.empty()) {
1639 commits.emplace_back(resolveCommit);
1640 lastId = resolveCommit;
1641 JAMI_WARN("Vote solved for %s. %s banned", contactUri.c_str(), isDevice ? "Device" : "Member");
1642 sthis->pimpl_->disconnectFromPeer(contactUri);
1643 }
1644
1645 sthis->pimpl_->announce(commits, true);
1646 lk.unlock();
1647 cb(!lastId.empty(), lastId);
1648 }
1649 });
1650}
1651
1652std::vector<std::map<std::string, std::string>>
1654{
1655 return pimpl_->getMembers(includeInvited, includeLeft, includeBanned);
1656}
1657
1658std::vector<std::map<std::string, std::string>>
1660{
1661 return pimpl_->getTrackedMembers();
1662}
1663
1664std::set<std::string>
1665Conversation::memberUris(std::string_view filter, const std::set<MemberRole>& filteredRoles) const
1666{
1667 return pimpl_->repository_->memberUris(filter, filteredRoles);
1668}
1669
1670std::vector<NodeId>
1672{
1673 auto s = pimpl_->swarmManager_->getConnectedNodes();
1674 for (const auto& [deviceId, _] : pimpl_->gitSocketList_)
1675 if (std::find(s.cbegin(), s.cend(), deviceId) == s.cend())
1676 s.emplace_back(deviceId);
1677 return s;
1678}
1679
1680bool
1682{
1683 return pimpl_->swarmManager_->isConnected();
1684}
1685
1686std::string
1687Conversation::uriFromDevice(const std::string& deviceId) const
1688{
1689 return pimpl_->repository_->uriFromDevice(deviceId);
1690}
1691
1692void
1694{
1695 pimpl_->swarmManager_->getRoutingTable().printRoutingTable();
1696}
1697
1698std::string
1700{
1701 return pimpl_->repository_->join();
1702}
1703
1704bool
1705Conversation::isMember(const std::string& uri, bool includeInvited) const
1706{
1707 auto uriCrt = uri + ".crt"sv;
1708 if (std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::ADMINS / uriCrt)
1709 || std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::MEMBERS / uriCrt)) {
1710 return true;
1711 }
1712 if (includeInvited) {
1713 if (std::filesystem::is_regular_file(pimpl_->repoPath_ / MemberPath::INVITED / uri)) {
1714 return true;
1715 }
1717 for (const auto& member : getInitialMembers()) {
1718 if (member == uri)
1719 return true;
1720 }
1721 }
1722 }
1723 return false;
1724}
1725
1726bool
1727Conversation::isBanned(const std::string& uri) const
1728{
1729 return !pimpl_->bannedType(uri).empty();
1730}
1731
1732void
1733Conversation::sendMessage(Json::Value&& value, const std::string& replyTo, OnCommitCb&& onCommit, OnDoneCb&& cb)
1734{
1735 if (!replyTo.empty()) {
1736 if (!pimpl_->repository_->hasCommit(replyTo)) {
1737 JAMI_ERR("Replying to invalid commit %s", replyTo.c_str());
1738 return;
1739 }
1740 value["reply-to"] = replyTo;
1741 }
1742 dht::ThreadPool::io().run(
1743 [w = weak(), value = std::move(value), onCommit = std::move(onCommit), cb = std::move(cb)] {
1744 if (auto sthis = w.lock()) {
1745 std::unique_lock lk(sthis->pimpl_->writeMtx_);
1746 auto commit = sthis->pimpl_->repository_->commitMessage(json::toString(value));
1747 lk.unlock();
1748 if (onCommit)
1749 onCommit(commit);
1750 sthis->pimpl_->announce(commit, true);
1751 if (cb)
1752 cb(!commit.empty(), commit);
1753 }
1754 });
1755}
1756
1757bool
1758Conversation::hasCommit(const std::string& commitId) const
1759{
1760 return pimpl_->repository_->hasCommit(commitId);
1761}
1762
1763std::optional<std::map<std::string, std::string>>
1764Conversation::getCommit(const std::string& commitId) const
1765{
1766 auto commit = pimpl_->repository_->getCommit(commitId);
1767 if (commit == std::nullopt)
1768 return std::nullopt;
1769 return pimpl_->repository_->convCommitToMap(*commit);
1770}
1771
1772void
1774{
1775 if (!cb)
1776 return;
1777 dht::ThreadPool::io().run([w = weak(), cb = std::move(cb), options] {
1778 if (auto sthis = w.lock()) {
1779 std::unique_lock lk(sthis->pimpl_->loadedHistory_.mutex);
1780 auto result = sthis->pimpl_->loadMessages(options);
1781 lk.unlock();
1782 cb(std::move(result));
1783 }
1784 });
1785}
1786
1787void
1789{
1790 std::lock_guard lk(pimpl_->loadedHistory_.mutex);
1791 pimpl_->loadedHistory_.messageList.clear();
1792 pimpl_->loadedHistory_.quickAccess.clear();
1793 pimpl_->loadedHistory_.pendingEditions.clear();
1794 pimpl_->loadedHistory_.pendingReactions.clear();
1795 {
1796 std::lock_guard lk(pimpl_->messageStatusMtx_);
1797 pimpl_->memberToStatus.clear();
1798 }
1799}
1800
1801std::string
1803{
1804 {
1805 std::lock_guard lk(pimpl_->loadedHistory_.mutex);
1806 if (!pimpl_->loadedHistory_.messageList.empty())
1807 return (*pimpl_->loadedHistory_.messageList.begin())->id;
1808 }
1810 options.nbOfCommits = 1;
1811 options.skipMerge = true;
1813 std::scoped_lock lock(pimpl_->writeMtx_, optHistory.mutex);
1814 auto res = pimpl_->loadMessages(options, &optHistory);
1815 if (res.empty())
1816 return {};
1817 return (*optHistory.messageList.begin())->id;
1818}
1819
1820bool
1821Conversation::pull(const std::string& deviceId, OnPullCb&& cb, std::string commitId)
1822{
1823 std::lock_guard lk(pimpl_->pullcbsMtx_);
1824 auto [it, notInProgress] = pimpl_->fetchingRemotes_.emplace(deviceId,
1825 std::deque<std::pair<std::string, OnPullCb>>());
1826 auto& pullcbs = it->second;
1827 auto itPull = std::find_if(pullcbs.begin(), pullcbs.end(), [&](const auto& elem) {
1828 return std::get<0>(elem) == commitId;
1829 });
1830 if (itPull != pullcbs.end()) {
1831 JAMI_DEBUG("{} Ignoring request to pull from {:s} with commit {:s}: pull already in progress",
1832 pimpl_->toString(),
1833 deviceId,
1834 commitId);
1835 cb(false);
1836 return false;
1837 }
1838 JAMI_DEBUG("{} [device {}] Pulling '{:s}'", pimpl_->toString(), deviceId, commitId);
1839 pullcbs.emplace_back(std::move(commitId), std::move(cb));
1840 if (notInProgress)
1841 dht::ThreadPool::io().run([w = weak(), deviceId] {
1842 if (auto sthis_ = w.lock())
1843 sthis_->pimpl_->pull(deviceId);
1844 });
1845 return true;
1846}
1847
1848void
1849Conversation::Impl::pull(const std::string& deviceId)
1850{
1851 auto& repo = repository_;
1852
1853 std::string commitId;
1854 OnPullCb cb;
1855 while (true) {
1856 {
1857 std::lock_guard lk(pullcbsMtx_);
1858 auto it = fetchingRemotes_.find(deviceId);
1859 if (it == fetchingRemotes_.end()) {
1860 JAMI_ERROR("Could not find device {:s} in fetchingRemotes", deviceId);
1861 break;
1862 }
1863 auto& pullcbs = it->second;
1864 if (pullcbs.empty()) {
1865 fetchingRemotes_.erase(it);
1866 break;
1867 }
1868 auto& elem = pullcbs.front();
1869 commitId = std::move(std::get<0>(elem));
1870 cb = std::move(std::get<1>(elem));
1871 pullcbs.pop_front();
1872 }
1873 // If recently fetched, the commit can already be there, so no need to do complex operations
1874 if (commitId != "" && repo->hasCommit(commitId)) {
1875 cb(true);
1876 continue;
1877 }
1878 // Pull from remote
1879 auto fetched = repo->fetch(deviceId);
1880 if (!fetched) {
1881 cb(false);
1882 continue;
1883 }
1884
1885 auto oldHead = repo->getHead();
1886
1887 std::unique_lock lk(writeMtx_);
1888 auto commits = repo->mergeHistory(deviceId,
1889 [this](const std::string& peerUri) { this->disconnectFromPeer(peerUri); });
1890 if (!commits.empty()) {
1891 announce(commits);
1892 }
1893 lk.unlock();
1894
1895 bool commitFound = false;
1896 if (commitId != "") {
1897 // If `commitId` is non-empty, then we were attempting to pull a specific commit.
1898 // We need to check if we actually got it; the fact that the fetch above was
1899 // successful doesn't guarantee that we did.
1900 for (const auto& commit : commits) {
1901 if (commit.at("id") == commitId) {
1902 commitFound = true;
1903 break;
1904 }
1905 }
1906 } else {
1907 commitFound = true;
1908 }
1909 if (!commitFound)
1910 JAMI_WARNING("Successfully fetched from device {} but didn't receive expected commit {}",
1911 deviceId,
1912 commitId);
1913 // WARNING: If its argument is `true`, this callback will attempt to send a message notification
1914 // for commit `commitId` to other members of the swarm. It's important that we only
1915 // send these notifications if we actually have the commit. Otherwise, we can end up
1916 // in a situation where the members of the swarm keep sending notifications to each
1917 // other for a commit that none of them have (note that we are unable to rule this out, as
1918 // nothing prevents a malicious user from intentionally sending a notification with
1919 // a fake commit ID).
1920 if (cb)
1921 cb(commitFound);
1922
1923 // Announce if profile changed
1924 if (!commits.empty()) {
1925 auto diffStats = repo->diffStats("HEAD", oldHead);
1926 auto changedFiles = repo->changedFiles(diffStats);
1927 if (find(changedFiles.begin(), changedFiles.end(), "profile.vcf") != changedFiles.end()) {
1929 repo->id(),
1930 repo->infos());
1931 }
1932 }
1933 }
1934}
1935
1936void
1937Conversation::sync(const std::string& member, const std::string& deviceId, OnPullCb&& cb, std::string commitId)
1938{
1939 pull(deviceId, std::move(cb), commitId);
1940 dht::ThreadPool::io().run([member, deviceId, w = weak_from_this()] {
1941 auto sthis = w.lock();
1942 // For waiting request, downloadFile
1943 for (const auto& wr : sthis->dataTransfer()->waitingRequests()) {
1944 sthis->downloadFile(wr.interactionId, wr.fileId, wr.path, member, deviceId);
1945 }
1946 });
1947}
1948
1949std::map<std::string, std::string>
1951{
1952 // Invite the new member to the conversation
1953 Json::Value root;
1955 for (const auto& [k, v] : infos()) {
1956 if (v.size() >= 64000) {
1957 JAMI_WARNING("Cutting invite because the SIP message will be too long");
1958 continue;
1959 }
1960 metadata[k] = v;
1961 }
1963 return {{"application/invite+json", json::toString(root)}};
1964}
1965
1966std::string
1968{
1970 std::lock_guard lk(pimpl_->writeMtx_);
1971 return pimpl_->repository_->leave();
1972}
1973
1974void
1976{
1977 pimpl_->isRemoving_ = true;
1978}
1979
1980bool
1982{
1983 return pimpl_->isRemoving_;
1984}
1985
1986void
1988{
1989 if (pimpl_->conversationDataPath_ != "")
1990 dhtnet::fileutils::removeAll(pimpl_->conversationDataPath_, true);
1991 if (!pimpl_->repository_)
1992 return;
1993 std::lock_guard lk(pimpl_->writeMtx_);
1994 pimpl_->repository_->erase();
1995}
1996
1999{
2000 return pimpl_->repository_->mode();
2001}
2002
2003std::vector<std::string>
2005{
2006 return pimpl_->repository_->getInitialMembers();
2007}
2008
2009bool
2010Conversation::isInitialMember(const std::string& uri) const
2011{
2012 auto members = getInitialMembers();
2013 return std::find(members.begin(), members.end(), uri) != members.end();
2014}
2015
2016void
2017Conversation::updateInfos(const std::map<std::string, std::string>& map, const OnDoneCb& cb)
2018{
2019 dht::ThreadPool::io().run([w = weak(), map = std::move(map), cb = std::move(cb)] {
2020 if (auto sthis = w.lock()) {
2021 auto& repo = sthis->pimpl_->repository_;
2022 std::unique_lock lk(sthis->pimpl_->writeMtx_);
2023 auto commit = repo->updateInfos(map);
2024 sthis->pimpl_->announce(commit, true);
2025 lk.unlock();
2026 if (cb)
2027 cb(!commit.empty(), commit);
2028 emitSignal<libjami::ConversationSignal::ConversationProfileUpdated>(sthis->pimpl_->accountId_,
2029 repo->id(),
2030 repo->infos());
2031 }
2032 });
2033}
2034
2035std::map<std::string, std::string>
2037{
2038 return pimpl_->repository_->infos();
2039}
2040
2041void
2042Conversation::updatePreferences(const std::map<std::string, std::string>& map)
2043{
2044 const auto& filePath = pimpl_->preferencesPath_;
2045 auto prefs = map;
2046 auto itLast = prefs.find(LAST_MODIFIED);
2047 if (itLast != prefs.end()) {
2048 std::error_code ec;
2049 if (std::filesystem::is_regular_file(filePath, ec)) {
2051 try {
2052 if (lastModified >= to_int<uint64_t>(itLast->second))
2053 return;
2054 } catch (...) {
2055 return;
2056 }
2057 }
2058 prefs.erase(itLast);
2059 }
2060
2061 std::ofstream file(filePath, std::ios::trunc | std::ios::binary);
2062 msgpack::pack(file, prefs);
2064}
2065
2066std::map<std::string, std::string>
2068{
2069 try {
2070 std::map<std::string, std::string> preferences;
2071 const auto& filePath = pimpl_->preferencesPath_;
2073 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
2074 oh.get().convert(preferences);
2077 return preferences;
2078 } catch (const std::exception& e) {
2079 }
2080 return {};
2081}
2082
2083std::vector<uint8_t>
2085{
2086 try {
2087 return fileutils::loadFile(pimpl_->repoPath_ / "profile.vcf");
2088 } catch (...) {
2089 }
2090 return {};
2091}
2092
2093std::shared_ptr<TransferManager>
2095{
2096 return pimpl_->transferManager_;
2097}
2098
2099bool
2101 const std::string& fileId,
2102 std::filesystem::path& path,
2103 std::string& sha3sum) const
2104{
2105 if (!isMember(member))
2106 return false;
2107
2108 auto sep = fileId.find('_');
2109 if (sep == std::string::npos)
2110 return false;
2111
2112 auto interactionId = fileId.substr(0, sep);
2113 auto commit = getCommit(interactionId);
2114 if (commit == std::nullopt || commit->find("type") == commit->end() || commit->find("tid") == commit->end()
2115 || commit->find("sha3sum") == commit->end() || commit->at("type") != "application/data-transfer+json") {
2116 JAMI_WARNING("[Account {:s}] {} requested invalid file transfer commit {}",
2117 pimpl_->accountId_,
2118 member,
2119 interactionId);
2120 return false;
2121 }
2122
2123 path = dataTransfer()->path(fileId);
2124 sha3sum = commit->at("sha3sum");
2125
2126 return true;
2127}
2128
2129bool
2130Conversation::downloadFile(const std::string& interactionId,
2131 const std::string& fileId,
2132 const std::string& path,
2133 const std::string&,
2134 const std::string& deviceId)
2135{
2136 auto commit = getCommit(interactionId);
2137 if (commit == std::nullopt || commit->at("type") != "application/data-transfer+json") {
2138 JAMI_ERROR("Commit doesn't exists or is not a file transfer {} (Conversation: {}) ", interactionId, id());
2139 return false;
2140 }
2141 auto tid = commit->find("tid");
2142 auto sha3sum = commit->find("sha3sum");
2143 auto size_str = commit->find("totalSize");
2144
2145 if (tid == commit->end() || sha3sum == commit->end() || size_str == commit->end()) {
2146 JAMI_ERROR("Invalid file transfer commit (missing tid, size or sha3)");
2147 return false;
2148 }
2149
2150 auto totalSize = to_int<ssize_t>(size_str->second, (ssize_t) -1);
2151 if (totalSize < 0) {
2152 JAMI_ERROR("Invalid file size {}", totalSize);
2153 return false;
2154 }
2155
2156 // Be sure to not lock conversation
2157 dht::ThreadPool().io().run(
2158 [w = weak(), deviceId, fileId, interactionId, sha3sum = sha3sum->second, path, totalSize] {
2159 if (auto shared = w.lock()) {
2160 std::filesystem::path filePath(path);
2161 if (filePath.empty()) {
2162 filePath = shared->dataTransfer()->path(fileId);
2163 }
2164
2165 std::error_code ec;
2166 if (std::filesystem::file_size(filePath, ec) == static_cast<size_t>(totalSize)) {
2167 if (fileutils::sha3File(filePath) == sha3sum) {
2168 JAMI_WARNING("Ignoring request to download existing file: {}", filePath);
2169 return;
2170 }
2171 }
2172
2173 std::filesystem::path tempFilePath(filePath);
2174 tempFilePath += ".tmp";
2175 auto start = std::filesystem::file_size(tempFilePath, ec);
2176 if (ec || start == static_cast<decltype(start)>(-1)) {
2177 start = 0;
2178 }
2179 size_t end = 0;
2180
2181 auto acc = shared->pimpl_->account_.lock();
2182 if (!acc)
2183 return;
2184 shared->dataTransfer()->waitForTransfer(fileId, interactionId, sha3sum, path, totalSize);
2185 acc->askForFileChannel(shared->id(), deviceId, interactionId, fileId, start, end);
2186 }
2187 });
2188 return true;
2189}
2190
2191void
2192Conversation::hasFetched(const std::string& deviceId, const std::string& commitId)
2193{
2194 dht::ThreadPool::io().run([w = weak(), deviceId, commitId]() {
2195 auto sthis = w.lock();
2196 if (!sthis)
2197 return;
2198 // Update fetched for Uri
2199 auto uri = sthis->uriFromDevice(deviceId);
2200 if (uri.empty() || uri == sthis->pimpl_->userId_)
2201 return;
2202 // When a user fetches a commit, the message is sent for this person
2203 sthis->pimpl_->updateStatus(uri,
2205 commitId,
2206 std::to_string(std::time(nullptr)),
2207 true);
2208 });
2209}
2210
2211void
2214 const std::string& commitId,
2215 const std::string& ts,
2216 bool emit)
2217{
2218 // This method can be called if peer send us a status or if another device sync. Emit will be true if a peer
2219 // send us a status and will emit to other connected devices.
2221 std::map<std::string, std::map<std::string, std::string>> newStatus;
2222 {
2223 // Update internal structures.
2224 std::lock_guard lk(messageStatusMtx_);
2225 auto& status = messagesStatus_[uri];
2226 auto& oldStatus = status[st == libjami::Account::MessageStates::SENT ? "fetched" : "read"];
2227 if (oldStatus == commitId)
2228 return; // Nothing to do
2229 options.to = oldStatus;
2230 options.from = commitId;
2232 status[st == libjami::Account::MessageStates::SENT ? "fetched_ts" : "read_ts"] = ts;
2233 saveStatus();
2234 if (emit)
2235 newStatus[uri].insert(status.begin(), status.end());
2236 }
2237 if (emit && messageStatusCb_) {
2238 messageStatusCb_(newStatus);
2239 }
2240 // Update messages status for all commit between the old and new one
2241 options.logIfNotFound = false;
2242 options.fastLog = true;
2244 std::unique_lock lk(optHistory.mutex); // Avoid to announce messages while updating status.
2246 std::unique_lock mlk(messageStatusMtx_);
2247 std::vector<std::pair<std::string, int32_t>> statusToUpdate;
2248 if (res.size() == 0) {
2249 // In this case, commit is not received yet, so we cache it
2250 futureStatus[commitId][uri] = static_cast<int32_t>(st);
2251 }
2252 for (const auto& [cid, _] : optHistory.quickAccess) {
2253 auto message = loadedHistory_.quickAccess.find(cid);
2254 if (message != loadedHistory_.quickAccess.end()) {
2255 // Update message and emit to client,
2256 if (static_cast<int32_t>(st) > message->second->status[uri]) {
2257 message->second->status[uri] = static_cast<int32_t>(st);
2258 statusToUpdate.emplace_back(cid, static_cast<int32_t>(st));
2259 }
2260 } else {
2261 // In this case, commit is not loaded by client, so we cache it
2262 // No need to emit to client, they will get a correct status on load.
2263 futureStatus[cid][uri] = static_cast<int32_t>(st);
2264 }
2265 }
2266 mlk.unlock();
2267 lk.unlock();
2268 for (const auto& [cid, status] : statusToUpdate)
2270 repository_->id(),
2271 uri,
2272 cid,
2273 static_cast<int>(status));
2274}
2275
2276bool
2277Conversation::setMessageDisplayed(const std::string& uri, const std::string& interactionId)
2278{
2279 std::lock_guard lk(pimpl_->messageStatusMtx_);
2280 if (pimpl_->messagesStatus_[uri]["read"] == interactionId)
2281 return false; // Nothing to do
2282 dht::ThreadPool::io().run([w = weak(), uri, interactionId]() {
2283 auto sthis = w.lock();
2284 if (!sthis)
2285 return;
2286 sthis->pimpl_->updateStatus(uri,
2288 interactionId,
2289 std::to_string(std::time(nullptr)),
2290 true);
2291 });
2292 return true;
2293}
2294
2295std::map<std::string, std::map<std::string, std::string>>
2297{
2298 std::lock_guard lk(pimpl_->messageStatusMtx_);
2299 return pimpl_->messagesStatus_;
2300}
2301
2302void
2303Conversation::updateMessageStatus(const std::map<std::string, std::map<std::string, std::string>>& messageStatus)
2304{
2305 std::unique_lock lk(pimpl_->messageStatusMtx_);
2306 std::vector<std::tuple<libjami::Account::MessageStates, std::string, std::string, std::string>> stVec;
2307 for (const auto& [uri, status] : messageStatus) {
2308 auto& oldMs = pimpl_->messagesStatus_[uri];
2309 if (status.find("fetched_ts") != status.end() && status.at("fetched") != oldMs["fetched"]) {
2310 if (oldMs["fetched_ts"].empty() || std::stol(oldMs["fetched_ts"]) <= std::stol(status.at("fetched_ts"))) {
2312 uri,
2313 status.at("fetched"),
2314 status.at("fetched_ts"));
2315 }
2316 }
2317 if (status.find("read_ts") != status.end() && status.at("read") != oldMs["read"]) {
2318 if (oldMs["read_ts"].empty() || std::stol(oldMs["read_ts"]) <= std::stol(status.at("read_ts"))) {
2320 uri,
2321 status.at("read"),
2322 status.at("read_ts"));
2323 }
2324 }
2325 }
2326 lk.unlock();
2327
2328 for (const auto& [status, uri, commitId, ts] : stVec) {
2329 pimpl_->updateStatus(uri, status, commitId, ts);
2330 }
2331}
2332
2333void
2335 const std::function<void(const std::map<std::string, std::map<std::string, std::string>>&)>& cb)
2336{
2337 std::unique_lock lk(pimpl_->messageStatusMtx_);
2338 pimpl_->messageStatusCb_ = cb;
2339}
2340
2341#ifdef LIBJAMI_TEST
2342void
2343Conversation::onBootstrapStatus(const std::function<void(std::string, BootstrapStatus)>& cb)
2344{
2345 std::lock_guard lock(pimpl_->bootstrapMtx_);
2346 pimpl_->bootstrapCbTest_ = cb;
2347}
2348
2349std::vector<libjami::SwarmMessage>
2350Conversation::loadMessagesSync(const LogOptions& options)
2351{
2352 std::lock_guard lk(pimpl_->loadedHistory_.mutex);
2353 auto result = pimpl_->loadMessages(options);
2354 return result;
2355}
2356
2357void
2358Conversation::announce(const std::vector<std::map<std::string, std::string>>& commits, bool commitFromSelf)
2359{
2360 pimpl_->announce(commits, commitFromSelf);
2361}
2362
2363void
2364Conversation::announce(const std::string& commitId, bool commitFromSelf)
2365{
2366 pimpl_->announce(commitId, commitFromSelf);
2367}
2368#endif
2369
2370void
2371Conversation::bootstrap(std::function<void()> onBootstrapped, const std::vector<DeviceId>& knownDevices)
2372{
2373 std::lock_guard lock(pimpl_->bootstrapMtx_);
2374 if (!pimpl_ || !pimpl_->repository_ || !pimpl_->swarmManager_)
2375 return;
2376 // Here, we bootstrap the DRT with devices who already wrote in the conversation
2377 // If it works, the callback onConnectionChanged will be called with ok=true
2378 pimpl_->bootstrapCb_ = std::move(onBootstrapped);
2379 std::vector<DeviceId> devices = knownDevices;
2380 JAMI_DEBUG("{} Bootstrap with {} device(s)", pimpl_->toString(), devices.size());
2381
2382 if (!devices.empty()) {
2383 pimpl_->swarmManager_->setKnownNodes(devices);
2384 }
2385
2386 pimpl_->monitorConnection(weak_from_this());
2387
2388 // If is shutdown, the conversation was re-added, causing no new nodes to be connected, but just a classic
2389 // connectivity change
2390 if (pimpl_->swarmManager_->isShutdown()) {
2391 pimpl_->swarmManager_->restart();
2392 pimpl_->swarmManager_->maintainBuckets();
2393 }
2394}
2395
2396void
2397Conversation::addKnownDevices(const std::vector<DeviceId>& devices, const std::string& memberUri)
2398{
2399 if (devices.empty())
2400 return;
2401 if (!memberUri.empty()) {
2402 // JAMI_WARNING("{} Adding {} known devices for member {}", pimpl_->toString(), devices.size(), memberUri);
2403 std::lock_guard lk(pimpl_->trackedMembersMtx_);
2404 auto it = pimpl_->trackedMembers_.find(memberUri);
2405 if (it != pimpl_->trackedMembers_.end()) {
2406 it->second.devices.insert(devices.begin(), devices.end());
2407 }
2408 } else {
2409 JAMI_ERROR("{} Adding {} known devices without member URI", pimpl_->toString(), devices.size());
2410 }
2411 pimpl_->swarmManager_->setKnownNodes(devices);
2412}
2413
2414std::vector<std::string>
2416{
2417 pimpl_->loadActiveCalls();
2418 pimpl_->loadHostedCalls();
2419 auto commits = pimpl_->commitsEndedCalls();
2420 if (!commits.empty()) {
2421 // Announce to client
2422 dht::ThreadPool::io().run([w = weak(), commits] {
2423 if (auto sthis = w.lock())
2424 sthis->pimpl_->announce(commits, true);
2425 });
2426 }
2427 return commits;
2428}
2429
2430void
2432{
2433 pimpl_->onMembersChanged_ = std::move(cb);
2434}
2435
2436void
2438{
2439 pimpl_->swarmManager_->needSocketCb_ = [needSocket = std::move(needSocket), w = weak()](const std::string& deviceId,
2440 ChannelCb&& cb) {
2441 if (auto sthis = w.lock()) {
2442 auto wrappedCb = [cb = std::move(cb), w, deviceId](const std::shared_ptr<dhtnet::ChannelSocket>& socket) {
2443 if (auto sthis = w.lock()) {
2444 if (!socket) {
2445 if (auto acc = sthis->pimpl_->account_.lock()) {
2446 if (auto cert = acc->certStore().getCertificate(deviceId)) {
2447 sthis->pimpl_->onConnectionFailed(DeviceId(deviceId), cert->issuer->getId().toString());
2448 } else {
2449 JAMI_WARNING("{} Unable to get member URI from device ID {}",
2450 sthis->pimpl_->toString(),
2451 deviceId);
2452 }
2453 } else {
2454 return false;
2455 }
2456 }
2457 }
2458 return cb(socket);
2459 };
2460 needSocket(sthis->id(), deviceId, std::move(wrappedCb), "application/im-gitmessage-id");
2461 }
2462 };
2463}
2464
2465void
2466Conversation::addSwarmChannel(std::shared_ptr<dhtnet::ChannelSocket> channel)
2467{
2468 auto deviceId = channel->deviceId();
2469 // Transmit avatar if necessary
2470 // We do this here, because at this point we know both sides are connected and in
2471 // the same conversation
2472 // addSwarmChannel is a bit more complex, but it should be the best moment to do this.
2473 auto cert = channel->peerCertificate();
2474 if (!cert || !cert->issuer)
2475 return;
2476 auto member = cert->issuer->getId().toString();
2477 pimpl_->swarmManager_->addChannel(std::move(channel));
2478 dht::ThreadPool::io().run([member, deviceId, a = pimpl_->account_, w = weak_from_this()] {
2479 auto sthis = w.lock();
2480 if (auto account = a.lock()) {
2481 account->sendProfile(sthis->id(), member, deviceId.toString());
2482 }
2483 });
2484}
2485
2487Conversation::countInteractions(const std::string& toId, const std::string& fromId, const std::string& authorUri) const
2488{
2490 options.to = toId;
2491 options.from = fromId;
2492 options.authorUri = authorUri;
2493 options.logIfNotFound = false;
2494 options.fastLog = true;
2496 std::lock_guard lk(history.mutex);
2497 auto res = pimpl_->loadMessages(options, &history);
2498 return res.size();
2499}
2500
2501void
2502Conversation::search(uint32_t req, const Filter& filter, const std::shared_ptr<std::atomic_int>& flag) const
2503{
2504 // Because logging a conversation can take quite some time,
2505 // do it asynchronously
2506 dht::ThreadPool::io().run([w = weak(), req, filter, flag] {
2507 if (auto sthis = w.lock()) {
2508 History history;
2509 std::vector<std::map<std::string, std::string>> commits {};
2510 // std::regex_constants::ECMAScript is the default flag.
2511 auto re = std::regex(filter.regexSearch,
2512 filter.caseSensitive ? std::regex_constants::ECMAScript : std::regex_constants::icase);
2513 sthis->pimpl_->repository_->log(
2514 [&](const std::string& /*id*/, const GitAuthor& author, const GitCommit& commit) {
2515 if (!filter.author.empty() && filter.author != sthis->uriFromDevice(author.email)) {
2516 // Filter author
2517 return CallbackResult::Skip;
2518 }
2519 auto commitTime = git_commit_time(commit.get());
2520 if (filter.before && filter.before < commitTime) {
2521 // Only get commits before this date
2522 return CallbackResult::Skip;
2523 }
2524 if (filter.after && filter.after > commitTime) {
2525 // Only get commits before this date
2526 if (git_commit_parentcount(commit.get()) <= 1)
2527 return CallbackResult::Break;
2528 else
2529 return CallbackResult::Skip; // Because we are sorting it with
2530 // GIT_SORT_TOPOLOGICAL | GIT_SORT_TIME
2531 }
2532
2533 return CallbackResult::Ok; // Continue
2534 },
2535 [&](ConversationCommit&& cc) {
2536 if (auto optMessage = sthis->pimpl_->repository_->convCommitToMap(cc))
2537 sthis->pimpl_->addToHistory(history, {optMessage.value()}, false, false);
2538 },
2539 [&](const std::string& id, const GitAuthor&, ConversationCommit&) {
2540 if (id == filter.lastId)
2541 return true;
2542 return false;
2543 },
2544 "",
2545 false);
2546 // Search on generated history
2547 for (auto& message : history.messageList) {
2548 auto contentType = message->type;
2549 auto isSearchable = contentType == "text/plain" || contentType == "application/data-transfer+json";
2550 if (filter.type.empty() && !isSearchable) {
2551 // Not searchable, at least for now
2552 continue;
2553 } else if (contentType == filter.type || filter.type.empty()) {
2554 if (isSearchable) {
2555 // If it's a text match the body, else the display name
2556 auto body = contentType == "text/plain" ? message->body.at("body")
2557 : message->body.at("displayName");
2558 std::smatch body_match;
2559 if (std::regex_search(body, body_match, re)) {
2560 auto commit = message->body;
2561 commit["id"] = message->id;
2562 commit["type"] = message->type;
2563 commits.emplace_back(commit);
2564 }
2565 } else {
2566 // Matching type, just add it to the results
2567 commits.emplace_back(message->body);
2568 }
2569
2570 if (filter.maxResult != 0 && commits.size() == filter.maxResult)
2571 break;
2572 }
2573 }
2574
2575 if (commits.size() > 0)
2577 sthis->pimpl_->accountId_,
2578 sthis->id(),
2579 std::move(commits));
2580 // If we're the latest thread, inform client that the search is finished
2581 if ((*flag)-- == 1 /* decrement return the old value */) {
2583 req, sthis->pimpl_->accountId_, std::string {}, std::vector<std::map<std::string, std::string>> {});
2584 }
2585 }
2586 });
2587}
2588
2589void
2591{
2592 if (!message.isMember("confId")) {
2593 JAMI_ERROR("{}Malformed commit: no confId", pimpl_->toString());
2594 return;
2595 }
2596
2597 auto now = std::chrono::system_clock::now();
2598 auto nowSecs = std::chrono::duration_cast<std::chrono::seconds>(now.time_since_epoch()).count();
2599 {
2600 std::lock_guard lk(pimpl_->activeCallsMtx_);
2601 pimpl_->hostedCalls_[message["confId"].asString()] = nowSecs;
2602 pimpl_->saveHostedCalls();
2603 }
2604
2605 sendMessage(std::move(message), "", {}, std::move(cb));
2606}
2607
2608bool
2609Conversation::isHosting(const std::string& confId) const
2610{
2611 auto info = infos();
2612 if (info["rdvDevice"] == pimpl_->deviceId_ && info["rdvHost"] == pimpl_->userId_)
2613 return true; // We are the current device Host
2614 std::lock_guard lk(pimpl_->activeCallsMtx_);
2615 return pimpl_->hostedCalls_.find(confId) != pimpl_->hostedCalls_.end();
2616}
2617
2618void
2620{
2621 if (!message.isMember("confId")) {
2622 JAMI_ERROR("{}Malformed commit: no confId", pimpl_->toString());
2623 return;
2624 }
2625
2626 auto erased = false;
2627 {
2628 std::lock_guard lk(pimpl_->activeCallsMtx_);
2629 erased = pimpl_->hostedCalls_.erase(message["confId"].asString());
2630 }
2631 if (erased) {
2632 pimpl_->saveHostedCalls();
2633 sendMessage(std::move(message), "", {}, std::move(cb));
2634 } else
2635 cb(false, "");
2636}
2637
2638std::vector<std::map<std::string, std::string>>
2640{
2641 std::lock_guard lk(pimpl_->activeCallsMtx_);
2642 return pimpl_->activeCalls_;
2643}
2644} // namespace jami
#define _(S)
Definition SHA3.cpp:119
This class gives access to the git repository that represents the conversation.
std::vector< std::map< std::string, std::string > > getTrackedMembers() const
std::map< std::string, int32_t > memberToStatus
Status: 0 = commited, 1 = fetched, 2 = read This cache the curent status to add in the messages.
const std::string userId_
const std::filesystem::path conversationDataPath_
const std::weak_ptr< JamiAccount > account_
void addGitSocket(const DeviceId &deviceId, const std::shared_ptr< dhtnet::ChannelSocket > &socket)
void onConnectionFailed(const DeviceId &deviceId, const std::string &memberUri="")
void updateStatus(const std::string &uri, libjami::Account::MessageStates status, const std::string &commitId, const std::string &ts, bool emit=false)
void rotateTrackedMembers(const std::string &memberUri="", const DeviceId &deviceId={})
Impl(const std::shared_ptr< JamiAccount > &account, const std::string &remoteDevice, const std::string &conversationId)
std::vector< std::string > commitsEndedCalls()
If, for whatever reason, the daemon is stopped while hosting a conference, we need to announce the en...
std::shared_ptr< Typers > typers_
std::shared_ptr< dhtnet::ChannelSocket > gitSocket(const DeviceId &deviceId) const
void removeGitSocket(const DeviceId &deviceId)
const std::filesystem::path preferencesPath_
const std::unique_ptr< ConversationRepository > repository_
void disconnectFromPeer(const std::string &peerUri)
Remove all git sockets and all DRT nodes associated with the given peer.
const std::filesystem::path fetchedPath_
History loadedHistory_
Loaded history represents the linearized history to show for clients.
const std::filesystem::path sendingPath_
void announce(const std::vector< std::string > &commits, bool commitFromSelf=false)
std::map< std::string, std::map< std::string, std::string > > messagesStatus_
const std::string deviceId_
std::vector< libjami::SwarmMessage > loadMessages(const LogOptions &options, History *optHistory=nullptr)
std::vector< std::map< std::string, std::string > > getMembers(bool includeInvited, bool includeLeft, bool includeBanned) const
std::filesystem::path activeCallsPath_
std::map< std::string, TrackedMember > trackedMembers_
const std::filesystem::path statusPath_
const std::shared_ptr< asio::io_context > ioContext_
void updateActiveCalls(const std::map< std::string, std::string > &commit, bool eraseOnly=false, bool emitSig=true) const
Update activeCalls_ via announced commits (in load or via new commits)
void handleReaction(History &history, const std::shared_ptr< libjami::SwarmMessage > &sharedCommit) const
std::vector< std::shared_ptr< libjami::SwarmMessage > > addToHistory(History &history, const std::vector< std::map< std::string, std::string > > &commits, bool messageReceived=false, bool commitFromSelf=false)
Impl(const std::shared_ptr< JamiAccount > &account, const std::string &conversationId)
void voteUnban(const std::string &contactUri, const std::string_view type, const OnDoneCb &cb)
void announce(const std::string &commitId, bool commitFromSelf=false)
const std::string accountId_
void initActiveCalls(const std::vector< std::map< std::string, std::string > > &commits) const
Initialize activeCalls_ from the list of commits in the repository.
std::atomic_bool isRemoving_
Impl(const std::shared_ptr< JamiAccount > &account, ConversationMode mode, const std::string &otherMember="")
OnMembersChanged onMembersChanged_
std::map< std::string, std::deque< std::pair< std::string, OnPullCb > > > fetchingRemotes_
std::vector< std::map< std::string, std::string > > activeCalls_
const std::shared_ptr< TransferManager > transferManager_
void pull(const std::string &deviceId)
std::vector< std::map< std::string, std::string > > getMembers(bool includeInvited, bool includeLeft) const
void monitorConnection(std::weak_ptr< Conversation > w)
std::function< void(const std::map< std::string, std::map< std::string, std::string > > &)> messageStatusCb_
void announce(const std::vector< std::map< std::string, std::string > > &commits, bool commitFromSelf=false)
std::string_view bannedType(const std::string &uri) const
void handleEdition(History &history, const std::shared_ptr< libjami::SwarmMessage > &sharedCommit, bool messageReceived) const
std::filesystem::path hostedCallsPath_
std::string toString() const
void startTracking(std::weak_ptr< Conversation > w)
const std::filesystem::path repoPath_
std::map< std::string, std::map< std::string, int32_t > > futureStatus
std::mutex messageStatusMtx_
{uri, { {"fetch", "commitId"}, {"fetched_ts", "timestamp"}, {"read", "commitId"}, {"read_ts",...
std::function< void()> bootstrapCb_
std::vector< std::map< std::string, std::string > > getConnectivity() const
std::map< std::string, uint64_t > hostedCalls_
void rectifyStatus(const std::shared_ptr< libjami::SwarmMessage > &message, History &history) const
bool handleMessage(History &history, const std::shared_ptr< libjami::SwarmMessage > &sharedCommit, bool messageReceived) const
const std::shared_ptr< SwarmManager > swarmManager_
bool pull(const std::string &deviceId, OnPullCb &&cb, std::string commitId="")
Fetch and merge from peer.
std::set< std::string > memberUris(std::string_view filter={}, const std::set< MemberRole > &filteredRoles={MemberRole::INVITED, MemberRole::LEFT, MemberRole::BANNED}) const
std::shared_ptr< Typers > typers() const
Get Typers object.
bool downloadFile(const std::string &interactionId, const std::string &fileId, const std::string &path, const std::string &member="", const std::string &deviceId="")
Adds a file to the waiting list and ask members.
void addGitSocket(const DeviceId &deviceId, const std::shared_ptr< dhtnet::ChannelSocket > &socket)
std::string leave()
Leave a conversation.
std::string id() const
Get conversation's id.
void clearCache()
Clear all cached messages.
std::vector< std::string > commitsEndedCalls()
Refresh active calls.
std::map< std::string, std::map< std::string, std::string > > messageStatus() const
Retrieve last displayed and fetch status per member.
bool isMember(const std::string &uri, bool includeInvited=false) const
Test if an URI is a member.
void search(uint32_t req, const Filter &filter, const std::shared_ptr< std::atomic_int > &flag) const
Search in the conversation via a filter.
std::vector< std::map< std::string, std::string > > getConnectivity() const
Get connectivity information for the conversation.
std::vector< uint8_t > vCard() const
std::vector< std::string > getInitialMembers() const
One to one util, get initial members.
void connectivityChanged()
If we change from one network to one another, we will need to update the state of the connections.
std::shared_ptr< TransferManager > dataTransfer() const
Access to transfer manager.
void removeGitSocket(const DeviceId &deviceId)
std::vector< std::map< std::string, std::string > > currentCalls() const
Return current detected calls.
void onMembersChanged(OnMembersChanged &&cb)
std::map< std::string, std::string > infos() const
Retrieve current infos (title, description, avatar, mode)
void monitor()
Print the state of the DRT linked to the conversation.
std::vector< NodeId > peersToSyncWith() const
Get peers to sync with.
void sendMessage(Json::Value &&message, const std::string &replyTo="", OnCommitCb &&onCommit={}, OnDoneCb &&cb={})
void bootstrap(std::function< void()> onBootstrapped, const std::vector< DeviceId > &knownDevices={})
Bootstrap swarm manager to other peers.
void removeActiveConference(Json::Value &&message, OnDoneCb &&cb={})
Announce the end of a call.
bool isInitialMember(const std::string &uri) const
ConversationMode mode() const
Get conversation's mode.
std::string join()
Join a conversation.
void addSwarmChannel(std::shared_ptr< dhtnet::ChannelSocket > channel)
Add swarm connection to the DRT.
std::vector< jami::DeviceId > getDeviceIdList() const
bool setMessageDisplayed(const std::string &uri, const std::string &interactionId)
Store last read commit (returned in getMembers)
void updateInfos(const std::map< std::string, std::string > &map, const OnDoneCb &cb={})
Change repository's infos.
void updateMessageStatus(const std::map< std::string, std::map< std::string, std::string > > &messageStatus)
Update fetch/read status.
void onMessageStatusChanged(const std::function< void(const std::map< std::string, std::map< std::string, std::string > > &)> &cb)
void shutdownConnections()
Stop SwarmManager, bootstrap and gitSockets.
void updatePreferences(const std::map< std::string, std::string > &map)
Change user's preferences.
std::vector< std::map< std::string, std::string > > getMembers(bool includeInvited=false, bool includeLeft=false, bool includeBanned=false) const
std::map< std::string, std::string > preferences(bool includeLastModified) const
Retrieve current preferences (color, notification, etc)
bool hasCommit(const std::string &commitId) const
Check if a commit exists in the repository.
void sync(const std::string &member, const std::string &deviceId, OnPullCb &&cb, std::string commitId="")
Fetch new commits and re-ask for waiting files.
std::shared_ptr< dhtnet::ChannelSocket > gitSocket(const DeviceId &deviceId) const
Git operations will need a ChannelSocket for cloning/fetching commits Because libgit2 is a C library,...
void onNeedSocket(NeedSocketCb cb)
Set the callback that will be called whenever a new socket will be needed.
std::string uriFromDevice(const std::string &deviceId) const
Retrieve the uri from a deviceId.
std::optional< std::map< std::string, std::string > > getCommit(const std::string &commitId) const
Retrieve one commit.
void erase()
Erase all related datas.
void setRemovingFlag()
Set a conversation as removing (when loading convInfo and still not sync)
void hasFetched(const std::string &deviceId, const std::string &commitId)
Store information about who fetch or not.
bool isHosting(const std::string &confId) const
Check if we're currently hosting this conference.
std::string lastCommitId() const
Get last commit id.
bool isBanned(const std::string &uri) const
void loadMessages(const OnLoadMessages &cb, const LogOptions &options)
Get a range of messages.
Conversation(const std::shared_ptr< JamiAccount > &account, ConversationMode mode, const std::string &otherMember="")
bool onFileChannelRequest(const std::string &member, const std::string &fileId, std::filesystem::path &path, std::string &sha3sum) const
Choose if we can accept channel request.
void addMember(const std::string &contactUri, const OnDoneCb &cb={})
Add conversation member.
bool hasSwarmChannel(const std::string &deviceId)
Used to avoid multiple connections, we just check if we got a swarm channel with a specific device.
void addKnownDevices(const std::vector< DeviceId > &devices, const std::string &memberUri)
Add known devices to the swarm manager.
bool isBootstrapped() const
Check if we're at least connected to one node.
bool isRemoving()
Check if we are removing the conversation.
void removeMember(const std::string &contactUri, bool isDevice, const OnDoneCb &cb={})
void hostConference(Json::Value &&message, OnDoneCb &&cb={})
Host a conference in the conversation.
std::vector< std::map< std::string, std::string > > getTrackedMembers() const
uint32_t countInteractions(const std::string &toId, const std::string &fromId="", const std::string &authorUri="") const
Retrieve how many interactions there is from HEAD to interactionId.
std::map< std::string, std::string > generateInvitation() const
Generate an invitation to send to new contacts.
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:694
std::mt19937_64 getSeededRandomEngine()
Definition manager.cpp:2756
#define JAMI_ERR(...)
Definition logger.h:230
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:238
#define JAMI_WARN(...)
Definition logger.h:229
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
Definition Address.h:25
static constexpr std::string_view SENDING
static constexpr std::string_view ACTIVE_CALLS
static constexpr std::string_view HOSTED_CALLS
static constexpr std::string_view PREFERENCES
static constexpr std::string_view FETCHED
static constexpr std::string_view STATUS
static constexpr const char * CREATED
static constexpr const char * REMOVED
static constexpr const char * ID
static constexpr const char * RECEIVED
static constexpr const char * METADATAS
static constexpr const char * MEMBERS
static constexpr const char * DECLINED
static constexpr const char * ERASED
static constexpr const char * FROM
static constexpr const char * CONVERSATIONID
static constexpr const char * LAST_DISPLAYED
static const std::filesystem::path DEVICES
static const std::filesystem::path INVITED
static const std::filesystem::path BANNED
static const std::filesystem::path ADMINS
static const std::filesystem::path MEMBERS
const std::filesystem::path & get_data_dir()
std::vector< uint8_t > loadFile(const std::filesystem::path &path, const std::filesystem::path &default_dir)
Read the full content of a file at path.
uint64_t lastWriteTimeInSeconds(const std::filesystem::path &filePath)
Return the last write time (epoch time) of a given file path (in seconds).
std::filesystem::path getFullPath(const std::filesystem::path &base, const std::filesystem::path &path)
If path is relative, it is appended to base.
std::string toString(const Json::Value &jsonVal)
Definition json_utils.h:42
std::function< void(const std::string &, const std::string &, ChannelCb &&, const std::string &)> NeedSocketCb
dht::PkId DeviceId
static constexpr std::string_view toString(AuthDecodingState state)
dht::h256 NodeId
void emitSignal(Args... args)
Definition jami_signal.h:64
std::function< void(std::vector< libjami::SwarmMessage > &&messages)> OnLoadMessages
std::unique_ptr< git_commit, GitCommitDeleter > GitCommit
Definition git_def.h:46
std::function< void(bool, const std::string &)> OnDoneCb
std::list< std::shared_ptr< libjami::SwarmMessage > > MessageList
std::function< bool(const std::shared_ptr< dhtnet::ChannelSocket > &)> ChannelCb
std::function< void(bool fetchOk)> OnPullCb
std::map< DeviceId, std::shared_ptr< dhtnet::ChannelSocket > > GitSocketList
std::function< void(const std::string &)> OnCommitCb
static const char *const LAST_MODIFIED
std::function< void(const std::set< std::string > &)> OnMembersChanged
bool regex_search(string_view sv, svmatch &m, const regex &e, regex_constants::match_flag_type flags=regex_constants::match_default)
std::set< std::string > members
ConvInfo()=default
std::string lastDisplayed
Json::Value toJson() const
std::string id
std::map< std::string, std::string > metadatas
Json::Value toJson() const
std::map< std::string, std::string > toMap() const
std::mutex mutex
std::map< std::string, std::shared_ptr< libjami::SwarmMessage > > quickAccess
std::map< std::string, std::list< std::map< std::string, std::string > > > pendingReactions
MessageList messageList
std::map< std::string, std::list< std::shared_ptr< libjami::SwarmMessage > > > pendingEditions
std::condition_variable cv