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