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