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