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