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