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