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 if (convId.empty()) {
1416 std::lock_guard lk(convInfosMtx_);
1417 for (const auto& [conversationId, convInfo] : convInfos_) {
1418 auto conv = getConversation(conversationId);
1419 if (!conv)
1420 return;
1421 if ((!conv->conversation && !conv->info.isRemoved())) {
1422 // Because we're not tracking contact presence in order to sync now,
1423 // we need to ask to clone requests when bootstraping all conversations
1424 // else it can stay syncing
1425 toClone.emplace_back(conversationId);
1426 } else if (conv->conversation) {
1427 bootstrap(conv->conversation);
1428 }
1429 }
1430 } else if (auto conv = getConversation(convId)) {
1431 std::lock_guard lk(conv->mtx);
1432 if (conv->conversation)
1433 bootstrap(conv->conversation);
1434 }
1435
1436 for (const auto& cid : toClone) {
1437 auto members = getConversationMembers(cid);
1438 for (const auto& member : members) {
1439 if (member.at("uri") != username_)
1440 cloneConversationFrom(cid, member.at("uri"));
1441 }
1442 }
1443}
1444
1445void
1446ConversationModule::Impl::cloneConversationFrom(const std::string& conversationId,
1447 const std::string& uri,
1448 const std::string& oldConvId)
1449{
1450 auto memberHash = dht::InfoHash(uri);
1451 if (!memberHash) {
1452 JAMI_WARNING("Invalid member detected: {}", uri);
1453 return;
1454 }
1455 auto conv = startConversation(conversationId);
1456 std::lock_guard lk(conv->mtx);
1457 conv->info = {conversationId};
1458 conv->info.created = std::time(nullptr);
1459 conv->info.members.emplace(username_);
1460 conv->info.members.emplace(uri);
1461 accountManager_->forEachDevice(memberHash,
1462 [w = weak(), conv, conversationId, oldConvId](
1463 const std::shared_ptr<dht::crypto::PublicKey>& pk) {
1464 auto sthis = w.lock();
1465 auto deviceId = pk->getLongId().toString();
1466 if (!sthis or deviceId == sthis->deviceId_)
1467 return;
1468 sthis->cloneConversationFrom(conv, deviceId, oldConvId);
1469 });
1470 addConvInfo(conv->info);
1471}
1472
1474
1475void
1477 const std::string& accountId,
1478 const std::map<std::string, ConversationRequest>& conversationsRequests)
1479{
1480 auto path = fileutils::get_data_dir() / accountId;
1481 saveConvRequestsToPath(path, conversationsRequests);
1482}
1483
1484void
1486 const std::filesystem::path& path,
1487 const std::map<std::string, ConversationRequest>& conversationsRequests)
1488{
1489 auto p = path / "convRequests";
1490 std::lock_guard lock(dhtnet::fileutils::getFileLock(p));
1491 std::ofstream file(p, std::ios::trunc | std::ios::binary);
1492 msgpack::pack(file, conversationsRequests);
1493}
1494
1495void
1496ConversationModule::saveConvInfos(const std::string& accountId, const ConvInfoMap& conversations)
1497{
1498 auto path = fileutils::get_data_dir() / accountId;
1499 saveConvInfosToPath(path, conversations);
1500}
1501
1502void
1503ConversationModule::saveConvInfosToPath(const std::filesystem::path& path,
1504 const ConvInfoMap& conversations)
1505{
1506 std::ofstream file(path / "convInfo", std::ios::trunc | std::ios::binary);
1507 msgpack::pack(file, conversations);
1508}
1509
1511
1513 std::shared_ptr<AccountManager> accountManager,
1516 NeedSocketCb&& onNeedSocket,
1520 : pimpl_ {std::make_unique<Impl>(std::move(account),
1521 std::move(accountManager),
1523 std::move(sendMsgCb),
1524 std::move(onNeedSocket),
1527{
1530 }
1531}
1532
1533void
1534ConversationModule::setAccountManager(std::shared_ptr<AccountManager> accountManager)
1535{
1536 std::unique_lock lk(pimpl_->conversationsMtx_);
1537 pimpl_->accountManager_ = accountManager;
1538}
1539
1540#ifdef LIBJAMI_TEST
1541void
1542ConversationModule::onBootstrapStatus(
1543 const std::function<void(std::string, Conversation::BootstrapStatus)>& cb)
1544{
1545 pimpl_->bootstrapCbTest_ = cb;
1546 for (auto& c : pimpl_->getConversations())
1548}
1549#endif
1550
1551void
1553{
1554 auto acc = pimpl_->account_.lock();
1555 if (!acc)
1556 return;
1557 JAMI_LOG("[Account {}] Start loading conversations…", pimpl_->accountId_);
1558 auto conversationsRepositories = dhtnet::fileutils::readDirectory(
1559 fileutils::get_data_dir() / pimpl_->accountId_ / "conversations");
1560
1561 std::unique_lock lk(pimpl_->conversationsMtx_);
1562 auto contacts = pimpl_->accountManager_->getContacts(
1563 true); // Avoid to lock configurationMtx while conv Mtx is locked
1564 std::unique_lock ilk(pimpl_->convInfosMtx_);
1565 pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
1566 pimpl_->conversations_.clear();
1567
1568 struct Ctx
1569 {
1570 std::mutex cvMtx;
1571 std::condition_variable cv;
1572 std::mutex toRmMtx;
1573 std::set<std::string> toRm;
1574 std::mutex convMtx;
1575 size_t convNb;
1576 std::vector<std::map<std::string, std::string>> contacts;
1577 std::vector<std::tuple<std::string, std::string, std::string>> updateContactConv;
1578 };
1579 auto ctx = std::make_shared<Ctx>();
1580 ctx->convNb = conversationsRepositories.size();
1581 ctx->contacts = std::move(contacts);
1582
1583 for (auto&& r : conversationsRepositories) {
1584 dht::ThreadPool::io().run([this, ctx, repository = std::move(r), acc] {
1585 try {
1586 auto sconv = std::make_shared<SyncedConversation>(repository);
1587 auto conv = std::make_shared<Conversation>(acc, repository);
1588 conv->onMessageStatusChanged([this, repository](const auto& status) {
1589 auto msg = std::make_shared<SyncMsg>();
1590 msg->ms = {{repository, status}};
1591 pimpl_->needsSyncingCb_(std::move(msg));
1592 });
1593 conv->onMembersChanged(
1594 [w = pimpl_->weak_from_this(), repository](const auto& members) {
1595 // Delay in another thread to avoid deadlocks
1596 dht::ThreadPool::io().run([w, repository, members = std::move(members)] {
1597 if (auto sthis = w.lock())
1598 sthis->setConversationMembers(repository, members);
1599 });
1600 });
1601 conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
1602 auto members = conv->memberUris(acc->getUsername(), {});
1603 // NOTE: The following if is here to protect against any incorrect state
1604 // that can be introduced
1605 if (conv->mode() == ConversationMode::ONE_TO_ONE && members.size() == 1) {
1606 // If we got a 1:1 conversation, but not in the contact details, it's rather a
1607 // duplicate or a weird state
1608 auto otherUri = *members.begin();
1609 auto itContact = std::find_if(ctx->contacts.cbegin(),
1610 ctx->contacts.cend(),
1611 [&](const auto& c) {
1612 return c.at("id") == otherUri;
1613 });
1614 if (itContact == ctx->contacts.end()) {
1615 JAMI_WARNING("Contact {} not found", otherUri);
1616 std::lock_guard lkCv {ctx->cvMtx};
1617 --ctx->convNb;
1618 ctx->cv.notify_all();
1619 return;
1620 }
1621 const std::string& convFromDetails = itContact->at("conversationId");
1622 auto removed = std::stoul(itContact->at("removed"));
1623 auto added = std::stoul(itContact->at("added"));
1624 auto isRemoved = removed > added;
1625 if (convFromDetails != repository) {
1626 if (convFromDetails.empty()) {
1627 if (isRemoved) {
1628 // If details is empty, contact is removed and not banned.
1629 JAMI_ERROR("Conversation {} detected for {} and should be removed",
1630 repository,
1631 otherUri);
1632 std::lock_guard lkMtx {ctx->toRmMtx};
1633 ctx->toRm.insert(repository);
1634 } else {
1635 JAMI_ERROR("No conversation detected for {} but one exists ({}). "
1636 "Update details",
1637 otherUri,
1638 repository);
1639 std::lock_guard lkMtx {ctx->toRmMtx};
1640 ctx->updateContactConv.emplace_back(
1641 std::make_tuple(otherUri, convFromDetails, repository));
1642 }
1643 } else {
1644 JAMI_ERROR("Multiple conversation detected for {} but ({} & {})",
1645 otherUri,
1646 repository,
1648 std::lock_guard lkMtx {ctx->toRmMtx};
1649 ctx->toRm.insert(repository);
1650 }
1651 }
1652 }
1653 {
1654 std::lock_guard lkMtx {ctx->convMtx};
1655 auto convInfo = pimpl_->convInfos_.find(repository);
1656 if (convInfo == pimpl_->convInfos_.end()) {
1657 JAMI_ERROR("Missing conv info for {}. This is a bug!", repository);
1658 sconv->info.created = std::time(nullptr);
1659 sconv->info.lastDisplayed
1661 } else {
1662 sconv->info = convInfo->second;
1663 if (convInfo->second.isRemoved()) {
1664 // A conversation was removed, but repository still exists
1665 conv->setRemovingFlag();
1666 std::lock_guard lkMtx {ctx->toRmMtx};
1667 ctx->toRm.insert(repository);
1668 }
1669 }
1670 // Even if we found the conversation in convInfos_, unable to assume that the
1671 // list of members stored in `convInfo` is correct
1672 // (https://git.jami.net/savoirfairelinux/jami-daemon/-/issues/1025). For this
1673 // reason, we always use the list we got from the conversation repository to set
1674 // the value of `sconv->info.members`.
1675 members.emplace(acc->getUsername());
1676 sconv->info.members = std::move(members);
1677 // convInfosMtx_ is already locked
1678 pimpl_->convInfos_[repository] = sconv->info;
1679 }
1680 auto commits = conv->commitsEndedCalls();
1681
1682 if (!commits.empty()) {
1683 // Note: here, this means that some calls were actives while the
1684 // daemon finished (can be a crash).
1685 // Notify other in the conversation that the call is finished
1686 pimpl_->sendMessageNotification(*conv, true, *commits.rbegin());
1687 }
1688 sconv->conversation = conv;
1689 std::lock_guard lkMtx {ctx->convMtx};
1690 pimpl_->conversations_.emplace(repository, std::move(sconv));
1691 } catch (const std::logic_error& e) {
1692 JAMI_WARNING("[Account {}] Conversations not loaded: {}",
1693 pimpl_->accountId_,
1694 e.what());
1695 }
1696 std::lock_guard lkCv {ctx->cvMtx};
1697 --ctx->convNb;
1698 ctx->cv.notify_all();
1699 });
1700 }
1701
1702 std::unique_lock lkCv(ctx->cvMtx);
1703 ctx->cv.wait(lkCv, [&] { return ctx->convNb == 0; });
1704
1705 // Prune any invalid conversations without members and
1706 // set the removed flag if needed
1707 std::set<std::string> removed;
1708 for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
1709 const auto& info = itInfo->second;
1710 if (info.members.empty()) {
1711 itInfo = pimpl_->convInfos_.erase(itInfo);
1712 continue;
1713 }
1714 if (info.isRemoved())
1715 removed.insert(info.id);
1716 auto itConv = pimpl_->conversations_.find(info.id);
1717 if (itConv == pimpl_->conversations_.end()) {
1718 // convInfos_ can contain a conversation that is not yet cloned
1719 // so we need to add it there.
1720 itConv = pimpl_->conversations_
1721 .emplace(info.id, std::make_shared<SyncedConversation>(info))
1722 .first;
1723 }
1724 if (itConv != pimpl_->conversations_.end() && itConv->second && itConv->second->conversation
1725 && info.isRemoved())
1726 itConv->second->conversation->setRemovingFlag();
1727 if (!info.isRemoved() && itConv == pimpl_->conversations_.end()) {
1728 // In this case, the conversation is not synced and we only know ourself
1729 if (info.members.size() == 1 && *info.members.begin() == acc->getUsername()) {
1730 JAMI_WARNING("[Account {:s}] Conversation {:s} seems not present/synced.",
1731 pimpl_->accountId_,
1732 info.id);
1734 info.id);
1735 itInfo = pimpl_->convInfos_.erase(itInfo);
1736 continue;
1737 }
1738 }
1739 ++itInfo;
1740 }
1741 // On oldest version, removeConversation didn't update "appdata/contacts"
1742 // causing a potential incorrect state between "appdata/contacts" and "appdata/convInfos"
1743 if (!removed.empty())
1744 acc->unlinkConversations(removed);
1745 // Save if we've removed some invalid entries
1746 pimpl_->saveConvInfos();
1747
1748 ilk.unlock();
1749 lk.unlock();
1750
1751 dht::ThreadPool::io().run([w = pimpl_->weak(),
1752 acc,
1753 updateContactConv = std::move(ctx->updateContactConv),
1754 toRm = std::move(ctx->toRm)]() {
1755 // Will lock account manager
1756 if (auto shared = w.lock())
1757 shared->fixStructures(acc, updateContactConv, toRm);
1758 });
1759}
1760
1761void
1763{
1764 auto acc = pimpl_->account_.lock();
1765 if (!acc)
1766 return;
1767 JAMI_LOG("[Account {}] Start loading conversation {}", pimpl_->accountId_, convId);
1768
1769 std::unique_lock lk(pimpl_->conversationsMtx_);
1770 std::unique_lock ilk(pimpl_->convInfosMtx_);
1771 // Load convInfos to retrieve requests that have been accepted but not yet synchronized.
1772 pimpl_->convInfos_ = convInfos(pimpl_->accountId_);
1773 pimpl_->conversations_.clear();
1774
1775 try {
1776 auto sconv = std::make_shared<SyncedConversation>(convId);
1777
1778 auto conv = std::make_shared<Conversation>(acc, convId);
1779
1780 conv->onNeedSocket(pimpl_->onNeedSwarmSocket_);
1781
1782 sconv->conversation = conv;
1783 pimpl_->conversations_.emplace(convId, std::move(sconv));
1784 } catch (const std::logic_error& e) {
1785 JAMI_WARNING("[Account {}] Conversations not loaded: {}", pimpl_->accountId_, e.what());
1786 }
1787
1788 // Add all other conversations as dummy conversations to indicate their existence so
1789 // isConversation could detect conversations correctly.
1790 auto conversationsRepositoryIds = dhtnet::fileutils::readDirectory(
1791 fileutils::get_data_dir() / pimpl_->accountId_ / "conversations");
1793 if (repositoryId != convId) {
1794 auto conv = std::make_shared<SyncedConversation>(repositoryId);
1795 pimpl_->conversations_.emplace(repositoryId, conv);
1796 }
1797 }
1798
1799 // Add conversations from convInfos_ so isConversation could detect conversations correctly.
1800 // This includes conversations that have been accepted but are not yet synchronized.
1801 for (auto itInfo = pimpl_->convInfos_.begin(); itInfo != pimpl_->convInfos_.end();) {
1802 const auto& info = itInfo->second;
1803 if (info.members.empty()) {
1804 itInfo = pimpl_->convInfos_.erase(itInfo);
1805 continue;
1806 }
1807 auto itConv = pimpl_->conversations_.find(info.id);
1808 if (itConv == pimpl_->conversations_.end()) {
1809 // convInfos_ can contain a conversation that is not yet cloned
1810 // so we need to add it there.
1811 pimpl_->conversations_.emplace(info.id, std::make_shared<SyncedConversation>(info));
1812 }
1813 ++itInfo;
1814 }
1815
1816 ilk.unlock();
1817 lk.unlock();
1818}
1819
1820void
1822{
1823 pimpl_->bootstrap(convId);
1824}
1825
1826void
1828{
1829 for (auto& conv : pimpl_->getConversations())
1830 conv->monitor();
1831}
1832
1833void
1835{
1836 // Note: This is a workaround. convModule() is kept if account is disabled/re-enabled.
1837 // iOS uses setAccountActive() a lot, and if for some reason the previous pending fetch
1838 // is not erased (callback not called), it will block the new messages as it will not
1839 // sync. The best way to debug this is to get logs from the last ICE connection for
1840 // syncing the conversation. It may have been killed in some un-expected way avoiding to
1841 // call the callbacks. This should never happen, but if it's the case, this will allow
1842 // new messages to be synced correctly.
1843 for (auto& conv : pimpl_->getSyncedConversations()) {
1844 std::lock_guard lk(conv->mtx);
1845 if (conv && conv->pending) {
1846 JAMI_ERR("This is a bug, seems to still fetch to some device on initializing");
1847 conv->pending.reset();
1848 }
1849 }
1850}
1851
1852void
1854{
1855 pimpl_->conversationsRequests_ = convRequests(pimpl_->accountId_);
1856}
1857
1858std::vector<std::string>
1860{
1861 std::vector<std::string> result;
1862 std::lock_guard lk(pimpl_->convInfosMtx_);
1863 result.reserve(pimpl_->convInfos_.size());
1864 for (const auto& [key, conv] : pimpl_->convInfos_) {
1865 if (conv.isRemoved())
1866 continue;
1867 result.emplace_back(key);
1868 }
1869 return result;
1870}
1871
1872std::string
1873ConversationModule::getOneToOneConversation(const std::string& uri) const noexcept
1874{
1875 return pimpl_->getOneToOneConversation(uri);
1876}
1877
1878bool
1880 const std::string& oldConv,
1881 const std::string& newConv)
1882{
1883 return pimpl_->updateConvForContact(uri, oldConv, newConv);
1884}
1885
1886std::vector<std::map<std::string, std::string>>
1888{
1889 std::vector<std::map<std::string, std::string>> requests;
1890 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
1891 requests.reserve(pimpl_->conversationsRequests_.size());
1892 for (const auto& [id, request] : pimpl_->conversationsRequests_) {
1893 if (request.declined)
1894 continue; // Do not add declined requests
1895 requests.emplace_back(request.toMap());
1896 }
1897 return requests;
1898}
1899
1900void
1902 const std::string& conversationId,
1903 const std::vector<uint8_t>& payload,
1904 time_t received)
1905{
1906 auto oldConv = getOneToOneConversation(uri);
1907 if (!oldConv.empty() && pimpl_->isConversation(oldConv)) {
1908 // If there is already an active one to one conversation here, it's an active
1909 // contact and the contact will reclone this activeConv, so ignore the request
1911 "Contact is sending a request for a non active conversation. Ignore. They will "
1912 "clone the old one");
1913 return;
1914 }
1915 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
1917 req.from = uri;
1918 req.conversationId = conversationId;
1919 req.received = std::time(nullptr);
1921 std::string_view(reinterpret_cast<const char*>(payload.data()), payload.size())));
1922 auto reqMap = req.toMap();
1923 if (pimpl_->addConversationRequest(conversationId, std::move(req))) {
1924 lk.unlock();
1926 conversationId,
1927 uri,
1928 payload,
1929 received);
1931 conversationId,
1932 reqMap);
1933 pimpl_->needsSyncingCb_({});
1934 } else {
1935 JAMI_DEBUG("[Account {}] Received a request for a conversation "
1936 "already existing. Ignore",
1937 pimpl_->accountId_);
1938 }
1939}
1940
1941void
1942ConversationModule::onConversationRequest(const std::string& from, const Json::Value& value)
1943{
1945 auto isOneToOne = req.isOneToOne();
1946 std::string oldConv;
1947 if (isOneToOne) {
1948 oldConv = pimpl_->getOneToOneConversation(from);
1949 }
1950 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
1951 JAMI_DEBUG("[Account {}] Receive a new conversation request for conversation {} from {}",
1952 pimpl_->accountId_,
1953 req.conversationId,
1954 from);
1955 auto convId = req.conversationId;
1956
1957 // Already accepted request, do nothing
1958 if (pimpl_->isConversation(convId))
1959 return;
1960 auto oldReq = pimpl_->getRequest(convId);
1961 if (oldReq != std::nullopt) {
1962 JAMI_DEBUG("[Account {}] Received a request for a conversation already existing. "
1963 "Ignore. Declined: {}",
1964 pimpl_->accountId_,
1965 static_cast<int>(oldReq->declined));
1966 return;
1967 }
1968
1969 if (!oldConv.empty()) {
1970 lk.unlock();
1971 // Already a conversation with the contact.
1972 // If there is already an active one to one conversation here, it's an active
1973 // contact and the contact will reclone this activeConv, so ignore the request
1975 "Contact is sending a request for a non active conversation. Ignore. They will "
1976 "clone the old one");
1977 return;
1978 }
1979
1980 req.received = std::time(nullptr);
1981 req.from = from;
1982 auto reqMap = req.toMap();
1983 if (pimpl_->addConversationRequest(convId, std::move(req))) {
1984 lk.unlock();
1985 // Note: no need to sync here because other connected devices should receive
1986 // the same conversation request. Will sync when the conversation will be added
1987 if (isOneToOne)
1988 pimpl_->oneToOneRecvCb_(convId, from);
1990 convId,
1991 reqMap);
1992 }
1993}
1994
1995std::string
1997{
1998 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
1999 auto it = pimpl_->conversationsRequests_.find(convId);
2000 if (it != pimpl_->conversationsRequests_.end()) {
2001 return it->second.from;
2002 }
2003 return {};
2004}
2005
2006void
2008 const std::string& conversationId)
2009{
2010 pimpl_->withConversation(conversationId, [&](auto& conversation) {
2011 if (!conversation.isMember(from, true)) {
2012 JAMI_WARNING("{} is asking a new invite for {}, but not a member", from, conversationId);
2013 return;
2014 }
2015 JAMI_LOG("{} is asking a new invite for {}", from, conversationId);
2016 pimpl_->sendMsgCb_(from, {}, conversation.generateInvitation(), 0);
2017 });
2018}
2019
2020void
2021ConversationModule::acceptConversationRequest(const std::string& conversationId,
2022 const std::string& deviceId)
2023{
2024 // For all conversation members, try to open a git channel with this conversation ID
2025 std::unique_lock lkCr(pimpl_->conversationsRequestsMtx_);
2026 auto request = pimpl_->getRequest(conversationId);
2027 if (request == std::nullopt) {
2028 lkCr.unlock();
2029 if (auto conv = pimpl_->getConversation(conversationId)) {
2030 std::unique_lock lk(conv->mtx);
2031 if (!conv->conversation) {
2032 lk.unlock();
2033 pimpl_->cloneConversationFrom(conv, deviceId);
2034 }
2035 }
2036 JAMI_WARNING("[Account {}] Request not found for conversation {}",
2037 pimpl_->accountId_,
2038 conversationId);
2039 return;
2040 }
2041 pimpl_->rmConversationRequest(conversationId);
2042 lkCr.unlock();
2043 pimpl_->accountManager_->acceptTrustRequest(request->from, true);
2044 cloneConversationFrom(conversationId, request->from);
2045}
2046
2047void
2048ConversationModule::declineConversationRequest(const std::string& conversationId)
2049{
2050 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2051 auto it = pimpl_->conversationsRequests_.find(conversationId);
2052 if (it != pimpl_->conversationsRequests_.end()) {
2053 it->second.declined = std::time(nullptr);
2054 pimpl_->saveConvRequests();
2055 }
2056 pimpl_->syncingMetadatas_.erase(conversationId);
2057 pimpl_->saveMetadata();
2059 conversationId);
2060 pimpl_->needsSyncingCb_({});
2061}
2062
2063std::string
2065{
2066 auto acc = pimpl_->account_.lock();
2067 if (!acc)
2068 return {};
2069 std::vector<DeviceId> kd;
2070 for (const auto& [id, _] : acc->getKnownDevices())
2071 kd.emplace_back(id);
2072 // Create the conversation object
2073 std::shared_ptr<Conversation> conversation;
2074 try {
2075 conversation = std::make_shared<Conversation>(acc, mode, otherMember.toString());
2076 auto conversationId = conversation->id();
2077 conversation->onMessageStatusChanged([this, conversationId](const auto& status) {
2078 auto msg = std::make_shared<SyncMsg>();
2079 msg->ms = {{conversationId, status}};
2080 pimpl_->needsSyncingCb_(std::move(msg));
2081 });
2082 conversation->onMembersChanged(
2083 [w = pimpl_->weak_from_this(), conversationId](const auto& members) {
2084 // Delay in another thread to avoid deadlocks
2085 dht::ThreadPool::io().run([w, conversationId, members = std::move(members)] {
2086 if (auto sthis = w.lock())
2087 sthis->setConversationMembers(conversationId, members);
2088 });
2089 });
2090 conversation->onNeedSocket(pimpl_->onNeedSwarmSocket_);
2091#ifdef LIBJAMI_TEST
2092 conversation->onBootstrapStatus(pimpl_->bootstrapCbTest_);
2093#endif
2094 conversation->bootstrap(std::bind(&ConversationModule::Impl::bootstrapCb,
2095 pimpl_.get(),
2096 conversationId),
2097 kd);
2098 } catch (const std::exception& e) {
2099 JAMI_ERROR("[Account {}] Error while generating a conversation {}",
2100 pimpl_->accountId_,
2101 e.what());
2102 return {};
2103 }
2104 auto convId = conversation->id();
2105 auto conv = pimpl_->startConversation(convId);
2106 std::unique_lock lk(conv->mtx);
2107 conv->info.created = std::time(nullptr);
2108 conv->info.members.emplace(pimpl_->username_);
2109 if (otherMember)
2110 conv->info.members.emplace(otherMember.toString());
2111 conv->conversation = conversation;
2112 addConvInfo(conv->info);
2113 lk.unlock();
2114
2115 pimpl_->needsSyncingCb_({});
2117 return convId;
2118}
2119
2120void
2121ConversationModule::cloneConversationFrom(const std::string& conversationId,
2122 const std::string& uri,
2123 const std::string& oldConvId)
2124{
2125 pimpl_->cloneConversationFrom(conversationId, uri, oldConvId);
2126}
2127
2128// Message send/load
2129void
2130ConversationModule::sendMessage(const std::string& conversationId,
2131 std::string message,
2132 const std::string& replyTo,
2133 const std::string& type,
2134 bool announce,
2136 OnDoneCb&& cb)
2137{
2138 pimpl_->sendMessage(conversationId,
2139 std::move(message),
2140 replyTo,
2141 type,
2142 announce,
2143 std::move(onCommit),
2144 std::move(cb));
2145}
2146
2147void
2148ConversationModule::sendMessage(const std::string& conversationId,
2149 Json::Value&& value,
2150 const std::string& replyTo,
2151 bool announce,
2153 OnDoneCb&& cb)
2154{
2155 pimpl_->sendMessage(conversationId,
2156 std::move(value),
2157 replyTo,
2158 announce,
2159 std::move(onCommit),
2160 std::move(cb));
2161}
2162
2163void
2164ConversationModule::editMessage(const std::string& conversationId,
2165 const std::string& newBody,
2166 const std::string& editedId)
2167{
2168 pimpl_->editMessage(conversationId, newBody, editedId);
2169}
2170
2171void
2172ConversationModule::reactToMessage(const std::string& conversationId,
2173 const std::string& newBody,
2174 const std::string& reactToId)
2175{
2176 // Commit message edition
2177 Json::Value json;
2178 json["body"] = newBody;
2179 json["react-to"] = reactToId;
2180 json["type"] = "text/plain";
2181 pimpl_->sendMessage(conversationId, std::move(json));
2182}
2183
2184void
2187 const std::string& reason)
2188{
2189 auto finalUri = uri.substr(0, uri.find("@ring.dht"));
2190 finalUri = finalUri.substr(0, uri.find("@jami.dht"));
2192 if (!convId.empty()) {
2193 Json::Value value;
2194 value["to"] = finalUri;
2195 value["type"] = "application/call-history+json";
2196 value["duration"] = std::to_string(duration_ms);
2197 if (!reason.empty())
2198 value["reason"] = reason;
2199 sendMessage(convId, std::move(value));
2200 }
2201}
2202
2203bool
2205 const std::string& conversationId,
2206 const std::string& interactionId)
2207{
2208 if (auto conv = pimpl_->getConversation(conversationId)) {
2209 std::unique_lock lk(conv->mtx);
2210 if (auto conversation = conv->conversation) {
2211 lk.unlock();
2212 return conversation->setMessageDisplayed(peer, interactionId);
2213 }
2214 }
2215 return false;
2216}
2217
2218std::map<std::string, std::map<std::string, std::map<std::string, std::string>>>
2220{
2221 std::map<std::string, std::map<std::string, std::map<std::string, std::string>>> messageStatus;
2222 for (const auto& conv : pimpl_->getConversations()) {
2223 auto d = conv->messageStatus();
2224 if (!d.empty())
2225 messageStatus[conv->id()] = std::move(d);
2226 }
2227 return messageStatus;
2228}
2229
2231ConversationModule::loadConversationMessages(const std::string& conversationId,
2232 const std::string& fromMessage,
2233 size_t n)
2234{
2235 auto acc = pimpl_->account_.lock();
2236 if (auto conv = pimpl_->getConversation(conversationId)) {
2237 std::lock_guard lk(conv->mtx);
2238 if (conv->conversation) {
2239 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2242 options.nbOfCommits = n;
2243 conv->conversation->loadMessages(
2244 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2246 accountId,
2247 conversationId,
2248 messages);
2249 },
2250 options);
2251 return id;
2252 }
2253 }
2254 return 0;
2255}
2256
2257void
2258ConversationModule::clearCache(const std::string& conversationId)
2259{
2260 if (auto conv = pimpl_->getConversation(conversationId)) {
2261 std::lock_guard lk(conv->mtx);
2262 if (conv->conversation) {
2263 conv->conversation->clearCache();
2264 }
2265 }
2266}
2267
2269ConversationModule::loadConversation(const std::string& conversationId,
2270 const std::string& fromMessage,
2271 size_t n)
2272{
2273 auto acc = pimpl_->account_.lock();
2274 if (auto conv = pimpl_->getConversation(conversationId)) {
2275 std::lock_guard lk(conv->mtx);
2276 if (conv->conversation) {
2277 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2280 options.nbOfCommits = n;
2281 conv->conversation->loadMessages2(
2282 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2284 accountId,
2285 conversationId,
2286 messages);
2287 },
2288 options);
2289 return id;
2290 }
2291 }
2292 return 0;
2293}
2294
2296ConversationModule::loadConversationUntil(const std::string& conversationId,
2297 const std::string& fromMessage,
2298 const std::string& toMessage)
2299{
2300 auto acc = pimpl_->account_.lock();
2301 if (auto conv = pimpl_->getConversation(conversationId)) {
2302 std::lock_guard lk(conv->mtx);
2303 if (conv->conversation) {
2304 const uint32_t id = std::uniform_int_distribution<uint32_t> {1}(acc->rand);
2307 options.to = toMessage;
2308 options.includeTo = true;
2309 conv->conversation->loadMessages(
2310 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2312 accountId,
2313 conversationId,
2314 messages);
2315 },
2316 options);
2317 return id;
2318 }
2319 }
2320 return 0;
2321}
2322
2324ConversationModule::loadSwarmUntil(const std::string& conversationId,
2325 const std::string& fromMessage,
2326 const std::string& toMessage)
2327{
2328 auto acc = pimpl_->account_.lock();
2329 if (auto conv = pimpl_->getConversation(conversationId)) {
2330 std::lock_guard lk(conv->mtx);
2331 if (conv->conversation) {
2332 const uint32_t id = std::uniform_int_distribution<uint32_t> {}(acc->rand);
2335 options.to = toMessage;
2336 options.includeTo = true;
2337 conv->conversation->loadMessages2(
2338 [accountId = pimpl_->accountId_, conversationId, id](auto&& messages) {
2340 accountId,
2341 conversationId,
2342 messages);
2343 },
2344 options);
2345 return id;
2346 }
2347 }
2348 return 0;
2349}
2350
2351std::shared_ptr<TransferManager>
2352ConversationModule::dataTransfer(const std::string& conversationId) const
2353{
2354 return pimpl_->withConversation(conversationId,
2355 [](auto& conversation) { return conversation.dataTransfer(); });
2356}
2357
2358bool
2359ConversationModule::onFileChannelRequest(const std::string& conversationId,
2360 const std::string& member,
2361 const std::string& fileId,
2362 bool verifyShaSum) const
2363{
2364 if (auto conv = pimpl_->getConversation(conversationId)) {
2365 std::filesystem::path path;
2366 std::string sha3sum;
2367 std::unique_lock lk(conv->mtx);
2368 if (!conv->conversation)
2369 return false;
2370 if (!conv->conversation->onFileChannelRequest(member, fileId, path, sha3sum))
2371 return false;
2372
2373 // Release the lock here to prevent the sha3 calculation from blocking other threads.
2374 lk.unlock();
2375 if (!std::filesystem::is_regular_file(path)) {
2376 JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for non existing file {}",
2377 pimpl_->accountId_,
2378 conversationId,
2379 member,
2380 fileId);
2381 return false;
2382 }
2383 // Check that our file is correct before sending
2384 if (verifyShaSum && sha3sum != fileutils::sha3File(path)) {
2385 JAMI_WARNING("[Account {:s}] [Conversation {}] {:s} asked for file {:s}, but our version is not "
2386 "complete or corrupted",
2387 pimpl_->accountId_,
2388 conversationId,
2389 member,
2390 fileId);
2391 return false;
2392 }
2393 return true;
2394 }
2395 return false;
2396}
2397
2398bool
2399ConversationModule::downloadFile(const std::string& conversationId,
2400 const std::string& interactionId,
2401 const std::string& fileId,
2402 const std::string& path)
2403{
2404 if (auto conv = pimpl_->getConversation(conversationId)) {
2405 std::lock_guard lk(conv->mtx);
2406 if (conv->conversation)
2407 return conv->conversation->downloadFile(interactionId, fileId, path, "", "");
2408 }
2409 return false;
2410}
2411
2412void
2413ConversationModule::syncConversations(const std::string& peer, const std::string& deviceId)
2414{
2415 // Sync conversations where peer is member
2416 std::set<std::string> toFetch;
2417 std::set<std::string> toClone;
2418 for (const auto& conv : pimpl_->getSyncedConversations()) {
2419 std::lock_guard lk(conv->mtx);
2420 if (conv->conversation) {
2421 if (!conv->conversation->isRemoving() && conv->conversation->isMember(peer, false)) {
2422 toFetch.emplace(conv->info.id);
2423 }
2424 } else if (!conv->info.isRemoved()
2425 && std::find(conv->info.members.begin(), conv->info.members.end(), peer)
2426 != conv->info.members.end()) {
2427 // In this case the conversation was never cloned (can be after an import)
2428 toClone.emplace(conv->info.id);
2429 }
2430 }
2431 for (const auto& cid : toFetch)
2432 pimpl_->fetchNewCommits(peer, deviceId, cid);
2433 for (const auto& cid : toClone)
2434 pimpl_->cloneConversation(deviceId, peer, cid);
2435 if (pimpl_->syncCnt.load() == 0)
2437}
2438
2439void
2441 const std::string& peerId,
2442 const std::string& deviceId)
2443{
2444 std::vector<std::string> toClone;
2445 for (const auto& [key, convInfo] : msg.c) {
2446 const auto& convId = convInfo.id;
2447 {
2448 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2449 pimpl_->rmConversationRequest(convId);
2450 }
2451
2452 auto conv = pimpl_->startConversation(convInfo);
2453 std::unique_lock lk(conv->mtx);
2454 // Skip outdated info
2455 if (std::max(convInfo.created, convInfo.removed)
2456 < std::max(conv->info.created, conv->info.removed))
2457 continue;
2458 if (not convInfo.isRemoved()) {
2459 // If multi devices, it can detect a conversation that was already
2460 // removed, so just check if the convinfo contains a removed conv
2461 if (conv->info.removed) {
2462 if (conv->info.removed >= convInfo.created) {
2463 // Only reclone if re-added, else the peer is not synced yet (could be
2464 // offline before)
2465 continue;
2466 }
2467 JAMI_DEBUG("Re-add previously removed conversation {:s}", convId);
2468 }
2469 conv->info = convInfo;
2470 if (!conv->conversation) {
2471 if (deviceId != "") {
2472 pimpl_->cloneConversation(deviceId, peerId, conv);
2473 } else {
2474 // In this case, information is from JAMS
2475 // JAMS does not store the conversation itself, so we
2476 // must use information to clone the conversation
2478 toClone.emplace_back(convId);
2479 }
2480 }
2481 } else {
2482 if (conv->conversation && !conv->conversation->isRemoving()) {
2484 convId);
2485 conv->conversation->setRemovingFlag();
2486 }
2487 auto update = false;
2488 if (!conv->info.removed) {
2489 update = true;
2490 conv->info.removed = std::time(nullptr);
2491 }
2492 if (convInfo.erased && !conv->info.erased) {
2493 conv->info.erased = std::time(nullptr);
2494 pimpl_->addConvInfo(conv->info);
2495 pimpl_->removeRepositoryImpl(*conv, false);
2496 } else if (update) {
2497 pimpl_->addConvInfo(conv->info);
2498 }
2499 }
2500 }
2501
2502 for (const auto& cid : toClone) {
2503 auto members = getConversationMembers(cid);
2504 for (const auto& member : members) {
2505 if (member.at("uri") != pimpl_->username_)
2506 cloneConversationFrom(cid, member.at("uri"));
2507 }
2508 }
2509
2510 for (const auto& [convId, req] : msg.cr) {
2511 if (req.from == pimpl_->username_) {
2512 JAMI_WARNING("Detected request from ourself, ignore {}.", convId);
2513 continue;
2514 }
2515 std::unique_lock lk(pimpl_->conversationsRequestsMtx_);
2516 if (pimpl_->isConversation(convId)) {
2517 // Already handled request
2518 pimpl_->rmConversationRequest(convId);
2519 continue;
2520 }
2521
2522 // New request
2523 if (!pimpl_->addConversationRequest(convId, req))
2524 continue;
2525 lk.unlock();
2526
2527 if (req.declined != 0) {
2528 // Request declined
2529 JAMI_LOG("[Account {:s}] Declined request detected for conversation {:s} (device {:s})",
2530 pimpl_->accountId_,
2531 convId,
2532 deviceId);
2533 pimpl_->syncingMetadatas_.erase(convId);
2534 pimpl_->saveMetadata();
2536 convId);
2537 continue;
2538 }
2539
2540 JAMI_LOG("[Account {:s}] New request detected for conversation {:s} (device {:s})",
2541 pimpl_->accountId_,
2542 convId,
2543 deviceId);
2544
2546 convId,
2547 req.toMap());
2548 }
2549
2550 // Updates preferences for conversations
2551 for (const auto& [convId, p] : msg.p) {
2552 if (auto conv = pimpl_->getConversation(convId)) {
2553 std::unique_lock lk(conv->mtx);
2554 if (conv->conversation) {
2555 auto conversation = conv->conversation;
2556 lk.unlock();
2557 conversation->updatePreferences(p);
2558 } else if (conv->pending) {
2559 conv->pending->preferences = p;
2560 }
2561 }
2562 }
2563
2564 // Updates displayed for conversations
2565 for (const auto& [convId, ms] : msg.ms) {
2566 if (auto conv = pimpl_->getConversation(convId)) {
2567 std::unique_lock lk(conv->mtx);
2568 if (conv->conversation) {
2569 auto conversation = conv->conversation;
2570 lk.unlock();
2571 conversation->updateMessageStatus(ms);
2572 } else if (conv->pending) {
2573 conv->pending->status = ms;
2574 }
2575 }
2576 }
2577}
2578
2579bool
2580ConversationModule::needsSyncingWith(const std::string& memberUri, const std::string& deviceId) const
2581{
2582 // Check if a conversation needs to fetch remote or to be cloned
2583 std::lock_guard lk(pimpl_->conversationsMtx_);
2584 for (const auto& [key, ci] : pimpl_->conversations_) {
2585 std::lock_guard lk(ci->mtx);
2586 if (ci->conversation) {
2587 if (ci->conversation->isRemoving() && ci->conversation->isMember(memberUri, false))
2588 return true;
2589 } else if (!ci->info.removed
2590 && std::find(ci->info.members.begin(), ci->info.members.end(), memberUri)
2591 != ci->info.members.end()) {
2592 // In this case the conversation was never cloned (can be after an import)
2593 return true;
2594 }
2595 }
2596 return false;
2597}
2598
2599void
2600ConversationModule::setFetched(const std::string& conversationId,
2601 const std::string& deviceId,
2602 const std::string& commitId)
2603{
2604 if (auto conv = pimpl_->getConversation(conversationId)) {
2605 std::lock_guard lk(conv->mtx);
2606 if (conv->conversation) {
2607 bool remove = conv->conversation->isRemoving();
2608 conv->conversation->hasFetched(deviceId, commitId);
2609 if (remove)
2610 pimpl_->removeRepositoryImpl(*conv, true);
2611 }
2612 }
2613}
2614
2615void
2617 const std::string& deviceId,
2618 const std::string& conversationId,
2619 const std::string& commitId)
2620{
2621 pimpl_->fetchNewCommits(peer, deviceId, conversationId, commitId);
2622}
2623
2624void
2625ConversationModule::addConversationMember(const std::string& conversationId,
2626 const dht::InfoHash& contactUri,
2627 bool sendRequest)
2628{
2629 auto conv = pimpl_->getConversation(conversationId);
2630 if (not conv || not conv->conversation) {
2631 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2632 return;
2633 }
2634 std::unique_lock lk(conv->mtx);
2635
2636 auto contactUriStr = contactUri.toString();
2637 if (conv->conversation->isMember(contactUriStr, true)) {
2638 JAMI_DEBUG("{:s} is already a member of {:s}, resend invite", contactUriStr, conversationId);
2639 // Note: This should not be necessary, but if for whatever reason the other side didn't
2640 // join we should not forbid new invites
2641 auto invite = conv->conversation->generateInvitation();
2642 lk.unlock();
2643 pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
2644 return;
2645 }
2646
2647 conv->conversation->addMember(
2649 [this, conv, conversationId, sendRequest, contactUriStr](bool ok,
2650 const std::string& commitId) {
2651 if (ok) {
2652 std::unique_lock lk(conv->mtx);
2653 pimpl_->sendMessageNotification(*conv->conversation,
2654 true,
2655 commitId); // For the other members
2656 if (sendRequest) {
2657 auto invite = conv->conversation->generateInvitation();
2658 lk.unlock();
2659 pimpl_->sendMsgCb_(contactUriStr, {}, std::move(invite), 0);
2660 }
2661 }
2662 });
2663}
2664
2665void
2666ConversationModule::removeConversationMember(const std::string& conversationId,
2667 const dht::InfoHash& contactUri,
2668 bool isDevice)
2669{
2670 auto contactUriStr = contactUri.toString();
2671 if (auto conv = pimpl_->getConversation(conversationId)) {
2672 std::lock_guard lk(conv->mtx);
2673 if (conv->conversation)
2674 return conv->conversation
2675 ->removeMember(contactUriStr,
2676 isDevice,
2677 [this, conversationId](bool ok, const std::string& commitId) {
2678 if (ok) {
2679 pimpl_->sendMessageNotification(conversationId,
2680 true,
2681 commitId);
2682 }
2683 });
2684 }
2685}
2686
2687std::vector<std::map<std::string, std::string>>
2688ConversationModule::getConversationMembers(const std::string& conversationId,
2689 bool includeBanned) const
2690{
2691 return pimpl_->getConversationMembers(conversationId, includeBanned);
2692}
2693
2696 const std::string& toId,
2697 const std::string& fromId,
2698 const std::string& authorUri) const
2699{
2700 if (auto conv = pimpl_->getConversation(convId)) {
2701 std::lock_guard lk(conv->mtx);
2702 if (conv->conversation)
2703 return conv->conversation->countInteractions(toId, fromId, authorUri);
2704 }
2705 return 0;
2706}
2707
2708void
2709ConversationModule::search(uint32_t req, const std::string& convId, const Filter& filter) const
2710{
2711 if (convId.empty()) {
2712 auto convs = pimpl_->getConversations();
2713 if (convs.empty()) {
2715 req,
2716 pimpl_->accountId_,
2717 std::string {},
2718 std::vector<std::map<std::string, std::string>> {});
2719 return;
2720 }
2721 auto finishedFlag = std::make_shared<std::atomic_int>(convs.size());
2722 for (const auto& conv : convs) {
2723 conv->search(req, filter, finishedFlag);
2724 }
2725 } else if (auto conv = pimpl_->getConversation(convId)) {
2726 std::lock_guard lk(conv->mtx);
2727 if (conv->conversation)
2728 conv->conversation->search(req, filter, std::make_shared<std::atomic_int>(1));
2729 }
2730}
2731
2732void
2733ConversationModule::updateConversationInfos(const std::string& conversationId,
2734 const std::map<std::string, std::string>& infos,
2735 bool sync)
2736{
2737 auto conv = pimpl_->getConversation(conversationId);
2738 if (not conv or not conv->conversation) {
2739 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2740 return;
2741 }
2742 std::lock_guard lk(conv->mtx);
2743 conv->conversation
2744 ->updateInfos(infos, [this, conversationId, sync](bool ok, const std::string& commitId) {
2745 if (ok && sync) {
2746 pimpl_->sendMessageNotification(conversationId, true, commitId);
2747 } else if (sync)
2748 JAMI_WARNING("Unable to update info on {:s}", conversationId);
2749 });
2750}
2751
2752std::map<std::string, std::string>
2753ConversationModule::conversationInfos(const std::string& conversationId) const
2754{
2755 {
2756 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2757 auto itReq = pimpl_->conversationsRequests_.find(conversationId);
2758 if (itReq != pimpl_->conversationsRequests_.end())
2759 return itReq->second.metadatas;
2760 }
2761 if (auto conv = pimpl_->getConversation(conversationId)) {
2762 std::lock_guard lk(conv->mtx);
2763 std::map<std::string, std::string> md;
2764 {
2765 auto syncingMetadatasIt = pimpl_->syncingMetadatas_.find(conversationId);
2766 if (syncingMetadatasIt != pimpl_->syncingMetadatas_.end()) {
2767 if (conv->conversation) {
2768 pimpl_->syncingMetadatas_.erase(syncingMetadatasIt);
2769 pimpl_->saveMetadata();
2770 } else {
2771 md = syncingMetadatasIt->second;
2772 }
2773 }
2774 }
2775 if (conv->conversation)
2776 return conv->conversation->infos();
2777 else
2778 return md;
2779 }
2780 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2781 return {};
2782}
2783
2784void
2785ConversationModule::setConversationPreferences(const std::string& conversationId,
2786 const std::map<std::string, std::string>& prefs)
2787{
2788 if (auto conv = pimpl_->getConversation(conversationId)) {
2789 std::unique_lock lk(conv->mtx);
2790 if (not conv->conversation) {
2791 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2792 return;
2793 }
2794 auto conversation = conv->conversation;
2795 lk.unlock();
2796 conversation->updatePreferences(prefs);
2797 auto msg = std::make_shared<SyncMsg>();
2798 msg->p = {{conversationId, conversation->preferences(true)}};
2799 pimpl_->needsSyncingCb_(std::move(msg));
2800 }
2801}
2802
2803std::map<std::string, std::string>
2804ConversationModule::getConversationPreferences(const std::string& conversationId,
2805 bool includeCreated) const
2806{
2807 if (auto conv = pimpl_->getConversation(conversationId)) {
2808 std::lock_guard lk(conv->mtx);
2809 if (conv->conversation)
2810 return conv->conversation->preferences(includeCreated);
2811 }
2812 return {};
2813}
2814
2815std::map<std::string, std::map<std::string, std::string>>
2817{
2818 std::map<std::string, std::map<std::string, std::string>> p;
2819 for (const auto& conv : pimpl_->getConversations()) {
2820 auto prefs = conv->preferences(true);
2821 if (!prefs.empty())
2822 p[conv->id()] = std::move(prefs);
2823 }
2824 return p;
2825}
2826
2827std::vector<uint8_t>
2828ConversationModule::conversationVCard(const std::string& conversationId) const
2829{
2830 if (auto conv = pimpl_->getConversation(conversationId)) {
2831 std::lock_guard lk(conv->mtx);
2832 if (conv->conversation)
2833 return conv->conversation->vCard();
2834 }
2835 JAMI_ERROR("Conversation {:s} does not exist", conversationId);
2836 return {};
2837}
2838
2839bool
2840ConversationModule::isBanned(const std::string& convId, const std::string& uri) const
2841{
2842 if (auto conv = pimpl_->getConversation(convId)) {
2843 std::lock_guard lk(conv->mtx);
2844 if (!conv->conversation)
2845 return true;
2846 if (conv->conversation->mode() != ConversationMode::ONE_TO_ONE)
2847 return conv->conversation->isBanned(uri);
2848 }
2849 // If 1:1 we check the certificate status
2850 std::lock_guard lk(pimpl_->conversationsMtx_);
2851 return pimpl_->accountManager_->getCertificateStatus(uri)
2852 == dhtnet::tls::TrustStore::PermissionStatus::BANNED;
2853}
2854
2855void
2856ConversationModule::removeContact(const std::string& uri, bool banned)
2857{
2858 // Remove linked conversation's requests
2859 {
2860 std::lock_guard lk(pimpl_->conversationsRequestsMtx_);
2861 auto update = false;
2862 for (auto it = pimpl_->conversationsRequests_.begin();
2863 it != pimpl_->conversationsRequests_.end();
2864 ++it) {
2865 if (it->second.from == uri && !it->second.declined) {
2866 JAMI_DEBUG("Declining conversation request {:s} from {:s}", it->first, uri);
2867 pimpl_->syncingMetadatas_.erase(it->first);
2868 pimpl_->saveMetadata();
2870 pimpl_->accountId_, it->first);
2871 update = true;
2872 it->second.declined = std::time(nullptr);
2873 }
2874 }
2875 if (update) {
2876 pimpl_->saveConvRequests();
2877 pimpl_->needsSyncingCb_({});
2878 }
2879 }
2880 if (banned) {
2881 auto conversationId = getOneToOneConversation(uri);
2882 pimpl_->withConversation(conversationId, [&](auto& conv) { conv.shutdownConnections(); });
2883 return; // Keep the conversation in banned model but stop connections
2884 }
2885
2886 // Removed contacts should not be linked to any conversation
2887 pimpl_->accountManager_->updateContactConversation(uri, "");
2888
2889 // Remove all one-to-one conversations with the removed contact
2890 auto isSelf = uri == pimpl_->username_;
2891 std::vector<std::string> toRm;
2892 auto removeConvInfo = [&](const auto& conv, const auto& members) {
2893 if ((isSelf && members.size() == 1)
2894 || (!isSelf && std::find(members.begin(), members.end(), uri) != members.end())) {
2895 // Mark the conversation as removed if it wasn't already
2896 if (!conv->info.isRemoved()) {
2897 conv->info.removed = std::time(nullptr);
2899 conv->info.id);
2900 pimpl_->addConvInfo(conv->info);
2901 return true;
2902 }
2903 }
2904 return false;
2905 };
2906 {
2907 std::lock_guard lk(pimpl_->conversationsMtx_);
2908 for (auto& [convId, conv] : pimpl_->conversations_) {
2909 std::lock_guard lk(conv->mtx);
2910 if (conv->conversation) {
2911 try {
2912 // Note it's important to check getUsername(), else
2913 // removing self can remove all conversations
2914 if (conv->conversation->mode() == ConversationMode::ONE_TO_ONE) {
2915 auto initMembers = conv->conversation->getInitialMembers();
2916 if (removeConvInfo(conv, initMembers))
2917 toRm.emplace_back(convId);
2918 }
2919 } catch (const std::exception& e) {
2920 JAMI_WARN("%s", e.what());
2921 }
2922 } else {
2923 removeConvInfo(conv, conv->info.members);
2924 }
2925 }
2926 }
2927 for (const auto& id : toRm)
2928 pimpl_->removeRepository(id, true, true);
2929}
2930
2931bool
2932ConversationModule::removeConversation(const std::string& conversationId)
2933{
2934 return pimpl_->removeConversation(conversationId);
2935}
2936
2937void
2938ConversationModule::initReplay(const std::string& oldConvId, const std::string& newConvId)
2939{
2940 if (auto conv = pimpl_->getConversation(oldConvId)) {
2941 std::lock_guard lk(conv->mtx);
2942 if (conv->conversation) {
2943 std::promise<bool> waitLoad;
2944 std::future<bool> fut = waitLoad.get_future();
2945 // we should wait for loadMessage, because it will be deleted after this.
2946 conv->conversation->loadMessages(
2947 [&](auto&& messages) {
2948 std::reverse(messages.begin(),
2949 messages.end()); // Log is inverted as we want to replay
2950 std::lock_guard lk(pimpl_->replayMtx_);
2951 pimpl_->replay_[newConvId] = std::move(messages);
2952 waitLoad.set_value(true);
2953 },
2954 {});
2955 fut.wait();
2956 }
2957 }
2958}
2959
2960bool
2961ConversationModule::isHosting(const std::string& conversationId, const std::string& confId) const
2962{
2963 if (conversationId.empty()) {
2964 std::lock_guard lk(pimpl_->conversationsMtx_);
2965 return std::find_if(pimpl_->conversations_.cbegin(),
2966 pimpl_->conversations_.cend(),
2967 [&](const auto& conv) {
2968 return conv.second->conversation
2969 && conv.second->conversation->isHosting(confId);
2970 })
2971 != pimpl_->conversations_.cend();
2972 } else if (auto conv = pimpl_->getConversation(conversationId)) {
2973 if (conv->conversation) {
2974 return conv->conversation->isHosting(confId);
2975 }
2976 }
2977 return false;
2978}
2979
2980std::vector<std::map<std::string, std::string>>
2981ConversationModule::getActiveCalls(const std::string& conversationId) const
2982{
2983 return pimpl_->withConversation(conversationId, [](const auto& conversation) {
2984 return conversation.currentCalls();
2985 });
2986}
2987
2988std::shared_ptr<SIPCall>
2990 const std::string& url,
2991 const std::vector<libjami::MediaMap>& mediaList,
2992 std::function<void(const std::string&, const DeviceId&, const std::shared_ptr<SIPCall>&)>&& cb)
2993{
2994 std::string conversationId = "", confId = "", uri = "", deviceId = "";
2995 if (url.find('/') == std::string::npos) {
2996 conversationId = url;
2997 } else {
2998 auto parameters = jami::split_string(url, '/');
2999 if (parameters.size() != 4) {
3000 JAMI_ERROR("Incorrect url {:s}", url);
3001 return {};
3002 }
3003 conversationId = parameters[0];
3004 uri = parameters[1];
3005 deviceId = parameters[2];
3006 confId = parameters[3];
3007 }
3008
3009 auto conv = pimpl_->getConversation(conversationId);
3010 if (!conv)
3011 return {};
3012 std::unique_lock lk(conv->mtx);
3013 if (!conv->conversation) {
3014 JAMI_ERROR("Conversation {:s} not found", conversationId);
3015 return {};
3016 }
3017
3018 // Check if we want to join a specific conference
3019 // So, if confId is specified or if there is some activeCalls
3020 // or if we are the default host.
3021 auto activeCalls = conv->conversation->currentCalls();
3022 auto infos = conv->conversation->infos();
3023 auto itRdvAccount = infos.find("rdvAccount");
3024 auto itRdvDevice = infos.find("rdvDevice");
3025 auto sendCallRequest = false;
3026 if (!confId.empty()) {
3027 sendCallRequest = true;
3028 JAMI_DEBUG("Calling self, join conference");
3029 } else if (!activeCalls.empty()) {
3030 // Else, we try to join active calls
3031 sendCallRequest = true;
3032 auto& ac = *activeCalls.rbegin();
3033 confId = ac.at("id");
3034 uri = ac.at("uri");
3035 deviceId = ac.at("device");
3036 } else if (itRdvAccount != infos.end() && itRdvDevice != infos.end()
3037 && !itRdvAccount->second.empty()) {
3038 // Else, creates "to" (accountId/deviceId/conversationId/confId) and ask remote host
3039 sendCallRequest = true;
3040 uri = itRdvAccount->second;
3041 deviceId = itRdvDevice->second;
3042 confId = "0";
3043 JAMI_DEBUG("Remote host detected. Calling {:s} on device {:s}", uri, deviceId);
3044 }
3045 lk.unlock();
3046
3047 auto account = pimpl_->account_.lock();
3048 std::vector<libjami::MediaMap> mediaMap
3050 pimpl_->account_.lock()->createDefaultMediaList(
3051 pimpl_->account_.lock()->isVideoEnabled()))
3052 : mediaList;
3053
3054 if (!sendCallRequest || (uri == pimpl_->username_ && deviceId == pimpl_->deviceId_)) {
3056 // TODO attach host with media list
3057 hostConference(conversationId, confId, "", mediaMap);
3058 return {};
3059 }
3060
3061 // Else we need to create a call
3062 auto& manager = Manager::instance();
3063 std::shared_ptr<SIPCall> call = manager.callFactory.newSipCall(account,
3065 mediaMap);
3066
3067 if (not call)
3068 return {};
3069
3070 auto callUri = fmt::format("{}/{}/{}/{}", conversationId, uri, deviceId, confId);
3071 account->getIceOptions([call,
3072 accountId = account->getAccountID(),
3073 callUri,
3074 uri = std::move(uri),
3075 conversationId,
3076 deviceId,
3077 cb = std::move(cb)](auto&& opts) {
3078 if (call->isIceEnabled()) {
3079 if (not call->createIceMediaTransport(false)
3080 or not call->initIceMediaTransport(true,
3081 std::forward<dhtnet::IceTransportOptions>(opts))) {
3082 return;
3083 }
3084 }
3085 JAMI_DEBUG("New outgoing call with {}", uri);
3086 call->setPeerNumber(uri);
3087 call->setPeerUri("swarm:" + uri);
3088
3089 JAMI_DEBUG("Calling: {:s}", callUri);
3091 call->setPeerNumber(callUri);
3092 call->setPeerUri("rdv:" + callUri);
3093 call->addStateListener([accountId, conversationId](Call::CallState call_state,
3095 int) {
3098 emitSignal<libjami::ConfigurationSignal::NeedsHost>(accountId, conversationId);
3099 return true;
3100 }
3101 return true;
3102 });
3103 cb(callUri, DeviceId(deviceId), call);
3104 });
3105
3106 return call;
3107}
3108
3109void
3110ConversationModule::hostConference(const std::string& conversationId,
3111 const std::string& confId,
3112 const std::string& callId,
3113 const std::vector<libjami::MediaMap>& mediaList)
3114{
3115 auto acc = pimpl_->account_.lock();
3116 if (!acc)
3117 return;
3118 auto conf = acc->getConference(confId);
3119 auto createConf = !conf;
3120 std::shared_ptr<SIPCall> call;
3121 if (!callId.empty()) {
3122 call = std::dynamic_pointer_cast<SIPCall>(acc->getCall(callId));
3123 if (!call) {
3124 JAMI_WARNING("No call with id {} found", callId);
3125 return;
3126 }
3127 }
3128 if (createConf) {
3129 conf = std::make_shared<Conference>(acc, confId);
3130 acc->attach(conf);
3131 }
3132
3133 if (!callId.empty())
3134 conf->addSubCall(callId);
3135
3136 if (callId.empty())
3137 conf->attachHost(mediaList);
3138
3139 if (createConf) {
3140 emitSignal<libjami::CallSignal::ConferenceCreated>(acc->getAccountID(),
3141 conversationId,
3142 conf->getConfId());
3143 } else {
3144 conf->reportMediaNegotiationStatus();
3145 emitSignal<libjami::CallSignal::ConferenceChanged>(acc->getAccountID(),
3146 conf->getConfId(),
3147 conf->getStateStr());
3148 return;
3149 }
3150
3151 auto conv = pimpl_->getConversation(conversationId);
3152 if (!conv)
3153 return;
3154 std::unique_lock lk(conv->mtx);
3155 if (!conv->conversation) {
3156 JAMI_ERROR("Conversation {} not found", conversationId);
3157 return;
3158 }
3159 // Add commit to conversation
3160 Json::Value value;
3161 value["uri"] = pimpl_->username_;
3162 value["device"] = pimpl_->deviceId_;
3163 value["confId"] = conf->getConfId();
3164 value["type"] = "application/call-history+json";
3165 conv->conversation->hostConference(std::move(value),
3166 [w = pimpl_->weak(),
3167 conversationId](bool ok, const std::string& commitId) {
3168 if (ok) {
3169 if (auto shared = w.lock())
3170 shared->sendMessageNotification(conversationId,
3171 true,
3172 commitId);
3173 } else {
3174 JAMI_ERR("Failed to send message to conversation %s",
3175 conversationId.c_str());
3176 }
3177 });
3178
3179 // When conf finished = remove host & commit
3180 // Master call, so when it's stopped, the conference will be stopped (as we use the hold
3181 // state for detaching the call)
3182 conf->onShutdown([w = pimpl_->weak(),
3183 accountUri = pimpl_->username_,
3184 confId = conf->getConfId(),
3185 conversationId,
3186 conv](int duration) {
3187 auto shared = w.lock();
3188 if (shared) {
3189 Json::Value value;
3190 value["uri"] = accountUri;
3191 value["device"] = shared->deviceId_;
3192 value["confId"] = confId;
3193 value["type"] = "application/call-history+json";
3194 value["duration"] = std::to_string(duration);
3195
3196 std::lock_guard lk(conv->mtx);
3197 if (!conv->conversation) {
3198 JAMI_ERROR("Conversation {} not found", conversationId);
3199 return;
3200 }
3201 conv->conversation->removeActiveConference(
3202 std::move(value), [w, conversationId](bool ok, const std::string& commitId) {
3203 if (ok) {
3204 if (auto shared = w.lock()) {
3205 shared->sendMessageNotification(conversationId, true, commitId);
3206 }
3207 } else {
3208 JAMI_ERROR("Failed to send message to conversation {}", conversationId);
3209 }
3210 });
3211 }
3212 });
3213}
3214
3215std::map<std::string, ConvInfo>
3216ConversationModule::convInfos(const std::string& accountId)
3217{
3218 return convInfosFromPath(fileutils::get_data_dir() / accountId);
3219}
3220
3221std::map<std::string, ConvInfo>
3222ConversationModule::convInfosFromPath(const std::filesystem::path& path)
3223{
3224 std::map<std::string, ConvInfo> convInfos;
3225 try {
3226 // read file
3227 std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convInfo"));
3228 auto file = fileutils::loadFile("convInfo", path);
3229 // load values
3230 msgpack::unpacked result;
3231 msgpack::unpack(result, (const char*) file.data(), file.size());
3232 result.get().convert(convInfos);
3233 } catch (const std::exception& e) {
3234 JAMI_WARN("[convInfo] error loading convInfo: %s", e.what());
3235 }
3236 return convInfos;
3237}
3238
3239std::map<std::string, ConversationRequest>
3240ConversationModule::convRequests(const std::string& accountId)
3241{
3242 auto path = fileutils::get_data_dir() / accountId;
3243 return convRequestsFromPath(path.string());
3244}
3245
3246std::map<std::string, ConversationRequest>
3247ConversationModule::convRequestsFromPath(const std::filesystem::path& path)
3248{
3249 std::map<std::string, ConversationRequest> convRequests;
3250 try {
3251 // read file
3252 std::lock_guard lock(dhtnet::fileutils::getFileLock(path / "convRequests"));
3253 auto file = fileutils::loadFile("convRequests", path);
3254 // load values
3255 msgpack::unpacked result;
3256 msgpack::unpack(result, (const char*) file.data(), file.size(), 0);
3257 result.get().convert(convRequests);
3258 } catch (const std::exception& e) {
3259 JAMI_WARN("[convInfo] error loading convInfo: %s", e.what());
3260 }
3261 return convRequests;
3262}
3263
3264void
3265ConversationModule::addConvInfo(const ConvInfo& info)
3266{
3267 pimpl_->addConvInfo(info);
3268}
3269
3270void
3271ConversationModule::Impl::setConversationMembers(const std::string& convId,
3272 const std::set<std::string>& members)
3273{
3274 if (auto conv = getConversation(convId)) {
3275 std::lock_guard lk(conv->mtx);
3276 conv->info.members = members;
3277 addConvInfo(conv->info);
3278 }
3279}
3280
3281std::shared_ptr<Conversation>
3282ConversationModule::getConversation(const std::string& convId)
3283{
3284 if (auto conv = pimpl_->getConversation(convId)) {
3285 std::lock_guard lk(conv->mtx);
3286 return conv->conversation;
3287 }
3288 return nullptr;
3289}
3290
3291std::shared_ptr<dhtnet::ChannelSocket>
3292ConversationModule::gitSocket(std::string_view deviceId, std::string_view convId) const
3293{
3294 if (auto conv = pimpl_->getConversation(convId)) {
3295 std::lock_guard lk(conv->mtx);
3296 if (conv->conversation)
3297 return conv->conversation->gitSocket(DeviceId(deviceId));
3298 else if (conv->pending)
3299 return conv->pending->socket;
3300 }
3301 return nullptr;
3302}
3303
3304void
3305ConversationModule::addGitSocket(std::string_view deviceId,
3306 std::string_view convId,
3307 const std::shared_ptr<dhtnet::ChannelSocket>& channel)
3308{
3309 if (auto conv = pimpl_->getConversation(convId)) {
3310 std::lock_guard lk(conv->mtx);
3311 conv->conversation->addGitSocket(DeviceId(deviceId), channel);
3312 } else
3313 JAMI_WARNING("addGitSocket: Unable to find conversation {:s}", convId);
3314}
3315
3316void
3317ConversationModule::removeGitSocket(std::string_view deviceId, std::string_view convId)
3318{
3319 pimpl_->withConversation(convId, [&](auto& conv) { conv.removeGitSocket(DeviceId(deviceId)); });
3320}
3321
3322void
3323ConversationModule::shutdownConnections()
3324{
3325 for (const auto& c : pimpl_->getSyncedConversations()) {
3326 std::lock_guard lkc(c->mtx);
3327 if (c->conversation)
3328 c->conversation->shutdownConnections();
3329 if (c->pending)
3330 c->pending->socket = {};
3331 }
3332}
3333void
3334ConversationModule::addSwarmChannel(const std::string& conversationId,
3335 std::shared_ptr<dhtnet::ChannelSocket> channel)
3336{
3337 pimpl_->withConversation(conversationId,
3338 [&](auto& conv) { conv.addSwarmChannel(std::move(channel)); });
3339}
3340
3341void
3342ConversationModule::connectivityChanged()
3343{
3344 for (const auto& conv : pimpl_->getConversations())
3345 conv->connectivityChanged();
3346}
3347
3348std::shared_ptr<Typers>
3349ConversationModule::getTypers(const std::string& convId)
3350{
3351 if (auto c = pimpl_->getConversation(convId)) {
3352 std::lock_guard lk(c->mtx);
3353 if (c->conversation)
3354 return c->conversation->typers();
3355 }
3356 return nullptr;
3357}
3358
3359} // 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