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