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