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