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