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