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