Ring Daemon 16.0.0
Loading...
Searching...
No Matches
conversation_module.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2025 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_module.h"
19
20#include <algorithm>
21#include <fstream>
22
23#include <opendht/thread_pool.h>
24
25#include "account_const.h"
26#include "call.h"
27#include "client/ring_signal.h"
28#include "fileutils.h"
30#include "jamidht/jamiaccount.h"
31#include "manager.h"
32#include "sip/sipcall.h"
33#include "vcard.h"
34#include "json_utils.h"
35
36namespace jami {
37
38using ConvInfoMap = std::map<std::string, ConvInfo>;
39
41{
42 bool ready {false};
43 bool cloning {false};
44 std::string deviceId {};
45 std::string removeId {};
46 std::map<std::string, std::string> preferences {};
47 std::map<std::string, std::map<std::string, std::string>> status {};
48 std::set<std::string> connectingTo {};
49 std::shared_ptr<dhtnet::ChannelSocket> socket {};
50};
51
52constexpr std::chrono::seconds MAX_FALLBACK {12 * 3600s};
53
55{
56 std::mutex mtx;
57 std::unique_ptr<asio::steady_timer> fallbackClone;
58 std::chrono::seconds fallbackTimer {5s};
60 std::unique_ptr<PendingConversationFetch> pending;
61 std::shared_ptr<Conversation> conversation;
62
63 SyncedConversation(const std::string& convId)
64 : info {convId}
65 {
66 fallbackClone = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext());
67 }
69 : info {info}
70 {
71 fallbackClone = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext());
72 }
73
74 bool startFetch(const std::string& deviceId, bool checkIfConv = false)
75 {
76 // conversation mtx must be locked
78 return false; // Already a conversation
79 if (pending) {
80 if (pending->ready)
81 return false; // Already doing stuff
82 // if (pending->deviceId == deviceId)
83 // return false; // Already fetching
84 if (pending->connectingTo.find(deviceId) != pending->connectingTo.end())
85 return false; // Already connecting to this device
86 } else {
87 pending = std::make_unique<PendingConversationFetch>();
88 pending->connectingTo.insert(deviceId);
89 return true;
90 }
91 return true;
92 }
93
94 void stopFetch(const std::string& deviceId)
95 {
96 // conversation mtx must be locked
97 if (!pending)
98 return;
99 pending->connectingTo.erase(deviceId);
100 if (pending->connectingTo.empty())
101 pending.reset();
102 }
103
104 std::vector<std::map<std::string, std::string>> getMembers(bool includeLeft,
105 bool includeBanned) const
106 {
107 // conversation mtx must be locked
108 if (conversation)
109 return conversation->getMembers(true, includeLeft, includeBanned);
110 // If we're cloning, we can return the initial members
111 std::vector<std::map<std::string, std::string>> result;
112 result.reserve(info.members.size());
113 for (const auto& uri : info.members) {
114 result.emplace_back(std::map<std::string, std::string> {{"uri", uri}});
115 }
116 return result;
117 }
118};
119
120class ConversationModule::Impl : public std::enable_shared_from_this<Impl>
121{
122public:
123 Impl(std::shared_ptr<JamiAccount>&& account,
124 std::shared_ptr<AccountManager>&& accountManager,
127 NeedSocketCb&& onNeedSocket,
130
131 template<typename S, typename T>
132 inline auto withConv(const S& convId, T&& cb) const
133 {
134 if (auto conv = getConversation(convId)) {
135 std::lock_guard lk(conv->mtx);
136 return cb(*conv);
137 } else {
138 JAMI_WARNING("Conversation {} not found", convId);
139 }
140 return decltype(cb(std::declval<SyncedConversation&>()))();
141 }
142 template<typename S, typename T>
143 inline auto withConversation(const S& convId, T&& cb)
144 {
145 if (auto conv = getConversation(convId)) {
146 std::lock_guard lk(conv->mtx);
147 if (conv->conversation)
148 return cb(*conv->conversation);
149 } else {
150 JAMI_WARNING("Conversation {} not found", convId);
151 }
152 return decltype(cb(std::declval<Conversation&>()))();
153 }
154
155 // Retrieving recent commits
161 void cloneConversation(const std::string& deviceId,
162 const std::string& peer,
163 const std::string& convId);
164 void cloneConversation(const std::string& deviceId,
165 const std::string& peer,
166 const std::shared_ptr<SyncedConversation>& conv);
167
175 void fetchNewCommits(const std::string& peer,
176 const std::string& deviceId,
177 const std::string& conversationId,
178 const std::string& commitId = "");
182 void handlePendingConversation(const std::string& conversationId, const std::string& deviceId);
183
184 // Requests
185 std::optional<ConversationRequest> getRequest(const std::string& id) const;
186
187 // Conversations
194 std::vector<std::map<std::string, std::string>> getConversationMembers(
195 const std::string& conversationId, bool includeBanned = false) const;
196 void setConversationMembers(const std::string& convId, const std::set<std::string>& members);
197
204 void removeRepository(const std::string& convId, bool sync, bool force = false);
205 void removeRepositoryImpl(SyncedConversation& conv, bool sync, bool force = false);
210 bool removeConversation(const std::string& conversationId);
212
220 void sendMessageNotification(const std::string& conversationId,
221 bool sync,
222 const std::string& commitId = "",
223 const std::string& deviceId = "");
224 void sendMessageNotification(Conversation& conversation,
225 bool sync,
226 const std::string& commitId = "",
227 const std::string& deviceId = "");
228
232 bool isConversation(const std::string& convId) const
233 {
234 std::lock_guard lk(conversationsMtx_);
235 auto c = conversations_.find(convId);
236 return c != conversations_.end() && c->second;
237 }
238
239 void addConvInfo(const ConvInfo& info)
240 {
241 std::lock_guard lk(convInfosMtx_);
242 convInfos_[info.id] = info;
244 }
245
246 std::string getOneToOneConversation(const std::string& uri) const noexcept;
247
248 bool updateConvForContact(const std::string& uri,
249 const std::string& oldConv,
250 const std::string& newConv);
251
252 std::shared_ptr<SyncedConversation> getConversation(std::string_view convId) const
253 {
254 std::lock_guard lk(conversationsMtx_);
255 auto c = conversations_.find(convId);
256 return c != conversations_.end() ? c->second : nullptr;
257 }
258 std::shared_ptr<SyncedConversation> getConversation(std::string_view convId)
259 {
260 std::lock_guard lk(conversationsMtx_);
261 auto c = conversations_.find(convId);
262 return c != conversations_.end() ? c->second : nullptr;
263 }
264 std::shared_ptr<SyncedConversation> startConversation(const std::string& convId)
265 {
266 std::lock_guard lk(conversationsMtx_);
267 auto& c = conversations_[convId];
268 if (!c)
269 c = std::make_shared<SyncedConversation>(convId);
270 return c;
271 }
272 std::shared_ptr<SyncedConversation> startConversation(const ConvInfo& info)
273 {
274 std::lock_guard lk(conversationsMtx_);
275 auto& c = conversations_[info.id];
276 if (!c)
277 c = std::make_shared<SyncedConversation>(info);
278 return c;
279 }
280 std::vector<std::shared_ptr<SyncedConversation>> getSyncedConversations() const
281 {
282 std::lock_guard lk(conversationsMtx_);
283 std::vector<std::shared_ptr<SyncedConversation>> result;
284 result.reserve(conversations_.size());
285 for (const auto& [_, c] : conversations_)
286 result.emplace_back(c);
287 return result;
288 }
289 std::vector<std::shared_ptr<Conversation>> getConversations() const
290 {
291 std::lock_guard lk(conversationsMtx_);
292 std::vector<std::shared_ptr<Conversation>> result;
293 result.reserve(conversations_.size());
294 for (const auto& [_, sc] : conversations_) {
295 if (auto c = sc->conversation)
296 result.emplace_back(std::move(c));
297 }
298 return result;
299 }
300
301 // Message send/load
302 void sendMessage(const std::string& conversationId,
303 Json::Value&& value,
304 const std::string& replyTo = "",
305 bool announce = true,
306 OnCommitCb&& onCommit = {},
307 OnDoneCb&& cb = {});
308
309 void sendMessage(const std::string& conversationId,
310 std::string message,
311 const std::string& replyTo = "",
312 const std::string& type = "text/plain",
313 bool announce = true,
314 OnCommitCb&& onCommit = {},
315 OnDoneCb&& cb = {});
316
317 void editMessage(const std::string& conversationId,
318 const std::string& newBody,
319 const std::string& editedId);
320
321 void bootstrapCb(std::string convId);
322
323 // The following methods modify what is stored on the disk
335 void declineOtherConversationWith(const std::string& uri);
336 bool addConversationRequest(const std::string& id, const ConversationRequest& req)
337 {
338 // conversationsRequestsMtx_ MUST BE LOCKED
339 if (isConversation(id))
340 return false;
341 auto it = conversationsRequests_.find(id);
342 if (it != conversationsRequests_.end()) {
343 // We only remove requests (if accepted) or change .declined
344 if (!req.declined)
345 return false;
346 } else if (req.isOneToOne()) {
347 // Check that we're not adding a second one to one trust request
348 // NOTE: If a new one to one request is received, we can decline the previous one.
350 }
351 JAMI_DEBUG("[Account {}] [Conversation {}] Adding conversation request from {}", accountId_, id, req.from);
352 conversationsRequests_[id] = req;
354 return true;
355 }
356 void rmConversationRequest(const std::string& id)
357 {
358 // conversationsRequestsMtx_ MUST BE LOCKED
359 auto it = conversationsRequests_.find(id);
360 if (it != conversationsRequests_.end()) {
361 auto& md = syncingMetadatas_[id];
362 md = it->second.metadatas;
363 md["syncing"] = "true";
364 md["created"] = std::to_string(it->second.received);
365 }
366 saveMetadata();
367 conversationsRequests_.erase(id);
369 }
370
371 std::weak_ptr<JamiAccount> account_;
372 std::shared_ptr<AccountManager> accountManager_;
373 const std::string accountId_ {};
379
380 std::string deviceId_ {};
381 std::string username_ {};
382
383 // Requests
384 mutable std::mutex conversationsRequestsMtx_;
385 std::map<std::string, ConversationRequest> conversationsRequests_;
386
387 // Conversations
388 mutable std::mutex conversationsMtx_ {};
389 std::map<std::string, std::shared_ptr<SyncedConversation>, std::less<>> conversations_;
390
391 // The following information are stored on the disk
392 mutable std::mutex convInfosMtx_; // Note, should be locked after conversationsMtx_ if needed
393 std::map<std::string, ConvInfo> convInfos_;
394
395 // When sending a new message, we need to send the notification to some peers of the
396 // conversation However, the conversation may be not bootstraped, so the list will be empty.
397 // notSyncedNotification_ will store the notifiaction to announce until we have peers to sync
398 // with.
400 std::map<std::string, std::string> notSyncedNotification_;
401
402 std::weak_ptr<Impl> weak() { return std::static_pointer_cast<Impl>(shared_from_this()); }
403
404 // Replay conversations (after erasing/re-adding)
405 std::mutex replayMtx_;
406 std::map<std::string, std::vector<std::map<std::string, std::string>>> replay_;
407 std::map<std::string, uint64_t> refreshMessage;
408 std::atomic_int syncCnt {0};
409
410#ifdef LIBJAMI_TEST
411 std::function<void(std::string, Conversation::BootstrapStatus)> bootstrapCbTest_;
412#endif
413
414 void fixStructures(
415 std::shared_ptr<JamiAccount> account,
416 const std::vector<std::tuple<std::string, std::string, std::string>>& updateContactConv,
417 const std::set<std::string>& toRm);
418
419 void cloneConversationFrom(const std::shared_ptr<SyncedConversation> conv,
420 const std::string& deviceId,
421 const std::string& oldConvId = "");
422 void bootstrap(const std::string& convId);
423 void fallbackClone(const asio::error_code& ec, const std::string& conversationId);
424 void cloneConversationFrom(const std::string& conversationId,
425 const std::string& uri,
426 const std::string& oldConvId = "");
427
428 // While syncing, we do not want to lose metadata (avatar/title and mode)
429 std::map<std::string, std::map<std::string, std::string>> syncingMetadatas_;
431 {
432 auto path = fileutils::get_data_dir() / accountId_;
433 std::ofstream file(path / "syncingMetadatas", std::ios::trunc | std::ios::binary);
434 msgpack::pack(file, syncingMetadatas_);
435 }
436
438 {
439 try {
440 // read file
441 auto path = fileutils::get_data_dir() / accountId_;
442 std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "syncingMetadatas"));
443 auto file = fileutils::loadFile("syncingMetadatas", path);
444 // load values
445 msgpack::unpacked result;
446 msgpack::unpack(result, (const char*) file.data(), file.size(), 0);
447 result.get().convert(syncingMetadatas_);
448 } catch (const std::exception& e) {
449 JAMI_WARNING("[Account {}] [ConversationModule] unable to load syncing metadata: {}",
451 e.what());
452 }
453 }
454};
455
456ConversationModule::Impl::Impl(std::shared_ptr<JamiAccount>&& account,
457 std::shared_ptr<AccountManager>&& accountManager,
460 NeedSocketCb&& onNeedSocket,
463 : account_(account)
464 , accountManager_(accountManager)
465 , accountId_(account->getAccountID())
466 , needsSyncingCb_(needsSyncingCb)
467 , sendMsgCb_(sendMsgCb)
468 , onNeedSocket_(onNeedSocket)
469 , onNeedSwarmSocket_(onNeedSwarmSocket)
470 , oneToOneRecvCb_(oneToOneRecvCb)
471{
472 if (auto accm = account->accountManager())
473 if (const auto* info = accm->getInfo()) {
474 deviceId_ = info->deviceId;
475 username_ = info->accountId;
476 }
478 loadMetadata();
479}
480
481void
483 const std::string& peerUri,
484 const std::string& convId)
485{
486 JAMI_DEBUG("[Account {}] [Conversation {}] [device {}] Cloning conversation", accountId_, convId, deviceId);
487
489 std::unique_lock lk(conv->mtx);
490 cloneConversation(deviceId, peerUri, conv);
491}
492
493void
495 const std::string& peerUri,
496 const std::shared_ptr<SyncedConversation>& conv)
497{
498 // conv->mtx must be locked
499 if (!conv->conversation) {
500 // Note: here we don't return and connect to all members
501 // the first that will successfully connect will be used for
502 // cloning.
503 // This avoid the case when we try to clone from convInfos + sync message
504 // at the same time.
505 if (!conv->startFetch(deviceId, true)) {
506 JAMI_WARNING("[Account {}] [Conversation {}] [device {}] Already fetching conversation", accountId_, conv->info.id, deviceId);
507 addConvInfo(conv->info);
508 return;
509 }
510 onNeedSocket_(
511 conv->info.id,
512 deviceId,
513 [w = weak(), conv, deviceId](const auto& channel) {
514 std::lock_guard lk(conv->mtx);
515 if (conv->pending && !conv->pending->ready) {
516 if (channel) {
517 conv->pending->ready = true;
518 conv->pending->deviceId = channel->deviceId().toString();
519 conv->pending->socket = channel;
520 if (!conv->pending->cloning) {
521 conv->pending->cloning = true;
522 dht::ThreadPool::io().run(
523 [w, convId = conv->info.id, deviceId = conv->pending->deviceId]() {
524 if (auto sthis = w.lock())
525 sthis->handlePendingConversation(convId, deviceId);
526 });
527 }
528 return true;
529 } else {
530 conv->stopFetch(deviceId);
531 }
532 }
533 return false;
534 },
536
537 JAMI_LOG("[Account {}] [Conversation {}] [device {}] Requesting device",
538 accountId_,
539 conv->info.id,
540 deviceId);
541 conv->info.members.emplace(username_);
542 conv->info.members.emplace(peerUri);
543 addConvInfo(conv->info);
544 } else {
545 JAMI_DEBUG("[Account {}] [Conversation {}] Conversation already cloned", accountId_, conv->info.id);
546 }
547}
548
549void
551 const std::string& deviceId,
552 const std::string& conversationId,
553 const std::string& commitId)
554{
555 {
556 std::lock_guard lk(convInfosMtx_);
557 auto itConv = convInfos_.find(conversationId);
558 if (itConv != convInfos_.end() && itConv->second.isRemoved()) {
559 // If the conversation is removed and we receives a new commit,
560 // it means that the contact was removed but not banned.
561 // If he wants a new conversation, they must removes/re-add the contact who declined.
562 JAMI_WARNING("[Account {:s}] [Conversation {}] Received a commit, but conversation is removed",
563 accountId_,
564 conversationId);
565 return;
566 }
567 }
568 std::optional<ConversationRequest> oldReq;
569 {
570 std::lock_guard lk(conversationsRequestsMtx_);
571 oldReq = getRequest(conversationId);
572 if (oldReq != std::nullopt && oldReq->declined) {
573 JAMI_DEBUG("[Account {}] [Conversation {}] Received a request for a conversation already declined.",
574 accountId_, conversationId);
575 return;
576 }
577 }
578 JAMI_DEBUG("[Account {:s}] [Conversation {}] [device {}] fetching '{:s}'",
579 accountId_,
580 conversationId,
581 deviceId,
582 commitId);
583
584 auto conv = getConversation(conversationId);
585 if (!conv) {
586 if (oldReq == std::nullopt) {
587 // We didn't find a conversation or a request with the given ID.
588 // This suggests that someone tried to send us an invitation but
589 // that we didn't receive it, so we ask for a new one.
590 JAMI_WARNING("[Account {}] [Conversation {}] Unable to find conversation, asking for an invite",
591 accountId_,
592 conversationId);
593 sendMsgCb_(peer,
594 {},
595 std::map<std::string, std::string> {{MIME_TYPE_INVITE, conversationId}},
596 0);
597 }
598 return;
599 }
600 std::unique_lock lk(conv->mtx);
601
602 if (conv->conversation) {
603 // Check if we already have the commit
604 if (not commitId.empty() && conv->conversation->getCommit(commitId) != std::nullopt) {
605 return;
606 }
607 if (conv->conversation->isRemoving()) {
608 JAMI_WARNING("[Account {}] [Conversation {}] conversaton is being removed",
609 accountId_,
610 conversationId);
611 return;
612 }
613 if (!conv->conversation->isMember(peer, true)) {
614 JAMI_WARNING("[Account {}] [Conversation {}] {} is not a membe", accountId_, conversationId, peer);
615 return;
616 }
617 if (conv->conversation->isBanned(deviceId)) {
618 JAMI_WARNING("[Account {}] [Conversation {}] device {} is banned",
619 accountId_,
620 conversationId,
621 deviceId);
622 return;
623 }
624
625 // Retrieve current last message
626 auto lastMessageId = conv->conversation->lastCommitId();
627 if (lastMessageId.empty()) {
628 JAMI_ERROR("[Account {}] [Conversation {}] No message detected. This is a bug", accountId_, conversationId);
629 return;
630 }
631
632 if (!conv->startFetch(deviceId)) {
633 JAMI_WARNING("[Account {}] [Conversation {}] Already fetching", accountId_, conversationId);
634 return;
635 }
636
637 syncCnt.fetch_add(1);
638 onNeedSocket_(
639 conversationId,
640 deviceId,
641 [w = weak(),
642 conv,
643 conversationId,
644 peer = std::move(peer),
645 deviceId,
646 commitId = std::move(commitId)](const auto& channel) {
647 auto sthis = w.lock();
648 auto acc = sthis ? sthis->account_.lock() : nullptr;
649 std::unique_lock lk(conv->mtx);
650 auto conversation = conv->conversation;
651 if (!channel || !acc || !conversation) {
652 conv->stopFetch(deviceId);
653 if (sthis)
654 sthis->syncCnt.fetch_sub(1);
655 return false;
656 }
657 conversation->addGitSocket(channel->deviceId(), channel);
658 lk.unlock();
659 conversation->sync(
660 peer,
661 deviceId,
662 [w,
663 conv,
664 conversationId = std::move(conversationId),
665 peer,
666 deviceId,
667 commitId](bool ok) {
668 auto shared = w.lock();
669 if (!shared)
670 return;
671 if (!ok) {
672 JAMI_WARNING("[Account {}] [Conversation {}] Unable to fetch new commit from "
673 "{}, other peer may be disconnected",
674 shared->accountId_,
675 conversationId,
676 deviceId);
677 JAMI_LOG("[Account {}] [Conversation {}] Relaunch sync with {}",
678 shared->accountId_,
679 conversationId,
680 deviceId);
681 }
682
683 {
684 std::lock_guard lk(conv->mtx);
685 conv->pending.reset();
686 // Notify peers that a new commit is there (DRT)
687 if (not commitId.empty() && ok) {
688 shared->sendMessageNotification(*conv->conversation,
689 false,
690 commitId,
691 deviceId);
692 }
693 }
694 if (shared->syncCnt.fetch_sub(1) == 1) {
696 shared->accountId_);
697 }
698 },
699 commitId);
700 return true;
701 },
702 "");
703 } else {
704 if (oldReq != std::nullopt)
705 return;
706 if (conv->pending)
707 return;
708 bool clone = !conv->info.isRemoved();
709 if (clone) {
710 cloneConversation(deviceId, peer, conv);
711 return;
712 }
713 lk.unlock();
714 JAMI_WARNING("[Account {}] [Conversation {}] Unable to find conversation, asking for an invite",
715 accountId_,
716 conversationId);
717 sendMsgCb_(peer,
718 {},
719 std::map<std::string, std::string> {{MIME_TYPE_INVITE, conversationId}},
720 0);
721 }
722}
723
724// Clone and store conversation
725void
727 const std::string& deviceId)
728{
729 auto acc = account_.lock();
730 if (!acc)
731 return;
732 std::vector<DeviceId> kd;
733 {
734 std::unique_lock lk(conversationsMtx_);
735 const auto& devices = accountManager_->getKnownDevices();
736 kd.reserve(devices.size());
737 for (const auto& [id, _] : devices)
738 kd.emplace_back(id);
739 }
740 auto conv = getConversation(conversationId);
741 if (!conv)
742 return;
743 std::unique_lock lk(conv->mtx, std::defer_lock);
744 auto erasePending = [&] {
745 std::string toRm;
746 if (conv->pending && !conv->pending->removeId.empty())
747 toRm = std::move(conv->pending->removeId);
748 conv->pending.reset();
749 lk.unlock();
750 if (!toRm.empty())
752 };
753 try {
754 auto conversation = std::make_shared<Conversation>(acc, deviceId, conversationId);
755 conversation->onMembersChanged([w = weak_from_this(), conversationId](const auto& members) {
756 // Delay in another thread to avoid deadlocks
757 dht::ThreadPool::io().run([w, conversationId, members = std::move(members)] {
758 if (auto sthis = w.lock())
759 sthis->setConversationMembers(conversationId, members);
760 });
761 });
762 conversation->onMessageStatusChanged([this, conversationId](const auto& status) {
763 auto msg = std::make_shared<SyncMsg>();
764 msg->ms = {{conversationId, status}};
765 needsSyncingCb_(std::move(msg));
766 });
767 conversation->onNeedSocket(onNeedSwarmSocket_);
768 if (!conversation->isMember(username_, true)) {
769 JAMI_ERROR("[Account {}] [Conversation {}] Conversation cloned but we do not seem to be a valid member",
770 accountId_,
771 conversationId);
772 conversation->erase();
773 lk.lock();
774 erasePending();
775 return;
776 }
777
778 // Make sure that the list of members stored in convInfos_ matches the
779 // one from the conversation's repository.
780 // (https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/1026)
781 setConversationMembers(conversationId, conversation->memberUris("", {}));
782
783 lk.lock();
784
785 if (conv->pending && conv->pending->socket)
786 conversation->addGitSocket(DeviceId(deviceId), std::move(conv->pending->socket));
787 auto removeRepo = false;
788 // Note: a removeContact while cloning. In this case, the conversation
789 // must not be announced and removed.
790 if (conv->info.isRemoved())
791 removeRepo = true;
792 std::map<std::string, std::string> preferences;
793 std::map<std::string, std::map<std::string, std::string>> status;
794 if (conv->pending) {
795 preferences = std::move(conv->pending->preferences);
796 status = std::move(conv->pending->status);
797 }
798 conv->conversation = conversation;
799 if (removeRepo) {
800 removeRepositoryImpl(*conv, false, true);
801 erasePending();
802 return;
803 }
804
805 auto commitId = conversation->join();
806 std::vector<std::map<std::string, std::string>> messages;
807 {
808 std::lock_guard lk(replayMtx_);
809 auto replayIt = replay_.find(conversationId);
810 if (replayIt != replay_.end()) {
811 messages = std::move(replayIt->second);
812 replay_.erase(replayIt);
813 }
814 }
815 if (!commitId.empty())
816 sendMessageNotification(*conversation, false, commitId);
817 erasePending(); // Will unlock
818
819#ifdef LIBJAMI_TEST
820 conversation->onBootstrapStatus(bootstrapCbTest_);
821#endif
822 conversation->bootstrap(std::bind(&ConversationModule::Impl::bootstrapCb,
823 this,
824 conversation->id()),
825 kd);
826
827 if (!preferences.empty())
828 conversation->updatePreferences(preferences);
829 if (!status.empty())
830 conversation->updateMessageStatus(status);
831 syncingMetadatas_.erase(conversationId);
832 saveMetadata();
833
834 // Inform user that the conversation is ready
836 needsSyncingCb_({});
837 std::vector<Json::Value> values;
838 values.reserve(messages.size());
839 for (const auto& message : messages) {
840 // For now, only replay text messages.
841 // File transfers will need more logic, and don't care about calls for now.
842 if (message.at("type") == "text/plain" && message.at("author") == username_) {
843 Json::Value json;
844 json["body"] = message.at("body");
845 json["type"] = "text/plain";
846 values.emplace_back(std::move(json));
847 }
848 }
849 if (!values.empty())
850 conversation->sendMessages(std::move(values),
851 [w = weak(), conversationId](const auto& commits) {
852 auto shared = w.lock();
853 if (shared and not commits.empty())
854 shared->sendMessageNotification(conversationId,
855 true,
856 *commits.rbegin());
857 });
858 // Download members profile on first sync
859 auto isOneOne = conversation->mode() == ConversationMode::ONE_TO_ONE;
860 auto askForProfile = isOneOne;
861 if (!isOneOne) {
862 // If not 1:1 only download profiles from self (to avoid non checked files)
863 auto cert = acc->certStore().getCertificate(deviceId);
864 askForProfile = cert && cert->issuer && cert->issuer->getId().toString() == username_;
865 }
866 if (askForProfile) {
867 for (const auto& member : conversation->memberUris(username_)) {
868 acc->askForProfile(conversationId, deviceId, member);
869 }
870 }
871 } catch (const std::exception& e) {
872 JAMI_WARNING("[Account {}] [Conversation {}] Something went wrong when cloning conversation: {}. Re-clone in {}s",
873 accountId_,
874 conversationId,
875 e.what(),
876 conv->fallbackTimer.count());
877 conv->fallbackClone->expires_at(std::chrono::steady_clock::now() + conv->fallbackTimer);
878 conv->fallbackTimer *= 2;
879 if (conv->fallbackTimer > MAX_FALLBACK)
880 conv->fallbackTimer = MAX_FALLBACK;
881 conv->fallbackClone->async_wait(std::bind(&ConversationModule::Impl::fallbackClone,
883 std::placeholders::_1,
884 conversationId));
885 }
886 lk.lock();
887 erasePending();
888}
889
890std::optional<ConversationRequest>
891ConversationModule::Impl::getRequest(const std::string& id) const
892{
893 // ConversationsRequestsMtx MUST BE LOCKED
894 auto it = conversationsRequests_.find(id);
895 if (it != conversationsRequests_.end())
896 return it->second;
897 return std::nullopt;
898}
899
900std::string
901ConversationModule::Impl::getOneToOneConversation(const std::string& uri) const noexcept
902{
903 if (auto details = accountManager_->getContactInfo(uri)) {
904 // If contact is removed there is no conversation
905 // If banned, conversation is still on disk
906 if (details->removed != 0 && details->banned == 0) {
907 // Check if contact is removed
908 if (details->removed > details->added)
909 return {};
910 }
911 return details->conversationId;
912 }
913 return {};
914}
915
916bool
918 const std::string& oldConv,
919 const std::string& newConv)
920{
921 if (newConv != oldConv) {
922 auto conversation = getOneToOneConversation(uri);
923 if (conversation != oldConv) {
924 JAMI_DEBUG("[Account {}] [Conversation {}] Old conversation is not found in details {} - found: {}",
925 accountId_,
926 newConv,
927 oldConv,
928 conversation);
929 return false;
930 }
931 accountManager_->updateContactConversation(uri, newConv);
932 return true;
933 }
934 return false;
935}
936
937void
939{
940 // conversationsRequestsMtx_ MUST BE LOCKED
941 for (auto& [id, request] : conversationsRequests_) {
942 if (request.declined)
943 continue; // Ignore already declined requests
944 if (request.isOneToOne() && request.from == uri) {
945 JAMI_WARNING("[Account {}] [Conversation {}] Decline conversation request from {}",
946 accountId_, id, uri);
947 request.declined = std::time(nullptr);
948 syncingMetadatas_.erase(id);
949 saveMetadata();
951 }
952 }
953}
954
955std::vector<std::map<std::string, std::string>>
956ConversationModule::Impl::getConversationMembers(const std::string& conversationId,
957 bool includeBanned) const
958{
959 return withConv(conversationId,
960 [&](const auto& conv) { return conv.getMembers(true, includeBanned); });
961}
962
963void
964ConversationModule::Impl::removeRepository(const std::string& conversationId, bool sync, bool force)
965{
966 auto conv = getConversation(conversationId);
967 if (!conv)
968 return;
969 std::unique_lock lk(conv->mtx);
970 removeRepositoryImpl(*conv, sync, force);
971}
972
973void
975{
976 if (conv.conversation && (force || conv.conversation->isRemoving())) {
977 // Stop fetch!
978 conv.pending.reset();
979
980 JAMI_LOG("[Account {}] [Conversation {}] Remove conversation", accountId_, conv.info.id);
981 try {
982 if (conv.conversation->mode() == ConversationMode::ONE_TO_ONE) {
983 for (const auto& member : conv.conversation->getInitialMembers()) {
984 if (member != username_) {
985 // Note: this can happen while re-adding a contact.
986 // In this case, check that we are removing the linked conversation.
987 if (conv.info.id == getOneToOneConversation(member)) {
988 accountManager_->removeContactConversation(member);
989 }
990 }
991 }
992 }
993 } catch (const std::exception& e) {
994 JAMI_ERR() << e.what();
995 }
996 conv.conversation->erase();
997 conv.conversation.reset();
998
999 if (!sync)
1000 return;
1001
1002 conv.info.erased = std::time(nullptr);
1003 needsSyncingCb_({});
1004 addConvInfo(conv.info);
1005 }
1006}
1007
1008bool
1009ConversationModule::Impl::removeConversation(const std::string& conversationId)
1010{
1011 return withConv(conversationId, [this](auto& conv) { return removeConversationImpl(conv); });
1012}
1013
1014bool
1016{
1017 auto members = conv.getMembers(false, false);
1018 auto isSyncing = !conv.conversation;
1019 auto hasMembers = !isSyncing // If syncing there is no member to inform
1020 && std::find_if(members.begin(),
1021 members.end(),
1022 [&](const auto& member) {
1023 return member.at("uri") == username_;
1024 })
1025 != members.end() // We must be still a member
1026 && members.size() != 1; // If there is only ourself
1027 conv.info.removed = std::time(nullptr);
1028 if (isSyncing)
1029 conv.info.erased = std::time(nullptr);
1030 // Sync now, because it can take some time to really removes the datas
1031 needsSyncingCb_({});
1032 addConvInfo(conv.info);
1034 if (isSyncing)
1035 return true;
1036 if (conv.conversation->mode() != ConversationMode::ONE_TO_ONE) {
1037 // For one to one, we do not notify the leave. The other can still generate request
1038 // and this is managed by the banned part. If we re-accept, the old conversation will be
1039 // retrieved
1040 auto commitId = conv.conversation->leave();
1041 if (hasMembers) {
1042 JAMI_LOG("Wait that someone sync that user left conversation {}", conv.info.id);
1043 // Commit that we left
1044 if (!commitId.empty()) {
1045 // Do not sync as it's synched by convInfos
1046 sendMessageNotification(*conv.conversation, false, commitId);
1047 } else {
1048 JAMI_ERROR("Failed to send message to conversation {}", conv.info.id);
1049 }
1050 // In this case, we wait that another peer sync the conversation
1051 // to definitely remove it from the device. This is to inform the
1052 // peer that we left the conversation and never want to receive
1053 // any messages
1054 return true;
1055 }
1056 } else {
1057 for (const auto& m : members)
1058 if (username_ != m.at("uri"))
1059 updateConvForContact(m.at("uri"), conv.info.id, "");
1060 }
1061 // Else we are the last member, so we can remove
1062 removeRepositoryImpl(conv, true);
1063 return true;
1064}
1065
1066void
1068 bool sync,
1069 const std::string& commitId,
1070 const std::string& deviceId)
1071{
1072 if (auto conv = getConversation(conversationId)) {
1073 std::lock_guard lk(conv->mtx);
1074 if (conv->conversation)
1075 sendMessageNotification(*conv->conversation, sync, commitId, deviceId);
1076 }
1077}
1078
1079void
1081 bool sync,
1082 const std::string& commitId,
1083 const std::string& deviceId)
1084{
1085 auto acc = account_.lock();
1086 if (!acc)
1087 return;
1088 Json::Value message;
1089 auto commit = commitId == "" ? conversation.lastCommitId() : commitId;
1090 message["id"] = conversation.id();
1091 message["commit"] = commit;
1092 message["deviceId"] = deviceId_;
1093 const auto text = json::toString(message);
1094
1095 // Send message notification will announce the new commit in 3 steps.
1096
1097 // First, because our account can have several devices, announce to other devices
1098 if (sync) {
1099 // Announce to our devices
1100 refreshMessage[username_] = sendMsgCb_(username_,
1101 {},
1102 std::map<std::string, std::string> {
1103 {MIME_TYPE_GIT, text}},
1104 refreshMessage[username_]);
1105 }
1106
1107 // Then, we announce to 2 random members in the conversation that aren't in the DRT
1108 // This allow new devices without the ability to sync to their other devices to sync with us.
1109 // Or they can also use an old backup.
1110 std::vector<std::string> nonConnectedMembers;
1111 std::vector<NodeId> devices;
1112 {
1113 std::lock_guard lk(notSyncedNotificationMtx_);
1114 devices = conversation.peersToSyncWith();
1115 auto members = conversation.memberUris(username_, {MemberRole::BANNED});
1116 std::vector<std::string> connectedMembers;
1117 // print all members
1118 for (const auto& device : devices) {
1119 auto cert = acc->certStore().getCertificate(device.toString());
1120 if (cert && cert->issuer)
1121 connectedMembers.emplace_back(cert->issuer->getId().toString());
1122 }
1123 std::sort(std::begin(connectedMembers), std::end(connectedMembers));
1124 std::set_difference(members.begin(),
1125 members.end(),
1126 connectedMembers.begin(),
1127 connectedMembers.end(),
1128 std::inserter(nonConnectedMembers, nonConnectedMembers.begin()));
1129 std::shuffle(nonConnectedMembers.begin(), nonConnectedMembers.end(), acc->rand);
1130 if (nonConnectedMembers.size() > 2)
1131 nonConnectedMembers.resize(2);
1132 if (!conversation.isBootstraped()) {
1133 JAMI_DEBUG("[Conversation {}] Not yet bootstraped, save notification",
1134 conversation.id());
1135 // Because we can get some git channels but not bootstraped, we should keep this
1136 // to refresh when bootstraped.
1137 notSyncedNotification_[conversation.id()] = commit;
1138 }
1139 }
1140
1141 for (const auto& member : nonConnectedMembers) {
1142 refreshMessage[member] = sendMsgCb_(member,
1143 {},
1144 std::map<std::string, std::string> {
1145 {MIME_TYPE_GIT, text}},
1146 refreshMessage[member]);
1147 }
1148
1149 // Finally we send to devices that the DRT choose.
1150 for (const auto& device : devices) {
1151 auto deviceIdStr = device.toString();
1152 auto memberUri = conversation.uriFromDevice(deviceIdStr);
1153 if (memberUri.empty() || deviceIdStr == deviceId)
1154 continue;
1155 refreshMessage[deviceIdStr] = sendMsgCb_(memberUri,
1156 device,
1157 std::map<std::string, std::string> {
1158 {MIME_TYPE_GIT, text}},
1159 refreshMessage[deviceIdStr]);
1160 }
1161}
1162
1163void
1164ConversationModule::Impl::sendMessage(const std::string& conversationId,
1165 std::string message,
1166 const std::string& replyTo,
1167 const std::string& type,
1168 bool announce,
1170 OnDoneCb&& cb)
1171{
1172 Json::Value json;
1173 json["body"] = std::move(message);
1174 json["type"] = type;
1175 sendMessage(conversationId,
1176 std::move(json),
1177 replyTo,
1178 announce,
1179 std::move(onCommit),
1180 std::move(cb));
1181}
1182
1183void
1184ConversationModule::Impl::sendMessage(const std::string& conversationId,
1185 Json::Value&& value,
1186 const std::string& replyTo,
1187 bool announce,
1189 OnDoneCb&& cb)
1190{
1191 if (auto conv = getConversation(conversationId)) {
1192 std::lock_guard lk(conv->mtx);
1193 if (conv->conversation)
1194 conv->conversation
1195 ->sendMessage(std::move(value),
1196 replyTo,
1197 std::move(onCommit),
1198 [this,
1199 conversationId,
1200 announce,
1201 cb = std::move(cb)](bool ok, const std::string& commitId) {
1202 if (cb)
1203 cb(ok, commitId);
1204 if (!announce)
1205 return;
1206 if (ok)
1207 sendMessageNotification(conversationId, true, commitId);
1208 else
1209 JAMI_ERR("Failed to send message to conversation %s",
1210 conversationId.c_str());
1211 });
1212 }
1213}
1214
1215void
1216ConversationModule::Impl::editMessage(const std::string& conversationId,
1217 const std::string& newBody,
1218 const std::string& editedId)
1219{
1220 // Check that editedId is a valid commit, from ourself and plain/text
1221 auto validCommit = false;
1222 std::string type, tid;
1223 if (auto conv = getConversation(conversationId)) {
1224 std::lock_guard lk(conv->mtx);
1225 if (conv->conversation) {
1226 auto commit = conv->conversation->getCommit(editedId);
1227 if (commit != std::nullopt) {
1228 type = commit->at("type");
1229 if (type == "application/data-transfer+json")
1230 tid = commit->at("tid");
1231 validCommit = commit->at("author") == username_
1232 && (type == "text/plain" || type == "application/data-transfer+json");
1233 }
1234 }
1235 }
1236 if (!validCommit) {
1237 JAMI_ERROR("Unable to edit commit {:s}", editedId);
1238 return;
1239 }
1240 // Commit message edition
1241 Json::Value json;
1242 if (type == "application/data-transfer+json") {
1243 json["tid"] = "";
1244 // Remove file!
1245 auto path = fileutils::get_data_dir() / accountId_ / "conversation_data" / conversationId
1246 / fmt::format("{}_{}", editedId, tid);
1247 dhtnet::fileutils::remove(path, true);
1248 } else {
1249 json["body"] = newBody;
1250 }
1251 json["edit"] = editedId;
1252 json["type"] = type;
1253 sendMessage(conversationId, std::move(json));
1254}
1255
1256void
1258{
1259 std::string commitId;
1260 {
1261 std::lock_guard lk(notSyncedNotificationMtx_);
1262 auto it = notSyncedNotification_.find(convId);
1263 if (it != notSyncedNotification_.end()) {
1264 commitId = it->second;
1265 notSyncedNotification_.erase(it);
1266 }
1267 }
1268 JAMI_DEBUG("[Account {}] [Conversation {}] Resend last message notification", accountId_, convId);
1269 dht::ThreadPool::io().run([w = weak(), convId, commitId = std::move(commitId)] {
1270 if (auto sthis = w.lock())
1271 sthis->sendMessageNotification(convId, true, commitId);
1272 });
1273}
1274
1275void
1277 std::shared_ptr<JamiAccount> acc,
1278 const std::vector<std::tuple<std::string, std::string, std::string>>& updateContactConv,
1279 const std::set<std::string>& toRm)
1280{
1281 for (const auto& [uri, oldConv, newConv] : updateContactConv) {
1283 }
1285 // Note: This is only to homogenize trust and convRequests
1286 std::vector<std::string> invalidPendingRequests;
1287 {
1288 auto requests = acc->getTrustRequests();
1289 std::lock_guard lk(conversationsRequestsMtx_);
1290 for (const auto& request : requests) {
1293 if (itConvId != request.end() && itConvFrom != request.end()) {
1294 // Check if requests exists or is declined.
1295 auto itReq = conversationsRequests_.find(itConvId->second);
1296 auto declined = itReq == conversationsRequests_.end() || itReq->second.declined;
1297 if (declined) {
1298 JAMI_WARNING("Invalid trust request found: {:s}", itConvId->second);
1299 invalidPendingRequests.emplace_back(itConvFrom->second);
1300 }
1301 }
1302 }
1303 auto requestRemoved = false;
1304 for (auto it = conversationsRequests_.begin(); it != conversationsRequests_.end();) {
1305 if (it->second.from == username_) {
1306 JAMI_WARNING("Detected request from ourself, this makes no sense. Remove {}",
1307 it->first);
1308 it = conversationsRequests_.erase(it);
1309 } else {
1310 ++it;
1311 }
1312 }
1313 if (requestRemoved) {
1315 }
1316 }
1318 acc->discardTrustRequest(invalidPendingRequest);
1319
1321 for (const auto& conv : toRm) {
1322 JAMI_ERROR("[Account {}] Remove conversation ({})", accountId_, conv);
1324 }
1325 JAMI_DEBUG("[Account {}] Conversations loaded!", accountId_);
1326}
1327
1328void
1329ConversationModule::Impl::cloneConversationFrom(const std::shared_ptr<SyncedConversation> conv,
1330 const std::string& deviceId,
1331 const std::string& oldConvId)
1332{
1333 std::lock_guard lk(conv->mtx);
1334 const auto& conversationId = conv->info.id;
1335 if (!conv->startFetch(deviceId, true)) {
1336 JAMI_WARNING("[Account {}] [Conversation {}] Already fetching", accountId_, conversationId);
1337 return;
1338 }
1339
1340 onNeedSocket_(
1341 conversationId,
1342 deviceId,
1343 [wthis = weak_from_this(), conv, conversationId, oldConvId, deviceId](const auto& channel) {
1344 std::lock_guard lk(conv->mtx);
1345 if (conv->pending && !conv->pending->ready) {
1346 conv->pending->removeId = oldConvId;
1347 if (channel) {
1348 conv->pending->ready = true;
1349 conv->pending->deviceId = channel->deviceId().toString();
1350 conv->pending->socket = channel;
1351 if (!conv->pending->cloning) {
1352 conv->pending->cloning = true;
1353 dht::ThreadPool::io().run(
1354 [wthis, conversationId, deviceId = conv->pending->deviceId]() {
1355 if (auto sthis = wthis.lock())
1356 sthis->handlePendingConversation(conversationId, deviceId);
1357 });
1358 }
1359 return true;
1360 } else if (auto sthis = wthis.lock()) {
1361 conv->stopFetch(deviceId);
1362 JAMI_WARNING("[Account {}] [Conversation {}] Clone failed. Re-clone in {}s", sthis->accountId_, conversationId, conv->fallbackTimer.count());
1363 conv->fallbackClone->expires_at(std::chrono::steady_clock::now()
1364 + conv->fallbackTimer);
1365 conv->fallbackTimer *= 2;
1366 if (conv->fallbackTimer > MAX_FALLBACK)
1367 conv->fallbackTimer = MAX_FALLBACK;
1368 conv->fallbackClone->async_wait(
1370 sthis,
1371 std::placeholders::_1,
1372 conversationId));
1373 }
1374 }
1375 return false;
1376 },
1378}
1379
1380void
1382 const std::string& conversationId)
1383{
1384 if (ec == asio::error::operation_aborted)
1385 return;
1386 auto conv = getConversation(conversationId);
1387 if (!conv || conv->conversation)
1388 return;
1389 auto members = getConversationMembers(conversationId);
1390 for (const auto& member : members)
1391 if (member.at("uri") != username_)
1392 cloneConversationFrom(conversationId, member.at("uri"));
1393}
1394
1395void
1397{
1398 std::vector<DeviceId> kd;
1399 {
1400 std::unique_lock lk(conversationsMtx_);
1401 const auto& devices = accountManager_->getKnownDevices();
1402 kd.reserve(devices.size());
1403 for (const auto& [id, _] : devices)
1404 kd.emplace_back(id);
1405 }
1406 auto bootstrap = [&](auto& conv) {
1407 if (conv) {
1408#ifdef LIBJAMI_TEST
1409 conv->onBootstrapStatus(bootstrapCbTest_);
1410#endif
1411 conv->bootstrap(std::bind(&ConversationModule::Impl::bootstrapCb, this, conv->id()), kd);
1412 }
1413 };
1414 std::vector<std::string> toClone;
1415 std::vector<std::shared_ptr<Conversation>> conversations;
1416 if (convId.empty()) {
1417 std::lock_guard lk(convInfosMtx_);
1418 for (const auto& [conversationId, convInfo] : convInfos_) {
1419 auto conv = getConversation(conversationId);
1420 if (!conv)
1421 return;
1422 if (!conv->conversation && !conv->info.isRemoved()) {
1423 // Because we're not tracking contact presence in order to sync now,
1424 // we need to ask to clone requests when bootstraping all conversations
1425 // else it can stay syncing
1426 toClone.emplace_back(conversationId);
1427 } else if (conv->conversation) {
1428 conversations.emplace_back(conv->conversation);
1429 }
1430 }
1431 } else if (auto conv = getConversation(convId)) {
1432 std::lock_guard lk(conv->mtx);
1433 if (conv->conversation)
1434 conversations.emplace_back(conv->conversation);
1435 }
1436
1437 for (const auto& conversation : conversations)
1438 bootstrap(conversation);
1439 for (const auto& cid : toClone) {
1440 auto members = getConversationMembers(cid);
1441 for (const auto& member : members) {
1442 if (member.at("uri") != username_)
1443 cloneConversationFrom(cid, member.at("uri"));
1444 }
1445 }
1446}
1447
1448void
1449ConversationModule::Impl::cloneConversationFrom(const std::string& conversationId,
1450 const std::string& uri,
1451 const std::string& oldConvId)
1452{
1453 auto memberHash = dht::InfoHash(uri);
1454 if (!memberHash) {
1455 JAMI_WARNING("Invalid member detected: {}", uri);
1456 return;
1457 }
1458 auto conv = startConversation(conversationId);
1459 std::lock_guard lk(conv->mtx);
1460 conv->info = {conversationId};
1461 conv->info.created = std::time(nullptr);
1462 conv->info.members.emplace(username_);
1463 conv->info.members.emplace(uri);
1464 accountManager_->forEachDevice(memberHash,
1465 [w = weak(), conv, conversationId, oldConvId](
1466 const std::shared_ptr<dht::crypto::PublicKey>& pk) {
1467 auto sthis = w.lock();
1468 auto deviceId = pk->getLongId().toString();
1469 if (!sthis or deviceId == sthis->deviceId_)
1470 return;
1471 sthis->cloneConversationFrom(conv, deviceId, oldConvId);
1472 });
1473 addConvInfo(conv->info);
1474}
1475
1477
1478void
1480 const std::string& accountId,
1481 const std::map<std::string, ConversationRequest>& conversationsRequests)
1482{
1483 auto path = fileutils::get_data_dir() / accountId;
1484 saveConvRequestsToPath(path, conversationsRequests);
1485}
1486
1487void
1489 const std::filesystem::path& path,
1490 const std::map<std::string, ConversationRequest>& conversationsRequests)
1491{
1492 auto p = path / "convRequests";
1493 std::lock_guard lock(dhtnet::fileutils::getFileLock(p));
1494 std::ofstream file(p, std::ios::trunc | std::ios::binary);
1495 msgpack::pack(file, conversationsRequests);
1496}
1497
1498void
1499ConversationModule::saveConvInfos(const std::string& accountId, const ConvInfoMap& conversations)
1500{
1501 auto path = fileutils::get_data_dir() / accountId;
1502 saveConvInfosToPath(path, conversations);
1503}
1504
1505void
1506ConversationModule::saveConvInfosToPath(const std::filesystem::path& path,
1507 const ConvInfoMap& conversations)
1508{
1509 std::ofstream file(path / "convInfo", std::ios::trunc | std::ios::binary);
1510 msgpack::pack(file, conversations);
1511}
1512
1514
1516 std::shared_ptr<AccountManager> accountManager,
1519 NeedSocketCb&& onNeedSocket,
1523 : pimpl_ {std::make_unique<Impl>(std::move(account),
1524 std::move(accountManager),
1526 std::move(sendMsgCb),
1527 std::move(onNeedSocket),
1530{
1533 }
1534}
1535
1536void
1537ConversationModule::setAccountManager(std::shared_ptr<AccountManager> accountManager)
1538{
1539 std::unique_lock lk(pimpl_->conversationsMtx_);
1540 pimpl_->accountManager_ = accountManager;
1541}
1542
1543#ifdef LIBJAMI_TEST
1544void
1545ConversationModule::onBootstrapStatus(
1546 const std::function<void(std::string, Conversation::BootstrapStatus)>& cb)
1547{
1548 pimpl_->bootstrapCbTest_ = cb;
1549 for (auto& c : pimpl_->getConversations())
1551}
1552#endif
1553
1554void
1556{
1557 auto acc = pimpl_->account_.lock();
1558 if (!acc)
1559 return;
1560 JAMI_LOG("[Account {}] Start loading conversations…", pimpl_->accountId_);
1561 auto conversationsRepositories = dhtnet::fileutils::readDirectory(
1562 fileutils::get_data_dir() / pimpl_->accountId_ / "conversations");
1563
1564 std::unique_lock lk(pimpl_->conversationsMtx_);
1565 auto contacts = pimpl_->accountManager_->getContacts(
1566 true); // Avoid to lock configurationMtx while conv Mtx is locked
1567 std::unique_lock ilk(pimpl_->convInfosMtx_);
1568 pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
1569 pimpl_->conversations_.clear();
1570
1571 struct Ctx
1572 {
1573 std::mutex cvMtx;
1574 std::condition_variable cv;
1575 std::mutex toRmMtx;
1576 std::set<std::string> toRm;
1577 std::mutex convMtx;
1578 size_t convNb;
1579 std::vector<std::map<std::string, std::string>> contacts;
1580 std::vector<std::tuple<std::string, std::string, std::string>> updateContactConv;
1581 };
1582 auto ctx = std::make_shared<Ctx>();
1583 ctx->convNb = conversationsRepositories.size();
1584 ctx->contacts = std::move(contacts);
1585
1586 for (auto&& r : conversationsRepositories) {
1587 dht::ThreadPool::io().run([this, ctx, repository = std::move(r), acc] {
1588 try {
1589 auto sconv = std::make_shared<SyncedConversation>(repository);
1590 auto conv = std::make_shared<Conversation>(acc, repository);
1591 conv->onMessageStatusChanged([this, repository](const auto& status) {
1592 auto msg = std::make_shared<SyncMsg>();
1593 msg->ms = {{repository, status}};
1594 pimpl_->needsSyncingCb_(std::move(msg));
1595 });
1596 conv->onMembersChanged(
1597 [w = pimpl_->weak_from_this(), repository](const auto& members) {
1598 // Delay in another thread to avoid deadlocks
1599 dht::ThreadPool::io().run([w, repository, members = std::move(members)] {
1600 if (auto sthis = w.lock())
1601 sthis->setConversationMembers(repository, members);
1602 });
1603 });
1604 conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
1605 auto members = conv->memberUris(acc->getUsername(), {});
1606 // NOTE: The following if is here to protect against any incorrect state
1607 // that can be introduced
1608 if (conv->mode() == ConversationMode::ONE_TO_ONE && members.size() == 1) {
1609 // If we got a 1:1 conversation, but not in the contact details, it's rather a
1610 // duplicate or a weird state
1611 auto otherUri = *members.begin();
1612 auto itContact = std::find_if(ctx->contacts.cbegin(),
1613 ctx->contacts.cend(),
1614 [&](const auto& c) {
1615 return c.at("id") == otherUri;
1616 });
1617 if (itContact == ctx->contacts.end()) {
1618 JAMI_WARNING("Contact {} not found", otherUri);
1619 std::lock_guard lkCv {ctx->cvMtx};
1620 --ctx->convNb;
1621 ctx->cv.notify_all();
1622 return;
1623 }
1624 const std::string& convFromDetails = itContact->at("conversationId");
1625 auto removed = std::stoul(itContact->at("removed"));
1626 auto added = std::stoul(itContact->at("added"));
1627 auto isRemoved = removed > added;
1628 if (convFromDetails != repository) {
1629 if (convFromDetails.empty()) {
1630 if (isRemoved) {
1631 // If details is empty, contact is removed and not banned.
1632 JAMI_ERROR("Conversation {} detected for {} and should be removed",
1633 repository,
1634 otherUri);
1635 std::lock_guard lkMtx {ctx->toRmMtx};
1636 ctx->toRm.insert(repository);
1637 } else {
1638 JAMI_ERROR("No conversation detected for {} but one exists ({}). "
1639 "Update details",
1640 otherUri,
1641 repository);
1642 std::lock_guard lkMtx {ctx->toRmMtx};
1643 ctx->updateContactConv.emplace_back(
1644 std::make_tuple(otherUri, convFromDetails, repository));
1645 }
1646 } else {
1647 JAMI_ERROR("Multiple conversation detected for {} but ({} & {})",
1648 otherUri,
1649 repository,
1651 std::lock_guard lkMtx {ctx->toRmMtx};
1652 ctx->toRm.insert(repository);
1653 }
1654 }
1655 }
1656 {
1657 std::lock_guard lkMtx {ctx->convMtx};
1658 auto convInfo = pimpl_->convInfos_.find(repository);
1659 if (convInfo == pimpl_->convInfos_.end()) {
1660 JAMI_ERROR("Missing conv info for {}. This is a bug!", repository);
1661 sconv->info.created = std::time(nullptr);
1662 sconv->info.lastDisplayed
1664 } else {
1665 sconv->info = convInfo->second;
1666 if (convInfo->second.isRemoved()) {
1667 // A conversation was removed, but repository still exists
1668 conv->setRemovingFlag();
1669 std::lock_guard lkMtx {ctx->toRmMtx};
1670 ctx->toRm.insert(repository);
1671 }
1672 }
1673 // Even if we found the conversation in convInfos_, unable to assume that the
1674 // list of members stored in `convInfo` is correct
1675 // (https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/1025). For this
1676 // reason, we always use the list we got from the conversation repository to set
1677 // the value of `sconv->info.members`.
1678 members.emplace(acc->getUsername());
1679 sconv->info.members = std::move(members);
1680 // convInfosMtx_ is already locked
1681 pimpl_->convInfos_[repository] = sconv->info;
1682 }
1683 auto commits = conv->commitsEndedCalls();
1684
1685 if (!commits.empty()) {
1686 // Note: here, this means that some calls were actives while the
1687 // daemon finished (can be a crash).
1688 // Notify other in the conversation that the call is finished
1689 pimpl_->sendMessageNotification(*conv, true, *commits.rbegin());
1690 }
1691 sconv->conversation = conv;
1692 std::lock_guard lkMtx {ctx->convMtx};
1693 pimpl_->conversations_.emplace(repository, std::move(sconv));
1694 } catch (const std::logic_error& e) {
1695 JAMI_WARNING("[Account {}] Conversations not loaded: {}",
1696 pimpl_->accountId_,
1697 e.what());
1698 }
1699 std::lock_guard lkCv {ctx->cvMtx};
1700 --ctx->convNb;
1701 ctx->cv.notify_all();
1702 });
1703 }
1704
1705 std::unique_lock lkCv(ctx->cvMtx);
1706 ctx->cv.wait(lkCv, [&] { return ctx->convNb == 0; });
1707
1708 // Prune any invalid conversations without members and
1709 // set the removed flag if needed
1710 std::set<std::string> removed;
1711 for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
1712 const auto& info = itInfo->second;
1713 if (info.members.empty()) {
1714 itInfo = pimpl_->convInfos_.erase(itInfo);
1715 continue;
1716 }
1717 if (info.isRemoved())
1718 removed.insert(info.id);
1719 auto itConv = pimpl_->conversations_.find(info.id);
1720 if (itConv == pimpl_->conversations_.end()) {
1721 // convInfos_ can contain a conversation that is not yet cloned
1722 // so we need to add it there.
1723 itConv = pimpl_->conversations_
1724 .emplace(info.id, std::make_shared<SyncedConversation>(info))
1725 .first;
1726 }
1727 if (itConv != pimpl_->conversations_.end() && itConv->second && itConv->second->conversation
1728 && info.isRemoved())
1729 itConv->second->conversation->setRemovingFlag();
1730 if (!info.isRemoved() && itConv == pimpl_->conversations_.end()) {
1731 // In this case, the conversation is not synced and we only know ourself
1732 if (info.members.size() == 1 && *info.members.begin() == acc->getUsername()) {
1733 JAMI_WARNING("[Account {:s}] Conversation {:s} seems not present/synced.",
1734 pimpl_->accountId_,
1735 info.id);
1737 info.id);
1738 itInfo = pimpl_->convInfos_.erase(itInfo);
1739 continue;
1740 }
1741 }
1742 ++itInfo;
1743 }
1744 // On oldest version, removeConversation didn't update "appdata/contacts"
1745 // causing a potential incorrect state between "appdata/contacts" and "appdata/convInfos"
1746 if (!removed.empty())
1747 acc->unlinkConversations(removed);
1748 // Save if we've removed some invalid entries
1749 pimpl_->saveConvInfos();
1750
1751 ilk.unlock();
1752 lk.unlock();
1753
1754 dht::ThreadPool::io().run([w = pimpl_->weak(),
1755 acc,
1756 updateContactConv = std::move(ctx->updateContactConv),
1757 toRm = std::move(ctx->toRm)]() {
1758 // Will lock account manager
1759 if (auto shared = w.lock())
1760 shared->fixStructures(acc, updateContactConv, toRm);
1761 });
1762}
1763
1764void
1766{
1767 auto acc = pimpl_->account_.lock();
1768 if (!acc)
1769 return;
1770 JAMI_LOG("[Account {}] Start loading conversation {}", pimpl_->accountId_, convId);
1771
1772 std::unique_lock lk(pimpl_->conversationsMtx_);
1773 std::unique_lock ilk(pimpl_->convInfosMtx_);
1774 // Load convInfos to retrieve requests that have been accepted but not yet synchronized.
1775 pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
1776 pimpl_->conversations_.clear();
1777
1778 try {
1779 auto sconv = std::make_shared<SyncedConversation>(convId);
1780
1781 auto conv = std::make_shared<Conversation>(acc, convId);
1782
1783 conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
1784
1785 sconv->conversation = conv;
1786 pimpl_->conversations_.emplace(convId, std::move(sconv));
1787 } catch (const std::logic_error& e) {
1788 JAMI_WARNING("[Account {}] Conversations not loaded: {}", pimpl_->accountId_, e.what());
1789 }
1790
1791 // Add all other conversations as dummy conversations to indicate their existence so
1792 // isConversation could detect conversations correctly.
1793 auto conversationsRepositoryIds = dhtnet::fileutils::readDirectory(
1794 fileutils::get_data_dir() / pimpl_->accountId_ / "conversations");
1796 if (repositoryId != convId) {
1797 auto conv = std::make_shared<SyncedConversation>(repositoryId);
1798 pimpl_->conversations_.emplace(repositoryId, conv);
1799 }
1800 }
1801
1802 // Add conversations from convInfos_ so isConversation could detect conversations correctly.
1803 // This includes conversations that have been accepted but are not yet synchronized.
1804 for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
1805 const auto& info = itInfo->second;
1806 if (info.members.empty()) {
1807 itInfo = pimpl_->convInfos_.erase(itInfo);
1808 continue;
1809 }
1810 auto itConv = pimpl_->conversations_.find(info.id);
1811 if (itConv == pimpl_->conversations_.end()) {
1812 // convInfos_ can contain a conversation that is not yet cloned
1813 // so we need to add it there.
1814 pimpl_->conversations_.emplace(info.id, std::make_shared<SyncedConversation>(info));
1815 }
1816 ++itInfo;
1817 }
1818
1819 ilk.unlock();
1820 lk.unlock();
1821}
1822
1823void
1825{
1826 pimpl_->bootstrap(convId);
1827}
1828
1829void
1831{
1832 for (auto& conv : pimpl_->getConversations())
1833 conv->monitor();
1834}
1835
1836void
1838{
1839 // Note: This is a workaround. convModule() is kept if account is disabled/re-enabled.
1840 // iOS uses setAccountActive() a lot, and if for some reason the previous pending fetch
1841 // is not erased (callback not called), it will block the new messages as it will not
1842 // sync. The best way to debug this is to get logs from the last ICE connection for
1843 // syncing the conversation. It may have been killed in some un-expected way avoiding to
1844 // call the callbacks. This should never happen, but if it's the case, this will allow
1845 // new messages to be synced correctly.
1846 for (auto& conv : pimpl_->getSyncedConversations()) {
1847 std::lock_guard lk(conv->mtx);
1848 if (conv && conv->pending) {
1849 JAMI_ERR("This is a bug, seems to still fetch to some device on initializing");
1850 conv->pending.reset();
1851 }
1852 }
1853}
1854
1855void
1857{
1858 pimpl_->conversationsRequests_ = convRequests(pimpl_->accountId_);
1859}
1860
1861std::vector<std::string>
1863{
1864 std::vector<std::string> result;
1865 std::lock_guard lk(pimpl_->convInfosMtx_);
1866 result.reserve(pimpl_->convInfos_.size());
1867 for (const auto& [key, conv] : pimpl_->convInfos_) {
1868 if (conv.isRemoved())
1869 continue;
1870 result.emplace_back(key);
1871 }
1872 return result;
1873}
1874
1875std::string
1876ConversationModule::getOneToOneConversation(const std::string& uri) const noexcept
1877{
1878 return pimpl_->getOneToOneConversation(uri);
1879}
1880
1881bool
1883 const std::string& oldConv,
1884 const std::string& newConv)
1885{
1886 return pimpl_->updateConvForContact(uri, oldConv, newConv);
1887}
1888
1889std::vector<std::map<std::string, std::string>>
1891{
1892 std::vector<std::map<std::string, std::string>> requests;
1893 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
1894 requests.reserve(pimpl_->conversationsRequests_.size());
1895 for (const auto& [id, request] : pimpl_->conversationsRequests_) {
1896 if (request.declined)
1897 continue; // Do not add declined requests
1898 requests.emplace_back(request.toMap());
1899 }
1900 return requests;
1901}
1902
1903void
1905 const std::string& conversationId,
1906 const std::vector<uint8_t>& payload,
1907 time_t received)
1908{
1909 auto oldConv = getOneToOneConversation(uri);
1910 if (!oldConv.empty() && pimpl_->isConversation(oldConv)) {
1911 // If there is already an active one to one conversation here, it's an active
1912 // contact and the contact will reclone this activeConv, so ignore the request
1914 "Contact is sending a request for a non active conversation. Ignore. They will "
1915 "clone the old one");
1916 return;
1917 }
1918 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
1920 req.from = uri;
1921 req.conversationId = conversationId;
1922 req.received = std::time(nullptr);
1924 std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size())));
1925 auto reqMap = req.toMap();
1926 if (pimpl_->addConversationRequest(conversationId, std::move(req))) {
1927 lk.unlock();
1929 conversationId,
1930 uri,
1931 payload,
1932 received);
1934 conversationId,
1935 reqMap);
1936 pimpl_->needsSyncingCb_({});
1937 } else {
1938 JAMI_DEBUG("[Account {}] Received a request for a conversation "
1939 "already existing. Ignore",
1940 pimpl_->accountId_);
1941 }
1942}
1943
1944void
1945ConversationModule::onConversationRequest(const std::string& from, const Json::Value& value)
1946{
1948 auto isOneToOne = req.isOneToOne();
1949 std::string oldConv;
1950 if (isOneToOne) {
1951 oldConv = pimpl_->getOneToOneConversation(from);
1952 }
1953 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
1954 JAMI_DEBUG("[Account {}] Receive a new conversation request for conversation {} from {}",
1955 pimpl_->accountId_,
1956 req.conversationId,
1957 from);
1958 auto convId = req.conversationId;
1959
1960 // Already accepted request, do nothing
1961 if (pimpl_->isConversation(convId))
1962 return;
1963 auto oldReq = pimpl_->getRequest(convId);
1964 if (oldReq != std::nullopt) {
1965 JAMI_DEBUG("[Account {}] Received a request for a conversation already existing. "
1966 "Ignore. Declined: {}",
1967 pimpl_->accountId_,
1968 static_cast<int>(oldReq->declined));
1969 return;
1970 }
1971
1972 if (!oldConv.empty()) {
1973 lk.unlock();
1974 // Already a conversation with the contact.
1975 // If there is already an active one to one conversation here, it's an active
1976 // contact and the contact will reclone this activeConv, so ignore the request
1978 "Contact is sending a request for a non active conversation. Ignore. They will "
1979 "clone the old one");
1980 return;
1981 }
1982
1983 req.received = std::time(nullptr);
1984 req.from = from;
1985 auto reqMap = req.toMap();
1986 if (pimpl_->addConversationRequest(convId, std::move(req))) {
1987 lk.unlock();
1988 // Note: no need to sync here because other connected devices should receive
1989 // the same conversation request. Will sync when the conversation will be added
1990 if (isOneToOne)
1991 pimpl_->oneToOneRecvCb_(convId, from);
1993 convId,
1994 reqMap);
1995 }
1996}
1997
1998std::string
2000{
2001 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2002 auto it = pimpl_->conversationsRequests_.find(convId);
2003 if (it != pimpl_->conversationsRequests_.end()) {
2004 return it->second.from;
2005 }
2006 return {};
2007}
2008
2009void
2011 const std::string& conversationId)
2012{
2013 pimpl_->withConversation(conversationId, [&](auto& conversation) {
2014 if (!conversation.isMember(from, true)) {
2015 JAMI_WARNING("{} is asking a new invite for {}, but not a member", from, conversationId);
2016 return;
2017 }
2018 JAMI_LOG("{} is asking a new invite for {}", from, conversationId);
2019 pimpl_->sendMsgCb_(from, {}, conversation.generateInvitation(), 0);
2020 });
2021}
2022
2023void
2024ConversationModule::acceptConversationRequest(const std::string& conversationId,
2025 const std::string& deviceId)
2026{
2027 // For all conversation members, try to open a git channel with this conversation ID
2028 std::unique_lock lkCr(pimpl_->conversationsRequestsMtx_);
2029 auto request = pimpl_->getRequest(conversationId);
2030 if (request == std::nullopt) {
2031 lkCr.unlock();
2032 if (auto conv = pimpl_->getConversation(conversationId)) {
2033 std::unique_lock lk(conv->mtx);
2034 if (!conv->conversation) {
2035 lk.unlock();
2036 pimpl_->cloneConversationFrom(conv, deviceId);
2037 }
2038 }
2039 JAMI_WARNING("[Account {}] Request not found for conversation {}",
2040 pimpl_->accountId_,
2041 conversationId);
2042 return;
2043 }
2044 pimpl_->rmConversationRequest(conversationId);
2045 lkCr.unlock();
2046 pimpl_->accountManager_->acceptTrustRequest(request->from, true);
2047 cloneConversationFrom(conversationId, request->from);
2048}
2049
2050void
2051ConversationModule::declineConversationRequest(const std::string& conversationId)
2052{
2053 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2054 auto it = pimpl_->conversationsRequests_.find(conversationId);
2055 if (it != pimpl_->conversationsRequests_.end()) {
2056 it->second.declined = std::time(nullptr);
2057 pimpl_->saveConvRequests();
2058 }
2059 pimpl_->syncingMetadatas_.erase(conversationId);
2060 pimpl_->saveMetadata();
2062 conversationId);
2063 pimpl_->needsSyncingCb_({});
2064}
2065
2066std::string
2068{
2069 auto acc = pimpl_->account_.lock();
2070 if (!acc)
2071 return {};
2072 std::vector<DeviceId> kd;
2073 for (const auto& [id, _] : acc->getKnownDevices())
2074 kd.emplace_back(id);
2075 // Create the conversation object
2076 std::shared_ptr<Conversation> conversation;
2077 try {
2078 conversation = std::make_shared<Conversation>(acc, mode, otherMember.toString());
2079 auto conversationId = conversation->id();
2080 conversation->onMessageStatusChanged([this, conversationId](const auto& status) {
2081 auto msg = std::make_shared<SyncMsg>();
2082 msg->ms = {{conversationId, status}};
2083 pimpl_->needsSyncingCb_(std::move(msg));
2084 });
2085 conversation->onMembersChanged(
2086 [w = pimpl_->weak_from_this(), conversationId](const auto& members) {
2087 // Delay in another thread to avoid deadlocks
2088 dht::ThreadPool::io().run([w, conversationId, members = std::move(members)] {
2089 if (auto sthis = w.lock())
2090 sthis->setConversationMembers(conversationId, members);
2091 });
2092 });
2093 conversation->onNeedSocket(pimpl_->onNeedSwarmSocket_);
2094#ifdef LIBJAMI_TEST
2095 conversation->onBootstrapStatus(pimpl_->bootstrapCbTest_);
2096#endif
2097 conversation->bootstrap(std::bind(&ConversationModule::Impl::bootstrapCb,
2098 pimpl_.get(),
2099 conversationId),
2100 kd);
2101 } catch (const std::exception& e) {
2102 JAMI_ERROR("[Account {}] Error while generating a conversation {}",
2103 pimpl_->accountId_,
2104 e.what());
2105 return {};
2106 }
2107 auto convId = conversation->id();
2108 auto conv = pimpl_->startConversation(convId);
2109 std::unique_lock lk(conv->mtx);
2110 conv->info.created = std::time(nullptr);
2111 conv->info.members.emplace(pimpl_->username_);
2112 if (otherMember)
2113 conv->info.members.emplace(otherMember.toString());
2114 conv->conversation = conversation;
2115 addConvInfo(conv->info);
2116 lk.unlock();
2117
2118 pimpl_->needsSyncingCb_({});
2120 return convId;
2121}
2122
2123void
2124ConversationModule::cloneConversationFrom(const std::string& conversationId,
2125 const std::string& uri,
2126 const std::string& oldConvId)
2127{
2128 pimpl_->cloneConversationFrom(conversationId, uri, oldConvId);
2129}
2130
2131// Message send/load
2132void
2133ConversationModule::sendMessage(const std::string& conversationId,
2134 std::string message,
2135 const std::string& replyTo,
2136 const std::string& type,
2137 bool announce,
2139 OnDoneCb&& cb)
2140{
2141 pimpl_->sendMessage(conversationId,
2142 std::move(message),
2143 replyTo,
2144 type,
2145 announce,
2146 std::move(onCommit),
2147 std::move(cb));
2148}
2149
2150void
2151ConversationModule::sendMessage(const std::string& conversationId,
2152 Json::Value&& value,
2153 const std::string& replyTo,
2154 bool announce,
2156 OnDoneCb&& cb)
2157{
2158 pimpl_->sendMessage(conversationId,
2159 std::move(value),
2160 replyTo,
2161 announce,
2162 std::move(onCommit),
2163 std::move(cb));
2164}
2165
2166void
2167ConversationModule::editMessage(const std::string& conversationId,
2168 const std::string& newBody,
2169 const std::string& editedId)
2170{
2171 pimpl_->editMessage(conversationId, newBody, editedId);
2172}
2173
2174void
2175ConversationModule::reactToMessage(const std::string& conversationId,
2176 const std::string& newBody,
2177 const std::string& reactToId)
2178{
2179 // Commit message edition
2180 Json::Value json;
2181 json["body"] = newBody;
2182 json["react-to"] = reactToId;
2183 json["type"] = "text/plain";
2184 pimpl_->sendMessage(conversationId, std::move(json));
2185}
2186
2187void
2190 const std::string& reason)
2191{
2192 auto finalUri = uri.substr(0, uri.find("@ring.dht"));
2193 finalUri = finalUri.substr(0, uri.find("@jami.dht"));
2195 if (!convId.empty()) {
2196 Json::Value value;
2197 value["to"] = finalUri;
2198 value["type"] = "application/call-history+json";
2199 value["duration"] = std::to_string(duration_ms);
2200 if (!reason.empty())
2201 value["reason"] = reason;
2202 sendMessage(convId, std::move(value));
2203 }
2204}
2205
2206bool
2208 const std::string& conversationId,
2209 const std::string& interactionId)
2210{
2211 if (auto conv = pimpl_->getConversation(conversationId)) {
2212 std::unique_lock lk(conv->mtx);
2213 if (auto conversation = conv->conversation) {
2214 lk.unlock();
2215 return conversation->setMessageDisplayed(peer, interactionId);
2216 }
2217 }
2218 return false;
2219}
2220
2221std::map<std::string, std::map<std::string, std::map<std::string, std::string>>>
2223{
2224 std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> messageStatus;
2225 for (const auto& conv : pimpl_->getConversations()) {
2226 auto d = conv->messageStatus();
2227 if (!d.empty())
2228 messageStatus[conv->id()] = std::move(d);
2229 }
2230 return messageStatus;
2231}
2232
2234ConversationModule::loadConversationMessages(const std::string& conversationId,
2235 const std::string& fromMessage,
2236 size_t n)
2237{
2238 auto acc = pimpl_->account_.lock();
2239 if (auto conv = pimpl_->getConversation(conversationId)) {
2240 std::lock_guard lk(conv->mtx);
2241 if (conv->conversation) {
2242 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2245 options.nbOfCommits = n;
2246 conv->conversation->loadMessages(
2247 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2249 accountId,
2250 conversationId,
2251 messages);
2252 },
2253 options);
2254 return id;
2255 }
2256 }
2257 return 0;
2258}
2259
2260void
2261ConversationModule::clearCache(const std::string& conversationId)
2262{
2263 if (auto conv = pimpl_->getConversation(conversationId)) {
2264 std::lock_guard lk(conv->mtx);
2265 if (conv->conversation) {
2266 conv->conversation->clearCache();
2267 }
2268 }
2269}
2270
2272ConversationModule::loadConversation(const std::string& conversationId,
2273 const std::string& fromMessage,
2274 size_t n)
2275{
2276 auto acc = pimpl_->account_.lock();
2277 if (auto conv = pimpl_->getConversation(conversationId)) {
2278 std::lock_guard lk(conv->mtx);
2279 if (conv->conversation) {
2280 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2283 options.nbOfCommits = n;
2284 conv->conversation->loadMessages2(
2285 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2287 accountId,
2288 conversationId,
2289 messages);
2290 },
2291 options);
2292 return id;
2293 }
2294 }
2295 return 0;
2296}
2297
2299ConversationModule::loadConversationUntil(const std::string& conversationId,
2300 const std::string& fromMessage,
2301 const std::string& toMessage)
2302{
2303 auto acc = pimpl_->account_.lock();
2304 if (auto conv = pimpl_->getConversation(conversationId)) {
2305 std::lock_guard lk(conv->mtx);
2306 if (conv->conversation) {
2307 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2310 options.to = toMessage;
2311 options.includeTo = true;
2312 conv->conversation->loadMessages(
2313 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2315 accountId,
2316 conversationId,
2317 messages);
2318 },
2319 options);
2320 return id;
2321 }
2322 }
2323 return 0;
2324}
2325
2327ConversationModule::loadSwarmUntil(const std::string& conversationId,
2328 const std::string& fromMessage,
2329 const std::string& toMessage)
2330{
2331 auto acc = pimpl_->account_.lock();
2332 if (auto conv = pimpl_->getConversation(conversationId)) {
2333 std::lock_guard lk(conv->mtx);
2334 if (conv->conversation) {
2335 const uint32_t id = std::uniform_int_distribution<uint32_t> {}(acc->rand);
2338 options.to = toMessage;
2339 options.includeTo = true;
2340 conv->conversation->loadMessages2(
2341 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2343 accountId,
2344 conversationId,
2345 messages);
2346 },
2347 options);
2348 return id;
2349 }
2350 }
2351 return 0;
2352}
2353
2354std::shared_ptr<TransferManager>
2355ConversationModule::dataTransfer(const std::string& conversationId) const
2356{
2357 return pimpl_->withConversation(conversationId,
2358 [](auto& conversation) { return conversation.dataTransfer(); });
2359}
2360
2361bool
2362ConversationModule::onFileChannelRequest(const std::string& conversationId,
2363 const std::string& member,
2364 const std::string& fileId,
2365 bool verifyShaSum) const
2366{
2367 if (auto conv = pimpl_->getConversation(conversationId)) {
2368 std::filesystem::path path;
2369 std::string sha3sum;
2370 std::unique_lock lk(conv->mtx);
2371 if (!conv->conversation)
2372 return false;
2373 if (!conv->conversation->onFileChannelRequest(member, fileId, path, sha3sum))
2374 return false;
2375
2376 // Release the lock here to prevent the sha3 calculation from blocking other threads.
2377 lk.unlock();
2378 if (!std::filesystem::is_regular_file(path)) {
2379 JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for non existing file {}",
2380 pimpl_->accountId_,
2381 conversationId,
2382 member,
2383 fileId);
2384 return false;
2385 }
2386 // Check that our file is correct before sending
2387 if (verifyShaSum && sha3sum != fileutils::sha3File(path)) {
2388 JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for file {:s}, but our version is not "
2389 "complete or corrupted",
2390 pimpl_->accountId_,
2391 conversationId,
2392 member,
2393 fileId);
2394 return false;
2395 }
2396 return true;
2397 }
2398 return false;
2399}
2400
2401bool
2402ConversationModule::downloadFile(const std::string& conversationId,
2403 const std::string& interactionId,
2404 const std::string& fileId,
2405 const std::string& path)
2406{
2407 if (auto conv = pimpl_->getConversation(conversationId)) {
2408 std::lock_guard lk(conv->mtx);
2409 if (conv->conversation)
2410 return conv->conversation->downloadFile(interactionId, fileId, path, "", "");
2411 }
2412 return false;
2413}
2414
2415void
2416ConversationModule::syncConversations(const std::string& peer, const std::string& deviceId)
2417{
2418 // Sync conversations where peer is member
2419 std::set<std::string> toFetch;
2420 std::set<std::string> toClone;
2421 for (const auto& conv : pimpl_->getSyncedConversations()) {
2422 std::lock_guard lk(conv->mtx);
2423 if (conv->conversation) {
2424 if (!conv->conversation->isRemoving() && conv->conversation->isMember(peer, false)) {
2425 toFetch.emplace(conv->info.id);
2426 }
2427 } else if (!conv->info.isRemoved()
2428 && std::find(conv->info.members.begin(), conv->info.members.end(), peer)
2429 != conv->info.members.end()) {
2430 // In this case the conversation was never cloned (can be after an import)
2431 toClone.emplace(conv->info.id);
2432 }
2433 }
2434 for (const auto& cid : toFetch)
2435 pimpl_->fetchNewCommits(peer, deviceId, cid);
2436 for (const auto& cid : toClone)
2437 pimpl_->cloneConversation(deviceId, peer, cid);
2438 if (pimpl_->syncCnt.load() == 0)
2440}
2441
2442void
2444 const std::string& peerId,
2445 const std::string& deviceId)
2446{
2447 std::vector<std::string> toClone;
2448 for (const auto& [key, convInfo] : msg.c) {
2449 const auto& convId = convInfo.id;
2450 {
2451 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2452 pimpl_->rmConversationRequest(convId);
2453 }
2454
2455 auto conv = pimpl_->startConversation(convInfo);
2456 std::unique_lock lk(conv->mtx);
2457 // Skip outdated info
2458 if (std::max(convInfo.created, convInfo.removed)
2459 < std::max(conv->info.created, conv->info.removed))
2460 continue;
2461 if (not convInfo.isRemoved()) {
2462 // If multi devices, it can detect a conversation that was already
2463 // removed, so just check if the convinfo contains a removed conv
2464 if (conv->info.removed) {
2465 if (conv->info.removed >= convInfo.created) {
2466 // Only reclone if re-added, else the peer is not synced yet (could be
2467 // offline before)
2468 continue;
2469 }
2470 JAMI_DEBUG("Re-add previously removed conversation {:s}", convId);
2471 }
2472 conv->info = convInfo;
2473 if (!conv->conversation) {
2474 if (deviceId != "") {
2475 pimpl_->cloneConversation(deviceId, peerId, conv);
2476 } else {
2477 // In this case, information is from JAMS
2478 // JAMS does not store the conversation itself, so we
2479 // must use information to clone the conversation
2481 toClone.emplace_back(convId);
2482 }
2483 }
2484 } else {
2485 if (conv->conversation && !conv->conversation->isRemoving()) {
2487 convId);
2488 conv->conversation->setRemovingFlag();
2489 }
2490 auto update = false;
2491 if (!conv->info.removed) {
2492 update = true;
2493 conv->info.removed = std::time(nullptr);
2494 }
2495 if (convInfo.erased && !conv->info.erased) {
2496 conv->info.erased = std::time(nullptr);
2497 pimpl_->addConvInfo(conv->info);
2498 pimpl_->removeRepositoryImpl(*conv, false);
2499 } else if (update) {
2500 pimpl_->addConvInfo(conv->info);
2501 }
2502 }
2503 }
2504
2505 for (const auto& cid : toClone) {
2506 auto members = getConversationMembers(cid);
2507 for (const auto& member : members) {
2508 if (member.at("uri") != pimpl_->username_)
2509 cloneConversationFrom(cid, member.at("uri"));
2510 }
2511 }
2512
2513 for (const auto& [convId, req] : msg.cr) {
2514 if (req.from == pimpl_->username_) {
2515 JAMI_WARNING("Detected request from ourself, ignore {}.", convId);
2516 continue;
2517 }
2518 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
2519 if (pimpl_->isConversation(convId)) {
2520 // Already handled request
2521 pimpl_->rmConversationRequest(convId);
2522 continue;
2523 }
2524
2525 // New request
2526 if (!pimpl_->addConversationRequest(convId, req))
2527 continue;
2528 lk.unlock();
2529
2530 if (req.declined != 0) {
2531 // Request declined
2532 JAMI_LOG("[Account {:s}] Declined request detected for conversation {:s} (device {:s})",
2533 pimpl_->accountId_,
2534 convId,
2535 deviceId);
2536 pimpl_->syncingMetadatas_.erase(convId);
2537 pimpl_->saveMetadata();
2539 convId);
2540 continue;
2541 }
2542
2543 JAMI_LOG("[Account {:s}] New request detected for conversation {:s} (device {:s})",
2544 pimpl_->accountId_,
2545 convId,
2546 deviceId);
2547
2549 convId,
2550 req.toMap());
2551 }
2552
2553 // Updates preferences for conversations
2554 for (const auto& [convId, p] : msg.p) {
2555 if (auto conv = pimpl_->getConversation(convId)) {
2556 std::unique_lock lk(conv->mtx);
2557 if (conv->conversation) {
2558 auto conversation = conv->conversation;
2559 lk.unlock();
2560 conversation->updatePreferences(p);
2561 } else if (conv->pending) {
2562 conv->pending->preferences = p;
2563 }
2564 }
2565 }
2566
2567 // Updates displayed for conversations
2568 for (const auto& [convId, ms] : msg.ms) {
2569 if (auto conv = pimpl_->getConversation(convId)) {
2570 std::unique_lock lk(conv->mtx);
2571 if (conv->conversation) {
2572 auto conversation = conv->conversation;
2573 lk.unlock();
2574 conversation->updateMessageStatus(ms);
2575 } else if (conv->pending) {
2576 conv->pending->status = ms;
2577 }
2578 }
2579 }
2580}
2581
2582bool
2583ConversationModule::needsSyncingWith(const std::string& memberUri, const std::string& deviceId) const
2584{
2585 // Check if a conversation needs to fetch remote or to be cloned
2586 std::lock_guard lk(pimpl_->conversationsMtx_);
2587 for (const auto& [key, ci] : pimpl_->conversations_) {
2588 std::lock_guard lk(ci->mtx);
2589 if (ci->conversation) {
2590 if (ci->conversation->isRemoving() && ci->conversation->isMember(memberUri, false))
2591 return true;
2592 } else if (!ci->info.removed
2593 && std::find(ci->info.members.begin(), ci->info.members.end(), memberUri)
2594 != ci->info.members.end()) {
2595 // In this case the conversation was never cloned (can be after an import)
2596 return true;
2597 }
2598 }
2599 return false;
2600}
2601
2602void
2603ConversationModule::setFetched(const std::string& conversationId,
2604 const std::string& deviceId,
2605 const std::string& commitId)
2606{
2607 if (auto conv = pimpl_->getConversation(conversationId)) {
2608 std::lock_guard lk(conv->mtx);
2609 if (conv->conversation) {
2610 bool remove = conv->conversation->isRemoving();
2611 conv->conversation->hasFetched(deviceId, commitId);
2612 if (remove)
2613 pimpl_->removeRepositoryImpl(*conv, true);
2614 }
2615 }
2616}
2617
2618void
2620 const std::string& deviceId,
2621 const std::string& conversationId,
2622 const std::string& commitId)
2623{
2624 pimpl_->fetchNewCommits(peer, deviceId, conversationId, commitId);
2625}
2626
2627void
2628ConversationModule::addConversationMember(const std::string& conversationId,
2629 const dht::InfoHash& contactUri,
2630 bool sendRequest)
2631{
2632 auto conv = pimpl_->getConversation(conversationId);
2633 if (not conv || not conv->conversation) {
2634 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2635 return;
2636 }
2637 std::unique_lock lk(conv->mtx);
2638
2639 auto contactUriStr = contactUri.toString();
2640 if (conv->conversation->isMember(contactUriStr, true)) {
2641 JAMI_DEBUG("{:s} is already a member of {:s}, resend invite", contactUriStr, conversationId);
2642 // Note: This should not be necessary, but if for whatever reason the other side didn't
2643 // join we should not forbid new invites
2644 auto invite = conv->conversation->generateInvitation();
2645 lk.unlock();
2646 pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
2647 return;
2648 }
2649
2650 conv->conversation->addMember(
2652 [this, conv, conversationId, sendRequest, contactUriStr](bool ok,
2653 const std::string& commitId) {
2654 if (ok) {
2655 std::unique_lock lk(conv->mtx);
2656 pimpl_->sendMessageNotification(*conv->conversation,
2657 true,
2658 commitId); // For the other members
2659 if (sendRequest) {
2660 auto invite = conv->conversation->generateInvitation();
2661 lk.unlock();
2662 pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
2663 }
2664 }
2665 });
2666}
2667
2668void
2669ConversationModule::removeConversationMember(const std::string& conversationId,
2670 const dht::InfoHash& contactUri,
2671 bool isDevice)
2672{
2673 auto contactUriStr = contactUri.toString();
2674 if (auto conv = pimpl_->getConversation(conversationId)) {
2675 std::lock_guard lk(conv->mtx);
2676 if (conv->conversation)
2677 return conv->conversation
2678 ->removeMember(contactUriStr,
2679 isDevice,
2680 [this, conversationId](bool ok, const std::string& commitId) {
2681 if (ok) {
2682 pimpl_->sendMessageNotification(conversationId,
2683 true,
2684 commitId);
2685 }
2686 });
2687 }
2688}
2689
2690std::vector<std::map<std::string, std::string>>
2691ConversationModule::getConversationMembers(const std::string& conversationId,
2692 bool includeBanned) const
2693{
2694 return pimpl_->getConversationMembers(conversationId, includeBanned);
2695}
2696
2699 const std::string& toId,
2700 const std::string& fromId,
2701 const std::string& authorUri) const
2702{
2703 if (auto conv = pimpl_->getConversation(convId)) {
2704 std::lock_guard lk(conv->mtx);
2705 if (conv->conversation)
2706 return conv->conversation->countInteractions(toId, fromId, authorUri);
2707 }
2708 return 0;
2709}
2710
2711void
2712ConversationModule::search(uint32_t req, const std::string& convId, const Filter& filter) const
2713{
2714 if (convId.empty()) {
2715 auto convs = pimpl_->getConversations();
2716 if (convs.empty()) {
2718 req,
2719 pimpl_->accountId_,
2720 std::string {},
2721 std::vector<std::map<std::string, std::string>> {});
2722 return;
2723 }
2724 auto finishedFlag = std::make_shared<std::atomic_int>(convs.size());
2725 for (const auto& conv : convs) {
2726 conv->search(req, filter, finishedFlag);
2727 }
2728 } else if (auto conv = pimpl_->getConversation(convId)) {
2729 std::lock_guard lk(conv->mtx);
2730 if (conv->conversation)
2731 conv->conversation->search(req, filter, std::make_shared<std::atomic_int>(1));
2732 }
2733}
2734
2735void
2736ConversationModule::updateConversationInfos(const std::string& conversationId,
2737 const std::map<std::string, std::string>& infos,
2738 bool sync)
2739{
2740 auto conv = pimpl_->getConversation(conversationId);
2741 if (not conv or not conv->conversation) {
2742 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2743 return;
2744 }
2745 std::lock_guard lk(conv->mtx);
2746 conv->conversation
2747 ->updateInfos(infos, [this, conversationId, sync](bool ok, const std::string& commitId) {
2748 if (ok && sync) {
2749 pimpl_->sendMessageNotification(conversationId, true, commitId);
2750 } else if (sync)
2751 JAMI_WARNING("Unable to update info on {:s}", conversationId);
2752 });
2753}
2754
2755std::map<std::string, std::string>
2756ConversationModule::conversationInfos(const std::string& conversationId) const
2757{
2758 {
2759 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2760 auto itReq = pimpl_->conversationsRequests_.find(conversationId);
2761 if (itReq != pimpl_->conversationsRequests_.end())
2762 return itReq->second.metadatas;
2763 }
2764 if (auto conv = pimpl_->getConversation(conversationId)) {
2765 std::lock_guard lk(conv->mtx);
2766 std::map<std::string, std::string> md;
2767 {
2768 auto syncingMetadatasIt = pimpl_->syncingMetadatas_.find(conversationId);
2769 if (syncingMetadatasIt != pimpl_->syncingMetadatas_.end()) {
2770 if (conv->conversation) {
2771 pimpl_->syncingMetadatas_.erase(syncingMetadatasIt);
2772 pimpl_->saveMetadata();
2773 } else {
2774 md = syncingMetadatasIt->second;
2775 }
2776 }
2777 }
2778 if (conv->conversation)
2779 return conv->conversation->infos();
2780 else
2781 return md;
2782 }
2783 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2784 return {};
2785}
2786
2787void
2788ConversationModule::setConversationPreferences(const std::string& conversationId,
2789 const std::map<std::string, std::string>& prefs)
2790{
2791 if (auto conv = pimpl_->getConversation(conversationId)) {
2792 std::unique_lock lk(conv->mtx);
2793 if (not conv->conversation) {
2794 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2795 return;
2796 }
2797 auto conversation = conv->conversation;
2798 lk.unlock();
2799 conversation->updatePreferences(prefs);
2800 auto msg = std::make_shared<SyncMsg>();
2801 msg->p = {{conversationId, conversation->preferences(true)}};
2802 pimpl_->needsSyncingCb_(std::move(msg));
2803 }
2804}
2805
2806std::map<std::string, std::string>
2807ConversationModule::getConversationPreferences(const std::string& conversationId,
2808 bool includeCreated) const
2809{
2810 if (auto conv = pimpl_->getConversation(conversationId)) {
2811 std::lock_guard lk(conv->mtx);
2812 if (conv->conversation)
2813 return conv->conversation->preferences(includeCreated);
2814 }
2815 return {};
2816}
2817
2818std::map<std::string, std::map<std::string, std::string>>
2820{
2821 std::map<std::string, std::map<std::string, std::string>> p;
2822 for (const auto& conv : pimpl_->getConversations()) {
2823 auto prefs = conv->preferences(true);
2824 if (!prefs.empty())
2825 p[conv->id()] = std::move(prefs);
2826 }
2827 return p;
2828}
2829
2830std::vector<uint8_t>
2831ConversationModule::conversationVCard(const std::string& conversationId) const
2832{
2833 if (auto conv = pimpl_->getConversation(conversationId)) {
2834 std::lock_guard lk(conv->mtx);
2835 if (conv->conversation)
2836 return conv->conversation->vCard();
2837 }
2838 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2839 return {};
2840}
2841
2842bool
2843ConversationModule::isBanned(const std::string& convId, const std::string& uri) const
2844{
2845 if (auto conv = pimpl_->getConversation(convId)) {
2846 std::lock_guard lk(conv->mtx);
2847 if (!conv->conversation)
2848 return true;
2849 if (conv->conversation->mode() != ConversationMode::ONE_TO_ONE)
2850 return conv->conversation->isBanned(uri);
2851 }
2852 // If 1:1 we check the certificate status
2853 std::lock_guard lk(pimpl_->conversationsMtx_);
2854 return pimpl_->accountManager_->getCertificateStatus(uri)
2855 == dhtnet::tls::TrustStore::PermissionStatus::BANNED;
2856}
2857
2858void
2859ConversationModule::removeContact(const std::string& uri, bool banned)
2860{
2861 // Remove linked conversation's requests
2862 {
2863 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2864 auto update = false;
2865 for (auto it = pimpl_->conversationsRequests_.begin();
2866 it != pimpl_->conversationsRequests_.end();
2867 ++it) {
2868 if (it->second.from == uri && !it->second.declined) {
2869 JAMI_DEBUG("Declining conversation request {:s} from {:s}", it->first, uri);
2870 pimpl_->syncingMetadatas_.erase(it->first);
2871 pimpl_->saveMetadata();
2873 pimpl_->accountId_, it->first);
2874 update = true;
2875 it->second.declined = std::time(nullptr);
2876 }
2877 }
2878 if (update) {
2879 pimpl_->saveConvRequests();
2880 pimpl_->needsSyncingCb_({});
2881 }
2882 }
2883 if (banned) {
2884 auto conversationId = getOneToOneConversation(uri);
2885 pimpl_->withConversation(conversationId, [&](auto& conv) { conv.shutdownConnections(); });
2886 return; // Keep the conversation in banned model but stop connections
2887 }
2888
2889 // Removed contacts should not be linked to any conversation
2890 pimpl_->accountManager_->updateContactConversation(uri, "");
2891
2892 // Remove all one-to-one conversations with the removed contact
2893 auto isSelf = uri == pimpl_->username_;
2894 std::vector<std::string> toRm;
2895 auto removeConvInfo = [&](const auto& conv, const auto& members) {
2896 if ((isSelf && members.size() == 1)
2897 || (!isSelf && std::find(members.begin(), members.end(), uri) != members.end())) {
2898 // Mark the conversation as removed if it wasn't already
2899 if (!conv->info.isRemoved()) {
2900 conv->info.removed = std::time(nullptr);
2902 conv->info.id);
2903 pimpl_->addConvInfo(conv->info);
2904 return true;
2905 }
2906 }
2907 return false;
2908 };
2909 {
2910 std::lock_guard lk(pimpl_->conversationsMtx_);
2911 for (auto& [convId, conv] : pimpl_->conversations_) {
2912 std::lock_guard lk(conv->mtx);
2913 if (conv->conversation) {
2914 try {
2915 // Note it's important to check getUsername(), else
2916 // removing self can remove all conversations
2917 if (conv->conversation->mode() == ConversationMode::ONE_TO_ONE) {
2918 auto initMembers = conv->conversation->getInitialMembers();
2919 if (removeConvInfo(conv, initMembers))
2920 toRm.emplace_back(convId);
2921 }
2922 } catch (const std::exception& e) {
2923 JAMI_WARN("%s", e.what());
2924 }
2925 } else {
2926 removeConvInfo(conv, conv->info.members);
2927 }
2928 }
2929 }
2930 for (const auto& id : toRm)
2931 pimpl_->removeRepository(id, true, true);
2932}
2933
2934bool
2935ConversationModule::removeConversation(const std::string& conversationId)
2936{
2937 return pimpl_->removeConversation(conversationId);
2938}
2939
2940void
2941ConversationModule::initReplay(const std::string& oldConvId, const std::string& newConvId)
2942{
2943 if (auto conv = pimpl_->getConversation(oldConvId)) {
2944 std::lock_guard lk(conv->mtx);
2945 if (conv->conversation) {
2946 std::promise<bool> waitLoad;
2947 std::future<bool> fut = waitLoad.get_future();
2948 // we should wait for loadMessage, because it will be deleted after this.
2949 conv->conversation->loadMessages(
2950 [&](auto&& messages) {
2951 std::reverse(messages.begin(),
2952 messages.end()); // Log is inverted as we want to replay
2953 std::lock_guard lk(pimpl_->replayMtx_);
2954 pimpl_->replay_[newConvId] = std::move(messages);
2955 waitLoad.set_value(true);
2956 },
2957 {});
2958 fut.wait();
2959 }
2960 }
2961}
2962
2963bool
2964ConversationModule::isHosting(const std::string& conversationId, const std::string& confId) const
2965{
2966 if (conversationId.empty()) {
2967 std::lock_guard lk(pimpl_->conversationsMtx_);
2968 return std::find_if(pimpl_->conversations_.cbegin(),
2969 pimpl_->conversations_.cend(),
2970 [&](const auto& conv) {
2971 return conv.second->conversation
2972 && conv.second->conversation->isHosting(confId);
2973 })
2974 != pimpl_->conversations_.cend();
2975 } else if (auto conv = pimpl_->getConversation(conversationId)) {
2976 if (conv->conversation) {
2977 return conv->conversation->isHosting(confId);
2978 }
2979 }
2980 return false;
2981}
2982
2983std::vector<std::map<std::string, std::string>>
2984ConversationModule::getActiveCalls(const std::string& conversationId) const
2985{
2986 return pimpl_->withConversation(conversationId, [](const auto& conversation) {
2987 return conversation.currentCalls();
2988 });
2989}
2990
2991std::shared_ptr<SIPCall>
2993 const std::string& url,
2994 const std::vector<libjami::MediaMap>& mediaList,
2995 std::function<void(const std::string&, const DeviceId&, const std::shared_ptr<SIPCall>&)>&& cb)
2996{
2997 std::string conversationId = "", confId = "", uri = "", deviceId = "";
2998 if (url.find('/') == std::string::npos) {
2999 conversationId = url;
3000 } else {
3001 auto parameters = jami::split_string(url, '/');
3002 if (parameters.size() != 4) {
3003 JAMI_ERROR("Incorrect url {:s}", url);
3004 return {};
3005 }
3006 conversationId = parameters[0];
3007 uri = parameters[1];
3008 deviceId = parameters[2];
3009 confId = parameters[3];
3010 }
3011
3012 auto conv = pimpl_->getConversation(conversationId);
3013 if (!conv)
3014 return {};
3015 std::unique_lock lk(conv->mtx);
3016 if (!conv->conversation) {
3017 JAMI_ERROR("Conversation {:s} not found", conversationId);
3018 return {};
3019 }
3020
3021 // Check if we want to join a specific conference
3022 // So, if confId is specified or if there is some activeCalls
3023 // or if we are the default host.
3024 auto activeCalls = conv->conversation->currentCalls();
3025 auto infos = conv->conversation->infos();
3026 auto itRdvAccount = infos.find("rdvAccount");
3027 auto itRdvDevice = infos.find("rdvDevice");
3028 auto sendCallRequest = false;
3029 if (!confId.empty()) {
3030 sendCallRequest = true;
3031 JAMI_DEBUG("Calling self, join conference");
3032 } else if (!activeCalls.empty()) {
3033 // Else, we try to join active calls
3034 sendCallRequest = true;
3035 auto& ac = *activeCalls.rbegin();
3036 confId = ac.at("id");
3037 uri = ac.at("uri");
3038 deviceId = ac.at("device");
3039 } else if (itRdvAccount != infos.end() && itRdvDevice != infos.end()
3040 && !itRdvAccount->second.empty()) {
3041 // Else, creates "to" (accountId/deviceId/conversationId/confId) and ask remote host
3042 sendCallRequest = true;
3043 uri = itRdvAccount->second;
3044 deviceId = itRdvDevice->second;
3045 confId = "0";
3046 JAMI_DEBUG("Remote host detected. Calling {:s} on device {:s}", uri, deviceId);
3047 }
3048 lk.unlock();
3049
3050 auto account = pimpl_->account_.lock();
3051 std::vector<libjami::MediaMap> mediaMap
3053 pimpl_->account_.lock()->createDefaultMediaList(
3054 pimpl_->account_.lock()->isVideoEnabled()))
3055 : mediaList;
3056
3057 if (!sendCallRequest || (uri == pimpl_->username_ && deviceId == pimpl_->deviceId_)) {
3059 // TODO attach host with media list
3060 hostConference(conversationId, confId, "", mediaMap);
3061 return {};
3062 }
3063
3064 // Else we need to create a call
3065 auto& manager = Manager::instance();
3066 std::shared_ptr<SIPCall> call = manager.callFactory.newSipCall(account,
3068 mediaMap);
3069
3070 if (not call)
3071 return {};
3072
3073 auto callUri = fmt::format("{}/{}/{}/{}", conversationId, uri, deviceId, confId);
3074 account->getIceOptions([call,
3075 accountId = account->getAccountID(),
3076 callUri,
3077 uri = std::move(uri),
3078 conversationId,
3079 deviceId,
3080 cb = std::move(cb)](auto&& opts) {
3081 if (call->isIceEnabled()) {
3082 if (not call->createIceMediaTransport(false)
3083 or not call->initIceMediaTransport(true,
3084 std::forward<dhtnet::IceTransportOptions>(opts))) {
3085 return;
3086 }
3087 }
3088 JAMI_DEBUG("New outgoing call with {}", uri);
3089 call->setPeerNumber(uri);
3090 call->setPeerUri("swarm:" + uri);
3091
3092 JAMI_DEBUG("Calling: {:s}", callUri);
3094 call->setPeerNumber(callUri);
3095 call->setPeerUri("rdv:" + callUri);
3096 call->addStateListener([accountId, conversationId](Call::CallState call_state,
3098 int) {
3101 emitSignal<libjami::ConfigurationSignal::NeedsHost>(accountId, conversationId);
3102 return true;
3103 }
3104 return true;
3105 });
3106 cb(callUri, DeviceId(deviceId), call);
3107 });
3108
3109 return call;
3110}
3111
3112void
3113ConversationModule::hostConference(const std::string& conversationId,
3114 const std::string& confId,
3115 const std::string& callId,
3116 const std::vector<libjami::MediaMap>& mediaList)
3117{
3118 auto acc = pimpl_->account_.lock();
3119 if (!acc)
3120 return;
3121 auto conf = acc->getConference(confId);
3122 auto createConf = !conf;
3123 std::shared_ptr<SIPCall> call;
3124 if (!callId.empty()) {
3125 call = std::dynamic_pointer_cast<SIPCall>(acc->getCall(callId));
3126 if (!call) {
3127 JAMI_WARNING("No call with id {} found", callId);
3128 return;
3129 }
3130 }
3131 if (createConf) {
3132 conf = std::make_shared<Conference>(acc, confId);
3133 acc->attach(conf);
3134 }
3135
3136 if (!callId.empty())
3137 conf->addSubCall(callId);
3138
3139 if (callId.empty())
3140 conf->attachHost(mediaList);
3141
3142 if (createConf) {
3143 emitSignal<libjami::CallSignal::ConferenceCreated>(acc->getAccountID(),
3144 conversationId,
3145 conf->getConfId());
3146 } else {
3147 conf->reportMediaNegotiationStatus();
3148 emitSignal<libjami::CallSignal::ConferenceChanged>(acc->getAccountID(),
3149 conf->getConfId(),
3150 conf->getStateStr());
3151 return;
3152 }
3153
3154 auto conv = pimpl_->getConversation(conversationId);
3155 if (!conv)
3156 return;
3157 std::unique_lock lk(conv->mtx);
3158 if (!conv->conversation) {
3159 JAMI_ERROR("Conversation {} not found", conversationId);
3160 return;
3161 }
3162 // Add commit to conversation
3163 Json::Value value;
3164 value["uri"] = pimpl_->username_;
3165 value["device"] = pimpl_->deviceId_;
3166 value["confId"] = conf->getConfId();
3167 value["type"] = "application/call-history+json";
3168 conv->conversation->hostConference(std::move(value),
3169 [w = pimpl_->weak(),
3170 conversationId](bool ok, const std::string& commitId) {
3171 if (ok) {
3172 if (auto shared = w.lock())
3173 shared->sendMessageNotification(conversationId,
3174 true,
3175 commitId);
3176 } else {
3177 JAMI_ERR("Failed to send message to conversation %s",
3178 conversationId.c_str());
3179 }
3180 });
3181
3182 // When conf finished = remove host & commit
3183 // Master call, so when it's stopped, the conference will be stopped (as we use the hold
3184 // state for detaching the call)
3185 conf->onShutdown([w = pimpl_->weak(),
3186 accountUri = pimpl_->username_,
3187 confId = conf->getConfId(),
3188 conversationId,
3189 conv](int duration) {
3190 auto shared = w.lock();
3191 if (shared) {
3192 Json::Value value;
3193 value["uri"] = accountUri;
3194 value["device"] = shared->deviceId_;
3195 value["confId"] = confId;
3196 value["type"] = "application/call-history+json";
3197 value["duration"] = std::to_string(duration);
3198
3199 std::lock_guard lk(conv->mtx);
3200 if (!conv->conversation) {
3201 JAMI_ERROR("Conversation {} not found", conversationId);
3202 return;
3203 }
3204 conv->conversation->removeActiveConference(
3205 std::move(value), [w, conversationId](bool ok, const std::string& commitId) {
3206 if (ok) {
3207 if (auto shared = w.lock()) {
3208 shared->sendMessageNotification(conversationId, true, commitId);
3209 }
3210 } else {
3211 JAMI_ERROR("Failed to send message to conversation {}", conversationId);
3212 }
3213 });
3214 }
3215 });
3216}
3217
3218std::map<std::string, ConvInfo>
3219ConversationModule::convInfos(const std::string& accountId)
3220{
3221 return convInfosFromPath(fileutils::get_data_dir() / accountId);
3222}
3223
3224std::map<std::string, ConvInfo>
3225ConversationModule::convInfosFromPath(const std::filesystem::path& path)
3226{
3227 std::map<std::string, ConvInfo> convInfos;
3228 try {
3229 // read file
3230 std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convInfo"));
3231 auto file = fileutils::loadFile("convInfo", path);
3232 // load values
3233 msgpack::unpacked result;
3234 msgpack::unpack(result, (const char*) file.data(), file.size());
3235 result.get().convert(convInfos);
3236 } catch (const std::exception& e) {
3237 JAMI_WARN("[convInfo] error loading convInfo: %s", e.what());
3238 }
3239 return convInfos;
3240}
3241
3242std::map<std::string, ConversationRequest>
3243ConversationModule::convRequests(const std::string& accountId)
3244{
3245 auto path = fileutils::get_data_dir() / accountId;
3246 return convRequestsFromPath(path.string());
3247}
3248
3249std::map<std::string, ConversationRequest>
3250ConversationModule::convRequestsFromPath(const std::filesystem::path& path)
3251{
3252 std::map<std::string, ConversationRequest> convRequests;
3253 try {
3254 // read file
3255 std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convRequests"));
3256 auto file = fileutils::loadFile("convRequests", path);
3257 // load values
3258 msgpack::unpacked result;
3259 msgpack::unpack(result, (const char*) file.data(), file.size(), 0);
3260 result.get().convert(convRequests);
3261 } catch (const std::exception& e) {
3262 JAMI_WARN("[convInfo] error loading convInfo: %s", e.what());
3263 }
3264 return convRequests;
3265}
3266
3267void
3268ConversationModule::addConvInfo(const ConvInfo& info)
3269{
3270 pimpl_->addConvInfo(info);
3271}
3272
3273void
3274ConversationModule::Impl::setConversationMembers(const std::string& convId,
3275 const std::set<std::string>& members)
3276{
3277 if (auto conv = getConversation(convId)) {
3278 std::lock_guard lk(conv->mtx);
3279 conv->info.members = members;
3280 addConvInfo(conv->info);
3281 }
3282}
3283
3284std::shared_ptr<Conversation>
3285ConversationModule::getConversation(const std::string& convId)
3286{
3287 if (auto conv = pimpl_->getConversation(convId)) {
3288 std::lock_guard lk(conv->mtx);
3289 return conv->conversation;
3290 }
3291 return nullptr;
3292}
3293
3294std::shared_ptr<dhtnet::ChannelSocket>
3295ConversationModule::gitSocket(std::string_view deviceId, std::string_view convId) const
3296{
3297 if (auto conv = pimpl_->getConversation(convId)) {
3298 std::lock_guard lk(conv->mtx);
3299 if (conv->conversation)
3300 return conv->conversation->gitSocket(DeviceId(deviceId));
3301 else if (conv->pending)
3302 return conv->pending->socket;
3303 }
3304 return nullptr;
3305}
3306
3307void
3308ConversationModule::addGitSocket(std::string_view deviceId,
3309 std::string_view convId,
3310 const std::shared_ptr<dhtnet::ChannelSocket>& channel)
3311{
3312 if (auto conv = pimpl_->getConversation(convId)) {
3313 std::lock_guard lk(conv->mtx);
3314 conv->conversation->addGitSocket(DeviceId(deviceId), channel);
3315 } else
3316 JAMI_WARNING("addGitSocket: Unable to find conversation {:s}", convId);
3317}
3318
3319void
3320ConversationModule::removeGitSocket(std::string_view deviceId, std::string_view convId)
3321{
3322 pimpl_->withConversation(convId, [&](auto& conv) { conv.removeGitSocket(DeviceId(deviceId)); });
3323}
3324
3325void
3326ConversationModule::shutdownConnections()
3327{
3328 for (const auto& c : pimpl_->getSyncedConversations()) {
3329 std::lock_guard lkc(c->mtx);
3330 if (c->conversation)
3331 c->conversation->shutdownConnections();
3332 if (c->pending)
3333 c->pending->socket = {};
3334 }
3335}
3336void
3337ConversationModule::addSwarmChannel(const std::string& conversationId,
3338 std::shared_ptr<dhtnet::ChannelSocket> channel)
3339{
3340 pimpl_->withConversation(conversationId,
3341 [&](auto& conv) { conv.addSwarmChannel(std::move(channel)); });
3342}
3343
3344void
3345ConversationModule::connectivityChanged()
3346{
3347 for (const auto& conv : pimpl_->getConversations())
3348 conv->connectivityChanged();
3349}
3350
3351std::shared_ptr<Typers>
3352ConversationModule::getTypers(const std::string& convId)
3353{
3354 if (auto c = pimpl_->getConversation(convId)) {
3355 std::lock_guard lk(c->mtx);
3356 if (c->conversation)
3357 return c->conversation->typers();
3358 }
3359 return nullptr;
3360}
3361
3362} // namespace jami
#define _(S)
Definition SHA3.cpp:123
std::string getNewCallID() const
CallState
The Call State.
Definition call.h:99
ConnectionState
Tell where we're at with the call.
Definition call.h:85
std::map< std::string, ConversationRequest > conversationsRequests_
std::string getOneToOneConversation(const std::string &uri) const noexcept
void fallbackClone(const asio::error_code &ec, const std::string &conversationId)
bool removeConversationImpl(SyncedConversation &conv)
std::shared_ptr< AccountManager > accountManager_
void editMessage(const std::string &conversationId, const std::string &newBody, const std::string &editedId)
bool isConversation(const std::string &convId) const
std::vector< std::map< std::string, std::string > > getConversationMembers(const std::string &conversationId, bool includeBanned=false) const
Get members.
std::optional< ConversationRequest > getRequest(const std::string &id) const
std::vector< std::shared_ptr< SyncedConversation > > getSyncedConversations() const
std::map< std::string, std::vector< std::map< std::string, std::string > > > replay_
std::shared_ptr< SyncedConversation > startConversation(const ConvInfo &info)
void rmConversationRequest(const std::string &id)
auto withConversation(const S &convId, T &&cb)
void fetchNewCommits(const std::string &peer, const std::string &deviceId, const std::string &conversationId, const std::string &commitId="")
Pull remote device.
std::shared_ptr< SyncedConversation > getConversation(std::string_view convId) const
std::map< std::string, std::shared_ptr< SyncedConversation >, std::less<> > conversations_
void sendMessage(const std::string &conversationId, Json::Value &&value, const std::string &replyTo="", bool announce=true, OnCommitCb &&onCommit={}, OnDoneCb &&cb={})
void declineOtherConversationWith(const std::string &uri)
void fixStructures(std::shared_ptr< JamiAccount > account, const std::vector< std::tuple< std::string, std::string, std::string > > &updateContactConv, const std::set< std::string > &toRm)
void bootstrap(const std::string &convId)
std::map< std::string, std::string > notSyncedNotification_
void cloneConversation(const std::string &deviceId, const std::string &peer, const std::string &convId)
Clone a conversation (initial) from device.
void removeRepositoryImpl(SyncedConversation &conv, bool sync, bool force=false)
bool addConversationRequest(const std::string &id, const ConversationRequest &req)
void removeRepository(const std::string &convId, bool sync, bool force=false)
Remove a repository and all files.
void cloneConversationFrom(const std::shared_ptr< SyncedConversation > conv, const std::string &deviceId, const std::string &oldConvId="")
std::vector< std::shared_ptr< Conversation > > getConversations() const
std::map< std::string, ConvInfo > convInfos_
bool removeConversation(const std::string &conversationId)
Remove a conversation.
std::shared_ptr< SyncedConversation > getConversation(std::string_view convId)
std::map< std::string, std::map< std::string, std::string > > syncingMetadatas_
std::map< std::string, uint64_t > refreshMessage
auto withConv(const S &convId, T &&cb) const
std::weak_ptr< JamiAccount > account_
void addConvInfo(const ConvInfo &info)
void handlePendingConversation(const std::string &conversationId, const std::string &deviceId)
Handle events to receive new commits.
void setConversationMembers(const std::string &convId, const std::set< std::string > &members)
void sendMessageNotification(const std::string &conversationId, bool sync, const std::string &commitId="", const std::string &deviceId="")
Send a message notification to all members.
std::shared_ptr< SyncedConversation > startConversation(const std::string &convId)
Impl(std::shared_ptr< JamiAccount > &&account, std::shared_ptr< AccountManager > &&accountManager, NeedsSyncingCb &&needsSyncingCb, SengMsgCb &&sendMsgCb, NeedSocketCb &&onNeedSocket, NeedSocketCb &&onNeedSwarmSocket, OneToOneRecvCb &&oneToOneRecvCb)
bool updateConvForContact(const std::string &uri, const std::string &oldConv, const std::string &newConv)
void removeContact(const std::string &uri, bool ban)
Remove one to one conversations related to a contact.
void onConversationRequest(const std::string &from, const Json::Value &value)
Called when receiving a new conversation's request.
bool onMessageDisplayed(const std::string &peer, const std::string &conversationId, const std::string &interactionId)
void editMessage(const std::string &conversationId, const std::string &newBody, const std::string &editedId)
static void saveConvInfosToPath(const std::filesystem::path &path, const std::map< std::string, ConvInfo > &conversations)
static void saveConvInfos(const std::string &accountId, const std::map< std::string, ConvInfo > &conversations)
void search(uint32_t req, const std::string &convId, const Filter &filter) const
Search in conversations via a filter.
void initReplay(const std::string &oldConvId, const std::string &newConvId)
std::vector< std::map< std::string, std::string > > getConversationRequests() const
Return conversation's requests.
void syncConversations(const std::string &peer, const std::string &deviceId)
Sync conversations with detected peer.
std::shared_ptr< Conversation > getConversation(const std::string &convId)
Get a conversation.
void setAccountManager(std::shared_ptr< AccountManager > accountManager)
void addConversationMember(const std::string &conversationId, const dht::InfoHash &contactUri, bool sendRequest=true)
Adds a new member to a conversation (this will triggers a member event + new message on success)
bool onFileChannelRequest(const std::string &conversationId, const std::string &member, const std::string &fileId, bool verifyShaSum=true) const
Choose if we can accept channel request.
void clearPendingFetch()
Clear not removed fetch.
void reloadRequests()
Reload requests from file.
void onNeedConversationRequest(const std::string &from, const std::string &conversationId)
Called when a peer needs an invite for a conversation (generally after that they received a commit no...
uint32_t loadConversationMessages(const std::string &conversationId, const std::string &fromMessage="", size_t n=0)
Load conversation's messages.
static void saveConvRequestsToPath(const std::filesystem::path &path, const std::map< std::string, ConversationRequest > &conversationsRequests)
std::map< std::string, std::map< std::string, std::string > > convPreferences() const
Retrieve all conversation preferences to sync with other devices.
std::vector< std::string > getConversations() const
Return all conversation's id (including syncing ones)
static void saveConvRequests(const std::string &accountId, const std::map< std::string, ConversationRequest > &conversationsRequests)
void setConversationPreferences(const std::string &conversationId, const std::map< std::string, std::string > &prefs)
Update user's preferences (like color, notifications, etc) to be synced across devices.
bool needsSyncingWith(const std::string &memberUri, const std::string &deviceId) const
Check if we need to share infos with a contact.
ConversationModule(std::shared_ptr< JamiAccount > account, std::shared_ptr< AccountManager > accountManager, NeedsSyncingCb &&needsSyncingCb, SengMsgCb &&sendMsgCb, NeedSocketCb &&onNeedSocket, NeedSocketCb &&onNeedSwarmSocket, OneToOneRecvCb &&oneToOneRecvCb, bool autoLoadConversations=true)
std::map< std::string, std::string > getConversationPreferences(const std::string &conversationId, bool includeCreated=false) const
static std::map< std::string, ConversationRequest > convRequests(const std::string &accountId)
std::map< std::string, std::string > conversationInfos(const std::string &conversationId) const
void acceptConversationRequest(const std::string &conversationId, const std::string &deviceId="")
Accept a conversation's request.
std::shared_ptr< SIPCall > call(const std::string &url, const std::vector< libjami::MediaMap > &mediaList, std::function< void(const std::string &, const DeviceId &, const std::shared_ptr< SIPCall > &)> &&cb)
Call the conversation.
bool updateConvForContact(const std::string &uri, const std::string &oldConv, const std::string &newConv)
Replace linked conversation in contact's details.
std::string getOneToOneConversation(const std::string &uri) const noexcept
Get related conversation with member.
void bootstrap(const std::string &convId="")
Bootstrap swarm managers to other peers.
std::string startConversation(ConversationMode mode=ConversationMode::INVITES_ONLY, const dht::InfoHash &otherMember={})
Starts a new conversation.
void fetchNewCommits(const std::string &peer, const std::string &deviceId, const std::string &conversationId, const std::string &commitId)
Launch fetch on new commit.
uint32_t loadConversationUntil(const std::string &conversationId, const std::string &fromMessage, const std::string &to)
void setFetched(const std::string &conversationId, const std::string &deviceId, const std::string &commit)
Notify that a peer fetched a commit.
void sendMessage(const std::string &conversationId, Json::Value &&value, const std::string &replyTo="", bool announce=true, OnCommitCb &&onCommit={}, OnDoneCb &&cb={})
bool isHosting(const std::string &conversationId, const std::string &confId) const
Check if we're hosting a specific conference.
uint32_t loadConversation(const std::string &conversationId, const std::string &fromMessage="", size_t n=0)
void removeConversationMember(const std::string &conversationId, const dht::InfoHash &contactUri, bool isDevice=false)
Remove a member from a conversation (this will trigger a member event + new message on success)
uint32_t countInteractions(const std::string &convId, const std::string &toId, const std::string &fromId, const std::string &authorUri) const
Retrieve the number of interactions from interactionId to HEAD.
bool downloadFile(const std::string &conversationId, const std::string &interactionId, const std::string &fileId, const std::string &path)
Ask conversation's members to send a file to this device.
std::vector< uint8_t > conversationVCard(const std::string &conversationId) const
static std::map< std::string, ConvInfo > convInfos(const std::string &accountId)
void addCallHistoryMessage(const std::string &uri, uint64_t duration_ms, const std::string &reason)
Add to the related conversation the call history message.
void clearCache(const std::string &conversationId)
Clear loaded interactions.
void reactToMessage(const std::string &conversationId, const std::string &newBody, const std::string &reactToId)
std::vector< std::map< std::string, std::string > > getConversationMembers(const std::string &conversationId, bool includeBanned=false) const
Get members.
void hostConference(const std::string &conversationId, const std::string &confId, const std::string &callId, const std::vector< libjami::MediaMap > &mediaList={})
uint32_t loadSwarmUntil(const std::string &conversationId, const std::string &fromMessage, const std::string &toMessage)
void onSyncData(const SyncMsg &msg, const std::string &peerId, const std::string &deviceId)
Detect new conversations and request from other devices.
void loadConversations()
Refresh information about conversations.
void addConvInfo(const ConvInfo &info)
void loadSingleConversation(const std::string &convId)
bool isBanned(const std::string &convId, const std::string &uri) const
Return if a device or member is banned from a conversation.
std::vector< std::map< std::string, std::string > > getActiveCalls(const std::string &conversationId) const
Return active calls.
void updateConversationInfos(const std::string &conversationId, const std::map< std::string, std::string > &infos, bool sync=true)
Update metadatas from conversations (like title, avatar, etc)
void cloneConversationFrom(const std::string &conversationId, const std::string &uri, const std::string &oldConvId="")
Clone conversation from a member.
std::string peerFromConversationRequest(const std::string &convId) const
Retrieve author of a conversation request.
void declineConversationRequest(const std::string &conversationId)
Decline a conversation's request.
void onTrustRequest(const std::string &uri, const std::string &conversationId, const std::vector< uint8_t > &payload, time_t received)
Called when detecting a new trust request with linked one to one.
std::shared_ptr< TransferManager > dataTransfer(const std::string &id) const
Returns related transfer manager.
bool removeConversation(const std::string &conversationId)
Remove a conversation, but not the contact.
std::map< std::string, std::map< std::string, std::map< std::string, std::string > > > convMessageStatus() const
static std::map< std::string, std::string > infosFromVCard(std::map< std::string, std::string > &&details)
std::set< std::string > memberUris(std::string_view filter={}, const std::set< MemberRole > &filteredRoles={MemberRole::INVITED, MemberRole::LEFT, MemberRole::BANNED}) const
std::string id() const
Get conversation's id.
std::vector< NodeId > peersToSyncWith() const
Get peers to sync with.
std::string uriFromDevice(const std::string &deviceId) const
Retrieve the uri from a deviceId.
bool isBootstraped() const
Check if we're at least connected to one node.
std::string lastCommitId() const
Get last commit id.
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:676
CallFactory callFactory
Definition manager.h:794
static std::vector< libjami::MediaMap > mediaAttributesToMediaMaps(std::vector< MediaAttribute > mediaAttrList)
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_ERROR(formatstr,...)
Definition logger.h:228
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARN(...)
Definition logger.h:217
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
#define JAMI_LOG(formatstr,...)
Definition logger.h:225
static constexpr const char * LAST_DISPLAYED
const std::filesystem::path & get_data_dir()
std::string sha3File(const std::filesystem::path &path)
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.
std::string toString(const Json::Value &jsonVal)
Definition json_utils.h:42
static constexpr const char MIME_TYPE_INVITE[]
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)
static constexpr const char MIME_TYPE_GIT[]
void emitSignal(Args... args)
Definition ring_signal.h:64
std::function< void(bool, const std::string &)> OnDoneCb
constexpr std::chrono::seconds MAX_FALLBACK
std::function< void(const std::string &, const std::string &)> OneToOneRecvCb
static constexpr float kd
std::function< void(const std::string &)> OnCommitCb
std::vector< std::string_view > split_string(std::string_view str, char delim)
std::function< void(std::shared_ptr< SyncMsg > &&)> NeedsSyncingCb
std::function< uint64_t(const std::string &, const DeviceId &, std::map< std::string, std::string >, uint64_t)> SengMsgCb
std::map< std::string, ConvInfo > ConvInfoMap
static constexpr const char CONVERSATIONID[]
static constexpr const char FROM[]
std::map< std::string, std::string > toMap(std::string_view content)
Payload to vCard.
Definition vcard.cpp:25
SIPCall are SIP implementation of a normal Call.
std::set< std::string > members
A ConversationRequest is a request which corresponds to a trust request, but for conversations It's s...
std::map< std::string, std::string > metadatas
std::map< std::string, std::string > toMap() const
std::set< std::string > connectingTo
std::map< std::string, std::map< std::string, std::string > > status
std::map< std::string, std::string > preferences
std::shared_ptr< dhtnet::ChannelSocket > socket
std::unique_ptr< asio::steady_timer > fallbackClone
void stopFetch(const std::string &deviceId)
std::chrono::seconds fallbackTimer
bool startFetch(const std::string &deviceId, bool checkIfConv=false)
SyncedConversation(const std::string &convId)
std::vector< std::map< std::string, std::string > > getMembers(bool includeLeft, bool includeBanned) const
std::unique_ptr< PendingConversationFetch > pending
SyncedConversation(const ConvInfo &info)
std::shared_ptr< Conversation > conversation