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 : #ifdef HAVE_CONFIG_H
19 : #include "config.h"
20 : #endif
21 :
22 : #include "jamiaccount.h"
23 : #include "presence_manager.h"
24 :
25 : #include "logger.h"
26 :
27 : #include "accountarchive.h"
28 : #include "jami_contact.h"
29 : #include "configkeys.h"
30 : #include "contact_list.h"
31 : #include "archive_account_manager.h"
32 : #include "server_account_manager.h"
33 : #include "jamidht/commit_message.h"
34 : #include "jamidht/channeled_transport.h"
35 : #include "jamidht/collaborative_editing.h"
36 : #include "conversation_channel_handler.h"
37 : #include "sync_channel_handler.h"
38 : #include "ydoc_channel_handler.h"
39 : #include "message_channel_handler.h"
40 : #include "auth_channel_handler.h"
41 : #include "transfer_channel_handler.h"
42 : #include "swarm/swarm_channel_handler.h"
43 : #include "service_manager.h"
44 : #include "svc_discovery_channel_handler.h"
45 : #include "svc_tunnel_channel_handler.h"
46 : #include "jami/media_const.h"
47 :
48 : #include "sip/sdp.h"
49 : #include "sip/sipvoiplink.h"
50 : #include "sip/sipcall.h"
51 : #include "sip/siptransport.h"
52 : #include "connectivity/sip_utils.h"
53 :
54 : #include "uri.h"
55 :
56 : #include "client/jami_signal.h"
57 : #include "jami/call_const.h"
58 : #include "jami/account_const.h"
59 :
60 : #include "system_codec_container.h"
61 :
62 : #include "account_schema.h"
63 : #include "manager.h"
64 : #include "connectivity/utf8_utils.h"
65 : #include "connectivity/ip_utils.h"
66 :
67 : #ifdef ENABLE_PLUGIN
68 : #include "plugin/jamipluginmanager.h"
69 : #include "plugin/chatservicesmanager.h"
70 : #endif
71 :
72 : #ifdef ENABLE_VIDEO
73 : #include "libav_utils.h"
74 : #endif
75 : #include "fileutils.h"
76 : #include "string_utils.h"
77 : #include "archiver.h"
78 : #include "data_transfer.h"
79 : #include "json_utils.h"
80 :
81 : #include "libdevcrypto/Common.h"
82 : #include "base64.h"
83 : #include "vcard.h"
84 : #include "im/instant_messaging.h"
85 :
86 : #include <dhtnet/ice_transport.h>
87 : #include <dhtnet/ice_transport_factory.h>
88 : #include <dhtnet/upnp/upnp_control.h>
89 : #include <dhtnet/multiplexed_socket.h>
90 : #include <dhtnet/certstore.h>
91 :
92 : #include <opendht/thread_pool.h>
93 : #include <opendht/peer_discovery.h>
94 : #include <opendht/http.h>
95 :
96 : #include <yaml-cpp/yaml.h>
97 : #include <fmt/format.h>
98 :
99 : #include <unistd.h>
100 :
101 : #include <algorithm>
102 : #include <array>
103 : #include <cctype>
104 : #include <charconv>
105 : #include <cinttypes>
106 : #include <cstdarg>
107 : #include <fstream>
108 : #include <initializer_list>
109 : #include <memory>
110 : #include <regex>
111 : #include <sstream>
112 : #include <string>
113 : #include <system_error>
114 : #include <utility>
115 :
116 : using namespace std::placeholders;
117 :
118 : namespace jami {
119 :
120 : constexpr pj_str_t STR_MESSAGE_ID = jami::sip_utils::CONST_PJ_STR("Message-ID");
121 : static constexpr const char MIME_TYPE_IMDN[] {"message/imdn+xml"};
122 : static constexpr const char MIME_TYPE_PIDF[] {"application/pidf+xml"};
123 : static constexpr const char MIME_TYPE_INVITE_JSON[] {"application/invite+json"};
124 : static constexpr const char DEVICE_ID_PATH[] {"ring_device"};
125 : static constexpr auto TREATED_PATH = "treatedImMessages"sv;
126 :
127 : struct VCardMessageCtx
128 : {
129 : std::shared_ptr<std::atomic_int> success;
130 : int total;
131 : std::string path;
132 : };
133 :
134 : namespace Migration {
135 :
136 : enum class State { // Contains all the Migration states
137 : SUCCESS,
138 : INVALID
139 : };
140 :
141 : std::string
142 4 : mapStateNumberToString(const State migrationState)
143 : {
144 : #define CASE_STATE(X) \
145 : case Migration::State::X: \
146 : return #X
147 :
148 4 : switch (migrationState) {
149 0 : CASE_STATE(INVALID);
150 12 : CASE_STATE(SUCCESS);
151 : }
152 0 : return {};
153 : }
154 :
155 : void
156 4 : setState(const std::string& accountID, const State migrationState)
157 : {
158 4 : emitSignal<libjami::ConfigurationSignal::MigrationEnded>(accountID, mapStateNumberToString(migrationState));
159 4 : }
160 :
161 : } // namespace Migration
162 :
163 : struct JamiAccount::PendingCall
164 : {
165 : std::chrono::steady_clock::time_point start;
166 : std::shared_ptr<IceTransport> ice_sp;
167 : std::shared_ptr<IceTransport> ice_tcp_sp;
168 : std::weak_ptr<SIPCall> call;
169 : std::future<size_t> listen_key;
170 : dht::InfoHash call_key;
171 : dht::InfoHash from;
172 : dht::InfoHash from_account;
173 : std::shared_ptr<dht::crypto::Certificate> from_cert;
174 : };
175 :
176 : struct JamiAccount::PendingMessage
177 : {
178 : std::set<DeviceId> to;
179 : };
180 :
181 : struct AccountPeerInfo
182 : {
183 : dht::InfoHash accountId;
184 : std::string displayName;
185 0 : MSGPACK_DEFINE(accountId, displayName)
186 : };
187 :
188 : struct JamiAccount::DiscoveredPeer
189 : {
190 : std::string displayName;
191 : std::unique_ptr<asio::steady_timer> cleanupTimer;
192 : };
193 :
194 : /**
195 : * Track sending state for a single message to one or more devices.
196 : */
197 : class JamiAccount::SendMessageContext
198 : {
199 : public:
200 : using OnComplete = std::function<void(bool, bool)>;
201 14314 : SendMessageContext(OnComplete onComplete)
202 14314 : : onComplete(std::move(onComplete))
203 14314 : {}
204 : /** Track new pending message for device */
205 13358 : bool add(const DeviceId& device)
206 : {
207 13358 : std::lock_guard lk(mtx);
208 26715 : return devices.insert(device).second;
209 13358 : }
210 : /** Call after all messages are sent */
211 14314 : void start()
212 : {
213 14314 : std::unique_lock lk(mtx);
214 14314 : started = true;
215 14314 : checkComplete(lk);
216 14314 : }
217 : /** Complete pending message for device */
218 12403 : bool complete(const DeviceId& device, bool success)
219 : {
220 12403 : std::unique_lock lk(mtx);
221 12404 : if (devices.erase(device) == 0)
222 0 : return false;
223 12403 : ++completeCount;
224 12403 : if (success)
225 12399 : ++successCount;
226 12403 : checkComplete(lk);
227 12402 : return true;
228 12402 : }
229 : bool empty() const
230 : {
231 : std::lock_guard lk(mtx);
232 : return devices.empty();
233 : }
234 2465 : bool pending(const DeviceId& device) const
235 : {
236 2465 : std::lock_guard lk(mtx);
237 4930 : return devices.find(device) != devices.end();
238 2465 : }
239 :
240 : private:
241 : mutable std::mutex mtx;
242 : OnComplete onComplete;
243 : std::set<DeviceId> devices;
244 : unsigned completeCount = 0;
245 : unsigned successCount = 0;
246 : bool started {false};
247 :
248 26715 : void checkComplete(std::unique_lock<std::mutex>& lk)
249 : {
250 26715 : if (started && (devices.empty() || successCount)) {
251 14312 : if (onComplete) {
252 14311 : auto cb = std::move(onComplete);
253 14312 : auto success = successCount != 0;
254 14312 : auto complete = completeCount != 0;
255 14312 : onComplete = {};
256 14312 : lk.unlock();
257 14313 : cb(success, complete);
258 14314 : }
259 : }
260 26712 : }
261 : };
262 :
263 : static const constexpr std::string_view RING_URI_PREFIX = "ring:";
264 : static const constexpr std::string_view JAMI_URI_PREFIX = "jami:";
265 : static const auto PROXY_REGEX = std::regex("(https?://)?([\\w\\.\\-_\\~]+)(:(\\d+)|:\\[(.+)-(.+)\\])?");
266 : static const constexpr std::string_view PEER_DISCOVERY_JAMI_SERVICE = "jami";
267 : const constexpr auto PEER_DISCOVERY_EXPIRATION = std::chrono::minutes(1);
268 :
269 : using ValueIdDist = std::uniform_int_distribution<dht::Value::Id>;
270 :
271 : std::string_view
272 40055 : stripPrefix(std::string_view toUrl)
273 : {
274 40055 : auto dhtf = toUrl.find(RING_URI_PREFIX);
275 40048 : if (dhtf != std::string_view::npos) {
276 0 : dhtf += RING_URI_PREFIX.size();
277 : } else {
278 40048 : dhtf = toUrl.find(JAMI_URI_PREFIX);
279 40062 : if (dhtf != std::string_view::npos) {
280 0 : dhtf += JAMI_URI_PREFIX.size();
281 : } else {
282 40062 : dhtf = toUrl.find("sips:");
283 40045 : dhtf = (dhtf == std::string_view::npos) ? 0 : dhtf + 5;
284 : }
285 : }
286 40045 : while (dhtf < toUrl.length() && toUrl[dhtf] == '/')
287 0 : dhtf++;
288 40055 : return toUrl.substr(dhtf);
289 : }
290 :
291 : std::string_view
292 40030 : parseJamiUri(std::string_view toUrl)
293 : {
294 40030 : auto sufix = stripPrefix(toUrl);
295 40075 : if (sufix.length() < 40)
296 0 : throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
297 :
298 40072 : const std::string_view toUri = sufix.substr(0, 40);
299 40074 : if (std::find_if_not(toUri.cbegin(), toUri.cend(), ::isxdigit) != toUri.cend())
300 0 : throw std::invalid_argument("Not a valid Jami URI: " + toUrl);
301 40133 : return toUri;
302 : }
303 :
304 : static constexpr std::string_view
305 4402 : dhtStatusStr(dht::NodeStatus status)
306 : {
307 : return status == dht::NodeStatus::Connected
308 4402 : ? "connected"sv
309 4402 : : (status == dht::NodeStatus::Connecting ? "connecting"sv : "disconnected"sv);
310 : }
311 :
312 823 : JamiAccount::JamiAccount(const std::string& accountId)
313 : : SIPAccountBase(accountId)
314 823 : , cachePath_(fileutils::get_cache_dir() / accountId)
315 823 : , dataPath_(cachePath_ / "values")
316 1646 : , logger_(Logger::dhtLogger(fmt::format("Account {}", accountId)))
317 823 : , certStore_ {std::make_shared<dhtnet::tls::CertificateStore>(idPath_, logger_)}
318 823 : , dht_(std::make_shared<dht::DhtRunner>())
319 823 : , treatedMessages_(cachePath_ / TREATED_PATH)
320 823 : , presenceManager_(std::make_unique<PresenceManager>(dht_))
321 823 : , connectionManager_ {}
322 6584 : , nonSwarmTransferManager_()
323 : {
324 823 : presenceListenerToken_ = presenceManager_->addListener([this](const std::string& uri, bool online) {
325 656 : runOnMainThread([w = weak(), uri, online] {
326 656 : if (auto sthis = w.lock()) {
327 656 : if (online) {
328 601 : sthis->onTrackedBuddyOnline(uri);
329 601 : sthis->messageEngine_.onPeerOnline(uri);
330 : } else {
331 55 : sthis->onTrackedBuddyOffline(uri);
332 : }
333 656 : }
334 656 : });
335 656 : });
336 : // When a device of a contact that advertises services changes presence,
337 : // re-publish that peer's service list with refreshed availability so an
338 : // already-open services menu greys out / re-enables entries live.
339 823 : svcPresenceListenerToken_ = presenceManager_->addDeviceListener([this](const std::string& uri,
340 : const dht::PkId&,
341 : bool) {
342 733 : runOnMainThread([w = weak(), uri] {
343 733 : auto sthis = w.lock();
344 733 : if (!sthis)
345 0 : return;
346 733 : auto servicesJson = sthis->buildPeerServicesJson(uri);
347 733 : if (servicesJson.empty())
348 733 : return;
349 0 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(0u,
350 0 : sthis->getAccountID(),
351 0 : uri,
352 : static_cast<int>(
353 : libjami::ServiceSignal::PeerServicesStatus::OK),
354 : servicesJson);
355 1466 : });
356 733 : });
357 823 : }
358 :
359 1646 : JamiAccount::~JamiAccount() noexcept
360 : {
361 823 : if (dht_)
362 823 : dht_->join();
363 823 : }
364 :
365 : void
366 840 : JamiAccount::shutdownConnections()
367 : {
368 840 : JAMI_LOG("[Account {}] Shutdown connections", getAccountID());
369 :
370 840 : decltype(gitServers_) gservers;
371 : {
372 840 : std::lock_guard lk(gitServersMtx_);
373 840 : gservers = std::move(gitServers_);
374 840 : }
375 1329 : for (auto& [_id, gs] : gservers)
376 974 : dht::ThreadPool::io().run([gs = std::shared_ptr(std::move(gs))] { gs->stop(); });
377 840 : gservers.clear();
378 :
379 : {
380 840 : std::lock_guard lk(connManagerMtx_);
381 : // Just move destruction on another thread.
382 840 : dht::ThreadPool::io().run([conMgr = std::shared_ptr(std::move(connectionManager_))] {});
383 840 : connectionManager_.reset();
384 840 : channelHandlers_.clear();
385 840 : }
386 840 : if (convModule_) {
387 741 : convModule_->shutdownConnections();
388 : }
389 :
390 840 : std::lock_guard lk(sipConnsMtx_);
391 840 : sipConns_.clear();
392 840 : }
393 :
394 : void
395 818 : JamiAccount::flush()
396 : {
397 : // Class base method
398 818 : SIPAccountBase::flush();
399 :
400 818 : dhtnet::fileutils::removeAll(cachePath_);
401 818 : dhtnet::fileutils::removeAll(dataPath_);
402 818 : dhtnet::fileutils::removeAll(idPath_, true);
403 818 : }
404 :
405 : std::shared_ptr<SIPCall>
406 97 : JamiAccount::newIncomingCall(const std::string& from,
407 : const std::vector<libjami::MediaMap>& mediaList,
408 : const std::shared_ptr<SipTransport>& sipTransp)
409 : {
410 97 : JAMI_DEBUG("New incoming call from {:s} with {:d} media", from, mediaList.size());
411 :
412 97 : if (sipTransp) {
413 97 : auto call = Manager::instance().callFactory.newSipCall(shared(), Call::CallType::INCOMING, mediaList);
414 97 : call->setPeerUri(JAMI_URI_PREFIX + from);
415 97 : call->setPeerNumber(from);
416 :
417 97 : call->setSipTransport(sipTransp, getContactHeader(sipTransp));
418 :
419 97 : return call;
420 97 : }
421 :
422 0 : JAMI_ERROR("newIncomingCall: unable to find matching call for {}", from);
423 0 : return nullptr;
424 : }
425 :
426 : std::shared_ptr<Call>
427 111 : JamiAccount::newOutgoingCall(std::string_view toUrl, const std::vector<libjami::MediaMap>& mediaList)
428 : {
429 111 : auto uri = Uri(toUrl);
430 111 : if (uri.scheme() == Uri::Scheme::SWARM || uri.scheme() == Uri::Scheme::RENDEZVOUS) {
431 : // NOTE: In this case newOutgoingCall can act as "resumeConference" and just attach the
432 : // host to the current hosted conference. So, no call will be returned in that case.
433 22 : return newSwarmOutgoingCallHelper(uri, mediaList);
434 : }
435 :
436 89 : auto& manager = Manager::instance();
437 89 : std::shared_ptr<SIPCall> call;
438 :
439 : // SIP allows sending empty invites, this use case is not used with Jami accounts.
440 89 : if (not mediaList.empty()) {
441 33 : call = manager.callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
442 : } else {
443 56 : JAMI_WARNING("Media list is empty, setting a default list");
444 112 : call = manager.callFactory.newSipCall(shared(),
445 : Call::CallType::OUTGOING,
446 112 : MediaAttribute::mediaAttributesToMediaMaps(
447 168 : createDefaultMediaList(isVideoEnabled())));
448 : }
449 :
450 89 : if (not call)
451 0 : return {};
452 :
453 89 : std::shared_lock lkCM(connManagerMtx_);
454 89 : if (!connectionManager_)
455 0 : return {};
456 :
457 89 : connectionManager_->getIceOptions([call, w = weak(), uri = std::move(uri)](auto&& opts) {
458 89 : if (call->isIceEnabled()) {
459 89 : if (not call->createIceMediaTransport(false)
460 178 : or not call->initIceMediaTransport(true, std::forward<dhtnet::IceTransportOptions>(opts))) {
461 0 : return;
462 : }
463 : }
464 89 : auto shared = w.lock();
465 89 : if (!shared)
466 0 : return;
467 89 : JAMI_LOG("New outgoing call with {}", uri.toString());
468 89 : call->setPeerNumber(uri.authority());
469 89 : call->setPeerUri(uri.toString());
470 :
471 89 : shared->newOutgoingCallHelper(call, uri);
472 89 : });
473 :
474 89 : return call;
475 111 : }
476 :
477 : void
478 89 : JamiAccount::newOutgoingCallHelper(const std::shared_ptr<SIPCall>& call, const Uri& uri)
479 : {
480 89 : JAMI_LOG("[Account {}] Calling peer {}", getAccountID(), uri.authority());
481 : try {
482 89 : startOutgoingCall(call, uri.authority());
483 0 : } catch (const std::invalid_argument&) {
484 0 : auto suffix = stripPrefix(uri.toString());
485 0 : NameDirectory::lookupUri(suffix,
486 0 : config().nameServer,
487 0 : [wthis_ = weak(), call](const std::string& regName,
488 : const std::string& address,
489 : NameDirectory::Response response) {
490 : // we may run inside an unknown thread, but following code must
491 : // be called in main thread
492 0 : runOnMainThread([wthis_, regName, address, response, call]() {
493 0 : if (response != NameDirectory::Response::found) {
494 0 : call->onFailure(PJSIP_SC_NOT_FOUND);
495 0 : return;
496 : }
497 0 : if (auto sthis = wthis_.lock()) {
498 : try {
499 0 : sthis->startOutgoingCall(call, address);
500 0 : } catch (const std::invalid_argument&) {
501 0 : call->onFailure(PJSIP_SC_NOT_FOUND);
502 0 : }
503 : } else {
504 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
505 0 : }
506 : });
507 0 : });
508 0 : }
509 89 : }
510 :
511 : std::shared_ptr<SIPCall>
512 22 : JamiAccount::newSwarmOutgoingCallHelper(const Uri& uri, const std::vector<libjami::MediaMap>& mediaList)
513 : {
514 22 : JAMI_DEBUG("[Account {}] Calling conversation {}", getAccountID(), uri.authority());
515 : return convModule()
516 22 : ->call(uri.authority(), mediaList, [this, uri](const auto& accountUri, const auto& deviceId, const auto& call) {
517 11 : if (!call)
518 0 : return;
519 :
520 11 : std::string peerId = accountUri;
521 11 : if (uri.scheme() == Uri::Scheme::RENDEZVOUS) {
522 9 : auto parts = jami::split_string(accountUri, '/');
523 9 : if (parts.size() == 4)
524 18 : peerId = std::string(parts[1]);
525 9 : }
526 :
527 11 : std::unique_lock lkSipConn(sipConnsMtx_);
528 12 : for (auto& [key, value] : sipConns_) {
529 1 : if (key.first != peerId || key.second != deviceId)
530 0 : continue;
531 1 : if (value.empty())
532 0 : continue;
533 1 : auto& sipConn = value.back();
534 :
535 1 : if (!sipConn.channel) {
536 0 : JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
537 0 : continue;
538 : }
539 :
540 1 : auto transport = sipConn.transport;
541 1 : if (!transport or !sipConn.channel)
542 0 : continue;
543 1 : call->setState(Call::ConnectionState::PROGRESSING);
544 1 : call->setSipTransport(transport, getContactHeader(transport));
545 :
546 1 : auto remoted_address = sipConn.channel->getRemoteAddress();
547 : try {
548 1 : onConnectedOutgoingCall(call, uri.authority(), remoted_address);
549 1 : return;
550 0 : } catch (const VoipLinkException&) {
551 : // In this case, the main scenario is that SIPStartCall failed because
552 : // the ICE is dead and the TLS session didn't send any packet on that dead
553 : // link (connectivity change, killed by the operating system, etc)
554 : // Here, we don't need to do anything, the TLS will fail and will delete
555 : // the cached transport
556 0 : continue;
557 : }
558 : }
559 10 : lkSipConn.unlock();
560 : {
561 10 : std::lock_guard lkP(pendingCallsMutex_);
562 10 : pendingCalls_[deviceId].emplace_back(call);
563 10 : }
564 :
565 : // Else, ask for a channel (for future calls/text messages)
566 10 : auto type = call->hasVideo() ? "videoCall" : "audioCall";
567 10 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
568 20 : requestSIPConnection(peerId, deviceId, type, true, call);
569 56 : });
570 : }
571 :
572 : void
573 10 : JamiAccount::handleIncomingConversationCall(const std::string& callId, const std::string& destination)
574 : {
575 10 : auto split = jami::split_string(destination, '/');
576 10 : if (split.size() != 4)
577 0 : return;
578 20 : auto conversationId = std::string(split[0]);
579 20 : auto accountUri = std::string(split[1]);
580 20 : auto deviceId = std::string(split[2]);
581 10 : auto confId = std::string(split[3]);
582 :
583 10 : if (getUsername() != accountUri || currentDeviceId() != deviceId)
584 0 : return;
585 :
586 : // Avoid concurrent checks in this part
587 10 : std::lock_guard lk(rdvMtx_);
588 10 : auto isNotHosting = !convModule()->isHosting(conversationId, confId);
589 10 : if (confId == "0") {
590 1 : auto currentCalls = convModule()->getActiveCalls(conversationId);
591 1 : if (!currentCalls.empty()) {
592 0 : confId = currentCalls[0]["id"];
593 0 : isNotHosting = false;
594 : } else {
595 1 : confId = callId;
596 1 : JAMI_DEBUG("No active call to join, create conference");
597 : }
598 1 : }
599 10 : auto preferences = convModule()->getConversationPreferences(conversationId);
600 10 : auto canHost = true;
601 : #if defined(__ANDROID__) || defined(__APPLE__)
602 : // By default, mobile devices SHOULD NOT host conferences.
603 : canHost = false;
604 : #endif
605 10 : auto itPref = preferences.find(ConversationPreferences::HOST_CONFERENCES);
606 10 : if (itPref != preferences.end()) {
607 0 : canHost = itPref->second == TRUE_STR;
608 : }
609 :
610 10 : auto call = getCall(callId);
611 10 : if (!call) {
612 0 : JAMI_ERROR("Call {} not found", callId);
613 0 : return;
614 : }
615 :
616 10 : if (isNotHosting && !canHost) {
617 0 : JAMI_DEBUG("Request for hosting a conference declined");
618 0 : Manager::instance().hangupCall(getAccountID(), callId);
619 0 : return;
620 : }
621 : // Due to the fact that in a conference, the host is not the one who
622 : // provides the initial sdp offer, the following block of code is responsible
623 : // for handling the medialist that the host will form his response with.
624 : // We always want the hosts response to be the same length as that of the
625 : // peer who is asking to join (providing the offer). A priori though the peer
626 : // doesn't know what active media streams the host will have so we deal with the
627 : // possible cases here.
628 10 : std::shared_ptr<Conference> conf;
629 10 : std::vector<libjami::MediaMap> currentMediaList;
630 10 : if (!isNotHosting) {
631 7 : conf = getConference(confId);
632 7 : if (!conf) {
633 0 : JAMI_ERROR("[conf:{}] Conference not found", confId);
634 0 : return;
635 : }
636 7 : auto hostMedias = conf->currentMediaList();
637 7 : auto sipCall = std::dynamic_pointer_cast<SIPCall>(call);
638 7 : if (hostMedias.empty()) {
639 0 : currentMediaList = MediaAttribute::mediaAttributesToMediaMaps(
640 0 : createDefaultMediaList(call->hasVideo(), true));
641 7 : } else if (hostMedias.size() < sipCall->getRtpSessionList().size()) {
642 : // First case: host has less media streams than the other person is joining
643 : // with. We need to add video media to the host before accepting the offer
644 : // This can happen if we host an audio call and someone joins with video
645 0 : currentMediaList = hostMedias;
646 0 : currentMediaList.push_back(
647 : {{libjami::Media::MediaAttributeKey::MEDIA_TYPE, libjami::Media::MediaAttributeValue::VIDEO},
648 : {libjami::Media::MediaAttributeKey::ENABLED, TRUE_STR},
649 : {libjami::Media::MediaAttributeKey::MUTED, TRUE_STR},
650 : {libjami::Media::MediaAttributeKey::SOURCE, ""},
651 : {libjami::Media::MediaAttributeKey::LABEL, "video_0"}});
652 : } else {
653 7 : bool hasVideo = false;
654 7 : if (sipCall) {
655 7 : const auto rtpSessions = sipCall->getRtpSessionList();
656 7 : hasVideo = std::any_of(rtpSessions.begin(), rtpSessions.end(), [](const auto& session) {
657 13 : return session && session->getMediaType() == MediaType::MEDIA_VIDEO;
658 : });
659 7 : }
660 : // The second case is that the host has the same or more media
661 : // streams than the person joining. In this case we match all their
662 : // medias to form our offer. They will then potentially join the call without seeing
663 : // seeing all of our medias. For now we deal with this by calling a
664 : // requestmediachange once they've joined.
665 14 : for (const auto& m : conf->currentMediaList()) {
666 : // We only expect to have 1 audio stream, add it.
667 26 : if (m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE) == libjami::Media::MediaAttributeValue::AUDIO) {
668 7 : currentMediaList.emplace_back(m);
669 6 : } else if (hasVideo
670 24 : && m.at(libjami::Media::MediaAttributeKey::MEDIA_TYPE)
671 6 : == libjami::Media::MediaAttributeValue::VIDEO) {
672 6 : currentMediaList.emplace_back(m);
673 6 : break;
674 : }
675 7 : }
676 : }
677 7 : }
678 10 : Manager::instance().acceptCall(*call, currentMediaList);
679 :
680 10 : if (isNotHosting) {
681 3 : JAMI_DEBUG("Creating conference for swarm {} with ID {}", conversationId, confId);
682 : // Create conference and host it.
683 3 : convModule()->hostConference(conversationId, confId, callId);
684 : } else {
685 7 : JAMI_DEBUG("Adding participant {} for swarm {} with ID {}", callId, conversationId, confId);
686 7 : Manager::instance().addAudio(*call);
687 7 : conf->addSubCall(callId);
688 7 : emitSignal<libjami::CallSignal::ConferenceChanged>(getAccountID(), conf->getConfId(), conf->getStateStr());
689 : }
690 10 : }
691 :
692 : std::shared_ptr<SIPCall>
693 178 : JamiAccount::createSubCall(const std::shared_ptr<SIPCall>& mainCall)
694 : {
695 178 : auto mediaList = MediaAttribute::mediaAttributesToMediaMaps(mainCall->getMediaAttributeList());
696 356 : return Manager::instance().callFactory.newSipCall(shared(), Call::CallType::OUTGOING, mediaList);
697 178 : }
698 :
699 : void
700 89 : JamiAccount::startOutgoingCall(const std::shared_ptr<SIPCall>& call, const std::string& toUri)
701 : {
702 89 : if (not accountManager_ or not dht_) {
703 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
704 0 : return;
705 : }
706 :
707 : // TODO: for now, we automatically trust all explicitly called peers
708 89 : setCertificateStatus(toUri, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
709 :
710 89 : call->setState(Call::ConnectionState::TRYING);
711 89 : std::weak_ptr<SIPCall> wCall = call;
712 :
713 178 : accountManager_->lookupAddress(toUri,
714 178 : [wCall](const std::string& regName,
715 : const std::string& /*address*/,
716 : const NameDirectory::Response& response) {
717 89 : if (response == NameDirectory::Response::found)
718 1 : if (auto call = wCall.lock()) {
719 1 : call->setPeerRegisteredName(regName);
720 1 : }
721 89 : });
722 :
723 89 : dht::InfoHash peer_account(toUri);
724 89 : if (!peer_account) {
725 0 : throw std::invalid_argument("Invalid peer account: " + toUri);
726 : }
727 :
728 : // Call connected devices
729 89 : std::set<DeviceId> devices;
730 89 : std::unique_lock lkSipConn(sipConnsMtx_);
731 : // NOTE: dummyCall is a call used to avoid to mark the call as failed if the
732 : // cached connection is failing with ICE (close event still not detected).
733 89 : auto dummyCall = createSubCall(call);
734 :
735 89 : if (!dummyCall) {
736 0 : call->onFailure(PJSIP_SC_SERVICE_UNAVAILABLE);
737 0 : return;
738 : }
739 :
740 89 : call->addSubCall(*dummyCall);
741 89 : dummyCall->setIceMedia(call->getIceMedia());
742 263 : auto sendRequest = [this, wCall, toUri, dummyCall = std::move(dummyCall)](const DeviceId& deviceId,
743 : bool eraseDummy) {
744 174 : if (eraseDummy) {
745 : // Mark the temp call as failed to stop the main call if necessary
746 89 : if (dummyCall)
747 89 : dummyCall->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
748 89 : return;
749 : }
750 85 : auto call = wCall.lock();
751 85 : if (not call)
752 0 : return;
753 85 : auto state = call->getConnectionState();
754 85 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING)
755 0 : return;
756 :
757 85 : auto dev_call = createSubCall(call);
758 85 : dev_call->setPeerNumber(call->getPeerNumber());
759 85 : dev_call->setState(Call::ConnectionState::TRYING);
760 85 : call->addStateListener([w = weak(), deviceId](Call::CallState, Call::ConnectionState state, int) {
761 251 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
762 85 : if (auto shared = w.lock())
763 85 : shared->callConnectionClosed(deviceId, true);
764 85 : return false;
765 : }
766 166 : return true;
767 : });
768 85 : call->addSubCall(*dev_call);
769 85 : dev_call->setIceMedia(call->getIceMedia());
770 : {
771 85 : std::lock_guard lk(pendingCallsMutex_);
772 85 : pendingCalls_[deviceId].emplace_back(dev_call);
773 85 : }
774 :
775 85 : JAMI_WARNING("[call {}] No channeled socket with this peer. Send request", call->getCallId());
776 : // Else, ask for a channel (for future calls/text messages)
777 85 : const auto* type = call->hasVideo() ? "videoCall" : "audioCall";
778 170 : requestSIPConnection(toUri, deviceId, type, true, dev_call);
779 174 : };
780 :
781 89 : std::vector<std::shared_ptr<dhtnet::ChannelSocket>> channels;
782 141 : for (auto& [key, value] : sipConns_) {
783 52 : if (key.first != toUri)
784 48 : continue;
785 4 : if (value.empty())
786 0 : continue;
787 4 : auto& sipConn = value.back();
788 :
789 4 : if (!sipConn.channel) {
790 0 : JAMI_WARNING("A SIP transport exists without Channel, this is a bug. Please report");
791 0 : continue;
792 : }
793 :
794 4 : auto transport = sipConn.transport;
795 4 : auto remote_address = sipConn.channel->getRemoteAddress();
796 4 : if (!transport or !remote_address)
797 0 : continue;
798 :
799 4 : channels.emplace_back(sipConn.channel);
800 :
801 4 : JAMI_WARNING("[call {}] A channeled socket is detected with this peer.", call->getCallId());
802 :
803 4 : auto dev_call = createSubCall(call);
804 4 : dev_call->setPeerNumber(call->getPeerNumber());
805 4 : dev_call->setSipTransport(transport, getContactHeader(transport));
806 4 : call->addSubCall(*dev_call);
807 4 : dev_call->setIceMedia(call->getIceMedia());
808 :
809 : // Set the call in PROGRESSING State because the ICE session
810 : // is already ready. Note that this line should be after
811 : // addSubcall() to change the state of the main call
812 : // and avoid to get an active call in a TRYING state.
813 4 : dev_call->setState(Call::ConnectionState::PROGRESSING);
814 :
815 : {
816 4 : std::lock_guard lk(onConnectionClosedMtx_);
817 4 : onConnectionClosed_[key.second] = sendRequest;
818 4 : }
819 :
820 4 : call->addStateListener([w = weak(), deviceId = key.second](Call::CallState, Call::ConnectionState state, int) {
821 12 : if (state != Call::ConnectionState::PROGRESSING and state != Call::ConnectionState::TRYING) {
822 4 : if (auto shared = w.lock())
823 4 : shared->callConnectionClosed(deviceId, true);
824 4 : return false;
825 : }
826 8 : return true;
827 : });
828 :
829 : try {
830 4 : onConnectedOutgoingCall(dev_call, toUri, remote_address);
831 0 : } catch (const VoipLinkException&) {
832 : // In this case, the main scenario is that SIPStartCall failed because
833 : // the ICE is dead and the TLS session didn't send any packet on that dead
834 : // link (connectivity change, killed by the os, etc)
835 : // Here, we don't need to do anything, the TLS will fail and will delete
836 : // the cached transport
837 0 : continue;
838 0 : }
839 4 : devices.emplace(key.second);
840 4 : }
841 :
842 89 : lkSipConn.unlock();
843 : // Note: Send beacon can destroy the socket (if storing last occurence of shared_ptr)
844 : // causing sipConn to be destroyed. So, do it while sipConns_ not locked.
845 93 : for (const auto& channel : channels)
846 4 : channel->sendBeacon();
847 :
848 : // Find listening devices for this account
849 267 : accountManager_->forEachDevice(
850 : peer_account,
851 178 : [this, devices = std::move(devices), sendRequest](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
852 : // Test if already sent via a SIP transport
853 89 : auto deviceId = dev->getLongId();
854 89 : if (devices.find(deviceId) != devices.end())
855 4 : return;
856 : {
857 85 : std::lock_guard lk(onConnectionClosedMtx_);
858 85 : onConnectionClosed_[deviceId] = sendRequest;
859 85 : }
860 85 : sendRequest(deviceId, false);
861 : },
862 178 : [wCall](bool ok) {
863 89 : if (not ok) {
864 1 : if (auto call = wCall.lock()) {
865 1 : JAMI_WARNING("[call:{}] No devices found", call->getCallId());
866 : // Note: if a P2P connection exists, the call will be at least in CONNECTING
867 1 : if (call->getConnectionState() == Call::ConnectionState::TRYING)
868 1 : call->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
869 1 : }
870 : }
871 89 : });
872 89 : }
873 :
874 : void
875 97 : JamiAccount::onConnectedOutgoingCall(const std::shared_ptr<SIPCall>& call,
876 : const std::string& to_id,
877 : dhtnet::IpAddr target)
878 : {
879 97 : if (!call)
880 0 : return;
881 97 : JAMI_LOG("[call:{}] Outgoing call connected to {}", call->getCallId(), to_id);
882 :
883 97 : const auto localAddress = dhtnet::ip_utils::getInterfaceAddr(getLocalInterface(), target.getFamily());
884 :
885 97 : dhtnet::IpAddr addrSdp = getPublishedSameasLocal() ? localAddress
886 97 : : connectionManager_->getPublishedIpAddress(target.getFamily());
887 :
888 : // fallback on local address
889 97 : if (not addrSdp)
890 0 : addrSdp = localAddress;
891 :
892 : // Building the local SDP offer
893 97 : auto& sdp = call->getSDP();
894 :
895 97 : sdp.setPublishedIP(addrSdp);
896 :
897 97 : auto mediaAttrList = call->getMediaAttributeList();
898 97 : if (mediaAttrList.empty()) {
899 0 : JAMI_ERROR("[call:{}] No media. Abort!", call->getCallId());
900 0 : return;
901 : }
902 :
903 97 : if (not sdp.createOffer(mediaAttrList)) {
904 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
905 0 : return;
906 : }
907 :
908 : // Note: pj_ice_strans_create can call onComplete in the same thread
909 : // This means that iceMutex_ in IceTransport can be locked when onInitDone is called
910 : // So, we need to run the call creation in the main thread
911 : // Also, we do not directly call SIPStartCall before receiving onInitDone, because
912 : // there is an inside waitForInitialization that can block the thread.
913 : // Note: avoid runMainThread as SIPStartCall use transportMutex
914 97 : dht::ThreadPool::io().run([w = weak(), call = std::move(call), target] {
915 97 : auto account = w.lock();
916 97 : if (not account)
917 0 : return;
918 :
919 97 : if (not account->SIPStartCall(*call, target)) {
920 0 : JAMI_ERROR("[call:{}] Unable to send outgoing INVITE request for new call", call->getCallId());
921 : }
922 97 : });
923 97 : }
924 :
925 : bool
926 97 : JamiAccount::SIPStartCall(SIPCall& call, const dhtnet::IpAddr& target)
927 : {
928 97 : JAMI_LOG("[call:{}] Start SIP call", call.getCallId());
929 :
930 97 : if (call.isIceEnabled())
931 97 : call.addLocalIceAttributes();
932 :
933 : std::string toUri(
934 97 : getToUri(call.getPeerNumber() + "@" + target.toString(true))); // expecting a fully well formed sip uri
935 :
936 97 : pj_str_t pjTo = sip_utils::CONST_PJ_STR(toUri);
937 :
938 : // Create the from header
939 97 : std::string from(getFromUri());
940 97 : pj_str_t pjFrom = sip_utils::CONST_PJ_STR(from);
941 :
942 97 : std::string targetStr = getToUri(target.toString(true));
943 97 : pj_str_t pjTarget = sip_utils::CONST_PJ_STR(targetStr);
944 :
945 97 : auto contact = call.getContactHeader();
946 97 : auto pjContact = sip_utils::CONST_PJ_STR(contact);
947 :
948 97 : JAMI_LOG("[call:{}] Contact header: {} / {} -> {} / {}", call.getCallId(), contact, from, toUri, targetStr);
949 :
950 97 : auto* local_sdp = call.getSDP().getLocalSdpSession();
951 97 : pjsip_dialog* dialog {nullptr};
952 97 : pjsip_inv_session* inv {nullptr};
953 97 : if (!CreateClientDialogAndInvite(&pjFrom, &pjContact, &pjTo, &pjTarget, local_sdp, &dialog, &inv))
954 0 : return false;
955 :
956 97 : inv->mod_data[link_.getModId()] = &call;
957 97 : call.setInviteSession(inv);
958 :
959 : pjsip_tx_data* tdata;
960 :
961 97 : if (pjsip_inv_invite(call.inviteSession_.get(), &tdata) != PJ_SUCCESS) {
962 0 : JAMI_ERROR("[call:{}] Unable to initialize invite", call.getCallId());
963 0 : return false;
964 : }
965 :
966 : pjsip_tpselector tp_sel;
967 97 : tp_sel.type = PJSIP_TPSELECTOR_TRANSPORT;
968 97 : if (!call.getTransport()) {
969 0 : JAMI_ERROR("[call:{}] Unable to get transport", call.getCallId());
970 0 : return false;
971 : }
972 97 : tp_sel.u.transport = call.getTransport()->get();
973 97 : if (pjsip_dlg_set_transport(dialog, &tp_sel) != PJ_SUCCESS) {
974 0 : JAMI_ERROR("[call:{}] Unable to associate transport for invite session dialog", call.getCallId());
975 0 : return false;
976 : }
977 :
978 97 : JAMI_LOG("[call:{}] Sending SIP invite", call.getCallId());
979 :
980 : // Add user-agent header
981 97 : sip_utils::addUserAgentHeader(getUserAgentName(), tdata);
982 :
983 97 : if (pjsip_inv_send_msg(call.inviteSession_.get(), tdata) != PJ_SUCCESS) {
984 0 : JAMI_ERROR("[call:{}] Unable to send invite message", call.getCallId());
985 0 : return false;
986 : }
987 :
988 97 : call.setState(Call::CallState::ACTIVE, Call::ConnectionState::PROGRESSING);
989 97 : return true;
990 97 : }
991 :
992 : void
993 2688 : JamiAccount::saveConfig() const
994 : {
995 : try {
996 2688 : auto accountConfig = config().path / "config.yml";
997 2688 : std::lock_guard lock(dhtnet::fileutils::getFileLock(accountConfig));
998 2688 : std::ofstream fout(accountConfig);
999 2688 : YAML::Emitter accountOut(fout);
1000 2688 : config().serialize(accountOut);
1001 2688 : JAMI_LOG("Saved account config to {}", accountConfig);
1002 2688 : } catch (const std::exception& e) {
1003 0 : JAMI_ERROR("Error saving account config: {}", e.what());
1004 0 : }
1005 2688 : }
1006 :
1007 : void
1008 840 : JamiAccount::loadConfig()
1009 : {
1010 840 : SIPAccountBase::loadConfig();
1011 840 : registeredName_ = config().registeredName;
1012 840 : if (accountManager_)
1013 22 : accountManager_->setAccountDeviceName(config().deviceName);
1014 840 : if (connectionManager_) {
1015 18 : if (auto c = connectionManager_->getConfig()) {
1016 : // Update connectionManager's config
1017 18 : c->upnpEnabled = config().upnpEnabled;
1018 18 : c->turnEnabled = config().turnEnabled;
1019 18 : c->turnServer = config().turnServer;
1020 18 : c->turnServerUserName = config().turnServerUserName;
1021 18 : c->turnServerPwd = config().turnServerPwd;
1022 18 : c->turnServerRealm = config().turnServerRealm;
1023 18 : }
1024 : }
1025 840 : if (config().proxyEnabled) {
1026 : try {
1027 0 : auto str = fileutils::loadCacheTextFile(cachePath_ / "dhtproxy", std::chrono::hours(24 * 14));
1028 0 : Json::Value root;
1029 0 : if (json::parse(str, root)) {
1030 0 : proxyServerCached_ = root[getProxyConfigKey()].asString();
1031 : }
1032 0 : } catch (const std::exception& e) {
1033 0 : JAMI_LOG("[Account {}] Unable to load proxy URL from cache: {}", getAccountID(), e.what());
1034 0 : proxyServerCached_.clear();
1035 0 : }
1036 : } else {
1037 840 : proxyServerCached_.clear();
1038 840 : std::error_code ec;
1039 840 : std::filesystem::remove(cachePath_ / "dhtproxy", ec);
1040 : }
1041 840 : if (not config().dhtProxyServerEnabled) {
1042 840 : dhtProxyServer_.reset();
1043 : }
1044 840 : auto credentials = consumeConfigCredentials();
1045 840 : loadAccount(credentials.archive_password_scheme, credentials.archive_password, credentials.archive_path);
1046 840 : }
1047 :
1048 : bool
1049 7 : JamiAccount::changeArchivePassword(const std::string& password_old, const std::string& password_new)
1050 : {
1051 : try {
1052 7 : if (!accountManager_->changePassword(password_old, password_new)) {
1053 2 : JAMI_ERROR("[Account {}] Unable to change archive password", getAccountID());
1054 2 : return false;
1055 : }
1056 10 : editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = not password_new.empty(); });
1057 0 : } catch (const std::exception& ex) {
1058 0 : JAMI_ERROR("[Account {}] Unable to change archive password: {}", getAccountID(), ex.what());
1059 0 : if (password_old.empty()) {
1060 0 : editConfig([&](JamiAccountConfig& config) { config.archiveHasPassword = true; });
1061 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
1062 : }
1063 0 : return false;
1064 0 : }
1065 5 : if (password_old != password_new)
1066 5 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
1067 5 : return true;
1068 : }
1069 :
1070 : bool
1071 3 : JamiAccount::isPasswordValid(const std::string& password)
1072 : {
1073 3 : return accountManager_ and accountManager_->isPasswordValid(password);
1074 : }
1075 :
1076 : std::vector<uint8_t>
1077 0 : JamiAccount::getPasswordKey(const std::string& password)
1078 : {
1079 0 : return accountManager_ ? accountManager_->getPasswordKey(password) : std::vector<uint8_t>();
1080 : }
1081 :
1082 : bool
1083 7 : JamiAccount::provideAccountAuthentication(const std::string& credentialsFromUser, const std::string& scheme)
1084 : {
1085 7 : if (auto manager = std::dynamic_pointer_cast<ArchiveAccountManager>(accountManager_)) {
1086 7 : return manager->provideAccountAuthentication(credentialsFromUser, scheme);
1087 7 : }
1088 0 : JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while providing current account authentication.");
1089 0 : return false;
1090 : }
1091 :
1092 : int32_t
1093 5 : JamiAccount::addDevice(const std::string& uriProvided)
1094 : {
1095 5 : JAMI_LOG("[LinkDevice] JamiAccount::addDevice({}, {})", getAccountID(), uriProvided);
1096 5 : if (not accountManager_) {
1097 0 : JAMI_ERROR("[LinkDevice] Invalid AccountManager instance while adding a device.");
1098 0 : return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
1099 : }
1100 5 : auto authHandler = channelHandlers_.find(Uri::Scheme::AUTH);
1101 5 : if (authHandler == channelHandlers_.end())
1102 0 : return static_cast<int32_t>(AccountManager::AddDeviceError::GENERIC);
1103 10 : return accountManager_->addDevice(uriProvided,
1104 5 : config().archiveHasPassword ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
1105 : : fileutils::ARCHIVE_AUTH_SCHEME_NONE,
1106 10 : (AuthChannelHandler*) authHandler->second.get());
1107 : }
1108 :
1109 : bool
1110 0 : JamiAccount::cancelAddDevice(uint32_t op_token)
1111 : {
1112 0 : if (!accountManager_)
1113 0 : return false;
1114 0 : return accountManager_->cancelAddDevice(op_token);
1115 : }
1116 :
1117 : bool
1118 4 : JamiAccount::confirmAddDevice(uint32_t op_token)
1119 : {
1120 4 : if (!accountManager_)
1121 0 : return false;
1122 4 : return accountManager_->confirmAddDevice(op_token);
1123 : }
1124 :
1125 : bool
1126 38 : JamiAccount::exportArchive(const std::string& destinationPath, std::string_view scheme, const std::string& password)
1127 : {
1128 38 : if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1129 38 : return manager->exportArchive(destinationPath, scheme, password);
1130 : }
1131 0 : return false;
1132 : }
1133 :
1134 : bool
1135 2 : JamiAccount::setValidity(std::string_view scheme, const std::string& pwd, const dht::InfoHash& id, int64_t validity)
1136 : {
1137 2 : if (auto* manager = dynamic_cast<ArchiveAccountManager*>(accountManager_.get())) {
1138 2 : if (manager->setValidity(scheme, pwd, id_, id, validity)) {
1139 2 : saveIdentity(id_, idPath_, DEVICE_ID_PATH);
1140 2 : return true;
1141 : }
1142 : }
1143 0 : return false;
1144 : }
1145 :
1146 : void
1147 4 : JamiAccount::forceReloadAccount()
1148 : {
1149 4 : editConfig([&](JamiAccountConfig& conf) {
1150 4 : conf.receipt.clear();
1151 4 : conf.receiptSignature.clear();
1152 4 : });
1153 4 : loadAccount();
1154 4 : }
1155 :
1156 : void
1157 2 : JamiAccount::unlinkConversations(const std::set<std::string>& removed)
1158 : {
1159 2 : std::lock_guard lock(configurationMutex_);
1160 2 : if (const auto* info = accountManager_->getInfo()) {
1161 2 : auto contacts = info->contacts->getContacts();
1162 4 : for (auto& [id, c] : contacts) {
1163 2 : if (removed.find(c.conversationId) != removed.end()) {
1164 2 : info->contacts->updateConversation(id, "");
1165 1 : JAMI_WARNING("[Account {}] Detected removed conversation ({}) in contact details for {}",
1166 : getAccountID(),
1167 : c.conversationId,
1168 : id.toString());
1169 : }
1170 : }
1171 2 : }
1172 2 : }
1173 :
1174 : bool
1175 1298 : JamiAccount::isValidAccountDevice(const dht::crypto::Certificate& cert) const
1176 : {
1177 1298 : if (accountManager_) {
1178 1298 : if (const auto* info = accountManager_->getInfo()) {
1179 1298 : if (info->contacts)
1180 1298 : return info->contacts->isValidAccountDevice(cert).isValid();
1181 : }
1182 : }
1183 0 : return false;
1184 : }
1185 :
1186 : bool
1187 3 : JamiAccount::revokeDevice(const std::string& device, std::string_view scheme, const std::string& password)
1188 : {
1189 3 : if (not accountManager_)
1190 0 : return false;
1191 3 : return accountManager_
1192 6 : ->revokeDevice(device, scheme, password, [this, device](AccountManager::RevokeDeviceResult result) {
1193 3 : emitSignal<libjami::ConfigurationSignal::DeviceRevocationEnded>(getAccountID(),
1194 3 : device,
1195 : static_cast<int>(result));
1196 6 : });
1197 : return true;
1198 : }
1199 :
1200 : std::pair<std::string, std::string>
1201 822 : JamiAccount::saveIdentity(const dht::crypto::Identity& id, const std::filesystem::path& path, const std::string& name)
1202 : {
1203 822 : auto names = std::make_pair(name + ".key", name + ".crt");
1204 822 : if (id.first)
1205 822 : fileutils::saveFile(path / names.first, id.first->serialize(), 0600);
1206 822 : if (id.second)
1207 822 : fileutils::saveFile(path / names.second, id.second->getPacked(), 0600);
1208 822 : return names;
1209 0 : }
1210 :
1211 : void
1212 820 : JamiAccount::scheduleAccountReady() const
1213 : {
1214 820 : const auto accountId = getAccountID();
1215 1640 : runOnMainThread([accountId] { Manager::instance().markAccountReady(accountId); });
1216 820 : }
1217 :
1218 : AccountManager::OnChangeCallback
1219 840 : JamiAccount::setupAccountCallbacks()
1220 : {
1221 840 : return AccountManager::OnChangeCallback {[this](const std::string& uri, bool confirmed) {
1222 128 : onContactAdded(uri, confirmed);
1223 128 : },
1224 840 : [this](const std::string& uri, bool banned) {
1225 11 : onContactRemoved(uri, banned);
1226 11 : },
1227 840 : [this](const std::string& uri,
1228 : const std::string& conversationId,
1229 : const std::vector<uint8_t>& payload,
1230 : TimePoint received,
1231 : TimePoint invited) {
1232 86 : onIncomingTrustRequest(uri, conversationId, payload, received, invited);
1233 86 : },
1234 840 : [this](const std::map<DeviceId, KnownDevice>& devices) {
1235 2976 : onKnownDevicesChanged(devices);
1236 2977 : },
1237 840 : [this](const std::string& conversationId, const std::string& deviceId) {
1238 62 : onConversationRequestAccepted(conversationId, deviceId);
1239 62 : },
1240 1680 : [this](const std::string& uri, const std::string& convFromReq) {
1241 63 : onContactConfirmed(uri, convFromReq);
1242 840 : }};
1243 : }
1244 :
1245 : void
1246 128 : JamiAccount::onContactAdded(const std::string& uri, bool confirmed)
1247 : {
1248 128 : if (!id_.first)
1249 3 : return;
1250 125 : if (jami::Manager::instance().syncOnRegister) {
1251 125 : dht::ThreadPool::io().run([w = weak(), uri, confirmed] {
1252 125 : if (auto shared = w.lock()) {
1253 125 : if (auto* cm = shared->convModule(true)) {
1254 125 : auto activeConv = cm->getOneToOneConversation(uri);
1255 125 : if (!activeConv.empty())
1256 125 : cm->bootstrap(activeConv);
1257 125 : }
1258 : // Propagate the new contact to our other devices.
1259 125 : shared->onSyncListChanged();
1260 125 : emitSignal<libjami::ConfigurationSignal::ContactAdded>(shared->getAccountID(), uri, confirmed);
1261 125 : }
1262 125 : });
1263 : }
1264 : }
1265 :
1266 : void
1267 11 : JamiAccount::onContactRemoved(const std::string& uri, bool banned)
1268 : {
1269 11 : if (!id_.first)
1270 0 : return;
1271 11 : dht::ThreadPool::io().run([w = weak(), uri, banned] {
1272 11 : if (auto shared = w.lock()) {
1273 : // Erase linked conversation's requests
1274 11 : if (auto* convModule = shared->convModule(true))
1275 11 : convModule->removeContact(uri, banned);
1276 : // Remove current connections with contact
1277 : // Note: if contact is ourself, we don't close the connection
1278 : // because it's used for syncing other conversations.
1279 11 : if (shared->connectionManager_ && uri != shared->getUsername()) {
1280 11 : shared->connectionManager_->closeConnectionsWith(uri);
1281 : }
1282 : // Propagate the removal to our other devices.
1283 11 : shared->onSyncListChanged();
1284 : // Update client.
1285 11 : emitSignal<libjami::ConfigurationSignal::ContactRemoved>(shared->getAccountID(), uri, banned);
1286 11 : }
1287 11 : });
1288 : }
1289 :
1290 : void
1291 86 : JamiAccount::onIncomingTrustRequest(const std::string& uri,
1292 : const std::string& conversationId,
1293 : const std::vector<uint8_t>& payload,
1294 : TimePoint received,
1295 : TimePoint invited)
1296 : {
1297 86 : if (!id_.first)
1298 0 : return;
1299 86 : dht::ThreadPool::io().run([w = weak(), uri, conversationId, payload, received, invited] {
1300 86 : if (auto shared = w.lock()) {
1301 86 : shared->clearProfileCache(uri);
1302 86 : if (conversationId.empty()) {
1303 : // Old path
1304 0 : emitSignal<libjami::ConfigurationSignal::IncomingTrustRequest>(shared->getAccountID(),
1305 0 : conversationId,
1306 0 : uri,
1307 0 : payload,
1308 0 : toSecondsSinceEpoch(received));
1309 0 : return;
1310 : }
1311 : // Here account can be initializing
1312 86 : if (auto* cm = shared->convModule(true)) {
1313 86 : auto activeConv = cm->getOneToOneConversation(uri);
1314 86 : if (activeConv != conversationId)
1315 60 : cm->onTrustRequest(uri, conversationId, payload, received, invited);
1316 86 : }
1317 86 : }
1318 : });
1319 : }
1320 :
1321 : void
1322 2976 : JamiAccount::onKnownDevicesChanged(const std::map<DeviceId, KnownDevice>& devices)
1323 : {
1324 2976 : std::map<std::string, std::string> ids;
1325 1010999 : for (auto& d : devices) {
1326 1008033 : auto id = d.first.toString();
1327 1008020 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
1328 1008025 : ids.emplace(std::move(id), std::move(label));
1329 1008017 : }
1330 2972 : runOnMainThread([id = getAccountID(), devices = std::move(ids)] {
1331 2977 : emitSignal<libjami::ConfigurationSignal::KnownDevicesChanged>(id, devices);
1332 2977 : });
1333 2977 : }
1334 :
1335 : void
1336 62 : JamiAccount::onConversationRequestAccepted(const std::string& conversationId, const std::string& deviceId)
1337 : {
1338 : // Note: Do not retrigger on another thread. This has to be done
1339 : // at the same time of acceptTrustRequest a synced state between TrustRequest
1340 : // and convRequests.
1341 62 : if (auto* cm = convModule(true))
1342 62 : cm->acceptConversationRequest(conversationId, deviceId);
1343 62 : }
1344 :
1345 : void
1346 63 : JamiAccount::onContactConfirmed(const std::string& uri, const std::string& convFromReq)
1347 : {
1348 63 : dht::ThreadPool::io().run([w = weak(), convFromReq, uri] {
1349 63 : if (auto shared = w.lock()) {
1350 63 : shared->convModule(true);
1351 : // Remove cached payload if there is one
1352 63 : auto requestPath = shared->cachePath_ / "requests" / uri;
1353 63 : dhtnet::fileutils::remove(requestPath);
1354 63 : dhtnet::fileutils::remove(shared->cachePath_ / "requests" / (uri + ".ts"));
1355 126 : }
1356 63 : });
1357 63 : }
1358 :
1359 : std::unique_ptr<AccountManager::AccountCredentials>
1360 822 : JamiAccount::buildAccountCredentials(const JamiAccountConfig& conf,
1361 : const dht::crypto::Identity& id,
1362 : const std::string& archive_password_scheme,
1363 : const std::string& archive_password,
1364 : const std::string& archive_path,
1365 : bool& migrating,
1366 : bool& hasPassword)
1367 : {
1368 822 : std::unique_ptr<AccountManager::AccountCredentials> creds;
1369 :
1370 822 : if (conf.managerUri.empty()) {
1371 822 : auto acreds = std::make_unique<ArchiveAccountManager::ArchiveAccountCredentials>();
1372 822 : auto archivePath = fileutils::getFullPath(idPath_, conf.archivePath);
1373 :
1374 822 : if (!archive_path.empty()) {
1375 39 : acreds->scheme = "file";
1376 39 : acreds->uri = archive_path;
1377 783 : } else if (!conf.archive_url.empty() && conf.archive_url == "jami-auth") {
1378 5 : JAMI_DEBUG("[Account {}] [LinkDevice] scheme p2p & uri {}", getAccountID(), conf.archive_url);
1379 5 : acreds->scheme = "p2p";
1380 5 : acreds->uri = conf.archive_url;
1381 778 : } else if (std::filesystem::is_regular_file(archivePath)) {
1382 4 : acreds->scheme = "local";
1383 4 : acreds->uri = archivePath.string();
1384 4 : acreds->updateIdentity = id;
1385 4 : migrating = true;
1386 : }
1387 :
1388 822 : creds = std::move(acreds);
1389 822 : } else {
1390 0 : auto screds = std::make_unique<ServerAccountManager::ServerAccountCredentials>();
1391 0 : screds->username = conf.managerUsername;
1392 0 : screds->identity = id;
1393 0 : creds = std::move(screds);
1394 0 : }
1395 :
1396 822 : creds->password = archive_password;
1397 822 : hasPassword = !archive_password.empty();
1398 1634 : creds->password_scheme = (hasPassword && archive_password_scheme.empty()) ? fileutils::ARCHIVE_AUTH_SCHEME_PASSWORD
1399 1634 : : archive_password_scheme;
1400 :
1401 822 : return creds;
1402 0 : }
1403 :
1404 : void
1405 820 : JamiAccount::onAuthenticationSuccess(bool migrating,
1406 : bool hasPassword,
1407 : const AccountInfo& info,
1408 : const std::map<std::string, std::string>& configMap,
1409 : std::string&& receipt,
1410 : std::vector<uint8_t>&& receiptSignature)
1411 : {
1412 820 : JAMI_LOG("[Account {}] Auth success! Device: {}", getAccountID(), info.deviceId);
1413 :
1414 820 : dhtnet::fileutils::check_dir(idPath_, 0700);
1415 :
1416 820 : auto id = info.identity;
1417 1640 : editConfig([&](JamiAccountConfig& conf) {
1418 820 : std::tie(conf.tlsPrivateKeyFile, conf.tlsCertificateFile) = saveIdentity(id, idPath_, DEVICE_ID_PATH);
1419 820 : conf.tlsPassword = {};
1420 :
1421 1640 : auto passwordIt = configMap.find(libjami::Account::ConfProperties::ARCHIVE_HAS_PASSWORD);
1422 1640 : conf.archiveHasPassword = (passwordIt != configMap.end() && !passwordIt->second.empty())
1423 1640 : ? passwordIt->second == "true"
1424 0 : : hasPassword;
1425 :
1426 820 : if (not conf.managerUri.empty()) {
1427 0 : conf.registeredName = conf.managerUsername;
1428 0 : registeredName_ = conf.managerUsername;
1429 : }
1430 :
1431 820 : conf.username = info.accountId;
1432 820 : conf.deviceName = accountManager_->getAccountDeviceName();
1433 :
1434 1640 : auto nameServerIt = configMap.find(libjami::Account::ConfProperties::Nameserver::URI);
1435 820 : if (nameServerIt != configMap.end() && !nameServerIt->second.empty())
1436 0 : conf.nameServer = nameServerIt->second;
1437 :
1438 1640 : auto displayNameIt = configMap.find(libjami::Account::ConfProperties::DISPLAYNAME);
1439 820 : if (displayNameIt != configMap.end() && !displayNameIt->second.empty())
1440 42 : conf.displayName = displayNameIt->second;
1441 :
1442 820 : conf.receipt = std::move(receipt);
1443 820 : conf.receiptSignature = std::move(receiptSignature);
1444 820 : conf.fromMap(configMap);
1445 820 : });
1446 :
1447 820 : id_ = std::move(id);
1448 : {
1449 820 : std::lock_guard lk(moduleMtx_);
1450 820 : convModule_.reset();
1451 820 : }
1452 :
1453 820 : if (migrating)
1454 4 : Migration::setState(getAccountID(), Migration::State::SUCCESS);
1455 :
1456 820 : setRegistrationState(RegistrationState::UNREGISTERED);
1457 :
1458 820 : if (!info.photo.empty() || !info.displayName.empty()) {
1459 : try {
1460 0 : auto newProfile = vCard::utils::initVcard();
1461 0 : newProfile[std::string(vCard::Property::FORMATTED_NAME)] = info.displayName;
1462 0 : newProfile[std::string(vCard::Property::PHOTO)] = info.photo;
1463 :
1464 0 : const auto& profiles = idPath_ / "profiles";
1465 0 : const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(info.accountId));
1466 0 : vCard::utils::save(newProfile, vCardPath, profilePath());
1467 :
1468 0 : runOnMainThread([w = weak(), id = info.accountId, vCardPath] {
1469 0 : if (auto shared = w.lock()) {
1470 0 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(shared->getAccountID(),
1471 0 : id,
1472 0 : vCardPath.string());
1473 0 : }
1474 0 : });
1475 0 : } catch (const std::exception& e) {
1476 0 : JAMI_WARNING("[Account {}] Unable to save profile after authentication: {}", getAccountID(), e.what());
1477 0 : }
1478 : }
1479 :
1480 820 : updateTrustedCa();
1481 820 : doRegister();
1482 820 : scheduleAccountReady();
1483 820 : }
1484 :
1485 : void
1486 0 : JamiAccount::onAuthenticationError(const std::weak_ptr<JamiAccount>& w,
1487 : bool hadIdentity,
1488 : bool migrating,
1489 : std::string accountId,
1490 : AccountManager::AuthError error,
1491 : const std::string& message)
1492 : {
1493 0 : JAMI_WARNING("[Account {}] Auth error: {} {}", accountId, (int) error, message);
1494 :
1495 0 : if ((hadIdentity || migrating) && error == AccountManager::AuthError::INVALID_ARGUMENTS) {
1496 0 : Migration::setState(accountId, Migration::State::INVALID);
1497 0 : if (auto acc = w.lock())
1498 0 : acc->setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1499 0 : return;
1500 : }
1501 :
1502 0 : if (auto acc = w.lock())
1503 0 : acc->setRegistrationState(RegistrationState::ERROR_GENERIC);
1504 :
1505 0 : runOnMainThread([accountId = std::move(accountId)] { Manager::instance().removeAccount(accountId, true); });
1506 : }
1507 :
1508 : // must be called while configurationMutex_ is locked
1509 : void
1510 844 : JamiAccount::loadAccount(const std::string& archive_password_scheme,
1511 : const std::string& archive_password,
1512 : const std::string& archive_path)
1513 : {
1514 844 : if (registrationState_ == RegistrationState::INITIALIZING)
1515 22 : return;
1516 :
1517 840 : JAMI_DEBUG("[Account {:s}] Loading account", getAccountID());
1518 :
1519 840 : const auto scheduleAccountReady = [accountId = getAccountID()] {
1520 18 : runOnMainThread([accountId] {
1521 18 : auto& manager = Manager::instance();
1522 18 : manager.markAccountReady(accountId);
1523 18 : });
1524 858 : };
1525 :
1526 840 : const auto& conf = config();
1527 840 : auto callbacks = setupAccountCallbacks();
1528 :
1529 : try {
1530 840 : auto oldIdentity = id_.first ? id_.first->getPublicKey().getLongId() : DeviceId();
1531 :
1532 840 : if (conf.managerUri.empty()) {
1533 1680 : accountManager_ = std::make_shared<ArchiveAccountManager>(
1534 840 : getAccountID(),
1535 : getPath(),
1536 44 : [this]() { return getAccountDetails(); },
1537 840 : [this](DeviceSync&& syncData) {
1538 820 : if (auto* sm = syncModule()) {
1539 820 : auto syncDataPtr = std::make_shared<SyncMsg>();
1540 820 : syncDataPtr->ds = std::move(syncData);
1541 820 : sm->syncWithConnected(syncDataPtr);
1542 820 : }
1543 820 : },
1544 1680 : conf.archivePath.empty() ? "archive.gz" : conf.archivePath,
1545 1680 : conf.nameServer);
1546 : } else {
1547 0 : accountManager_ = std::make_shared<ServerAccountManager>(getAccountID(),
1548 : getPath(),
1549 0 : conf.managerUri,
1550 0 : conf.nameServer);
1551 : }
1552 :
1553 840 : auto id = accountManager_->loadIdentity(conf.tlsCertificateFile, conf.tlsPrivateKeyFile, conf.tlsPassword);
1554 :
1555 840 : if (const auto* info
1556 840 : = accountManager_->useIdentity(id, conf.receipt, conf.receiptSignature, conf.managerUsername, callbacks)) {
1557 18 : id_ = std::move(id);
1558 18 : config_->username = info->accountId;
1559 18 : JAMI_WARNING("[Account {:s}] Loaded account identity", getAccountID());
1560 :
1561 18 : if (info->identity.first->getPublicKey().getLongId() != oldIdentity) {
1562 0 : JAMI_WARNING("[Account {:s}] Identity changed", getAccountID());
1563 : {
1564 0 : std::lock_guard lk(moduleMtx_);
1565 0 : convModule_.reset();
1566 0 : }
1567 0 : convModule();
1568 : } else {
1569 18 : convModule()->setAccountManager(accountManager_);
1570 : }
1571 :
1572 18 : convModule()->initPresence();
1573 18 : if (not isEnabled())
1574 0 : setRegistrationState(RegistrationState::UNREGISTERED);
1575 :
1576 18 : updateTrustedCa();
1577 18 : scheduleAccountReady();
1578 18 : return;
1579 : }
1580 :
1581 822 : if (!isEnabled())
1582 0 : return;
1583 :
1584 822 : JAMI_WARNING("[Account {}] useIdentity failed!", getAccountID());
1585 :
1586 822 : if (not conf.managerUri.empty() && archive_password.empty()) {
1587 0 : Migration::setState(accountID_, Migration::State::INVALID);
1588 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
1589 0 : return;
1590 : }
1591 :
1592 822 : bool migrating = registrationState_ == RegistrationState::ERROR_NEED_MIGRATION;
1593 822 : setRegistrationState(RegistrationState::INITIALIZING);
1594 :
1595 822 : bool hasPassword = false;
1596 : auto creds = buildAccountCredentials(conf,
1597 : id,
1598 : archive_password_scheme,
1599 : archive_password,
1600 : archive_path,
1601 : migrating,
1602 822 : hasPassword);
1603 :
1604 822 : JAMI_WARNING("[Account {}] initAuthentication {}", getAccountID(), fmt::ptr(this));
1605 :
1606 822 : const bool hadIdentity = static_cast<bool>(id.first);
1607 3288 : accountManager_->initAuthentication(
1608 1644 : ip_utils::getDeviceName(),
1609 822 : std::move(creds),
1610 1644 : [w = weak(), migrating, hasPassword](const AccountInfo& info,
1611 : const std::map<std::string, std::string>& configMap,
1612 : std::string&& receipt,
1613 : std::vector<uint8_t>&& receiptSignature) {
1614 820 : if (auto self = w.lock())
1615 1640 : self->onAuthenticationSuccess(migrating,
1616 : hasPassword,
1617 : info,
1618 : configMap,
1619 820 : std::move(receipt),
1620 1640 : std::move(receiptSignature));
1621 820 : },
1622 1644 : [w = weak(), hadIdentity, accountId = getAccountID(), migrating](AccountManager::AuthError error,
1623 : const std::string& message) {
1624 0 : JamiAccount::onAuthenticationError(w, hadIdentity, migrating, accountId, error, message);
1625 0 : },
1626 : callbacks);
1627 840 : } catch (const std::exception& e) {
1628 0 : JAMI_WARNING("[Account {}] Error loading account: {}", getAccountID(), e.what());
1629 0 : accountManager_.reset();
1630 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
1631 0 : }
1632 858 : }
1633 :
1634 : std::map<std::string, std::string>
1635 4870 : JamiAccount::getVolatileAccountDetails() const
1636 : {
1637 4870 : auto a = SIPAccountBase::getVolatileAccountDetails();
1638 4870 : a.emplace(libjami::Account::VolatileProperties::InstantMessaging::OFF_CALL, TRUE_STR);
1639 4870 : auto registeredName = getRegisteredName();
1640 4870 : if (not registeredName.empty())
1641 3 : a.emplace(libjami::Account::VolatileProperties::REGISTERED_NAME, registeredName);
1642 4870 : a.emplace(libjami::Account::ConfProperties::PROXY_SERVER, proxyServerCached_);
1643 4870 : a.emplace(libjami::Account::VolatileProperties::DHT_BOUND_PORT, std::to_string(dhtBoundPort_));
1644 4870 : a.emplace(libjami::Account::VolatileProperties::DEVICE_ANNOUNCED, deviceAnnounced_ ? TRUE_STR : FALSE_STR);
1645 4870 : if (accountManager_) {
1646 4870 : if (const auto* info = accountManager_->getInfo()) {
1647 4006 : a.emplace(libjami::Account::ConfProperties::DEVICE_ID, info->deviceId);
1648 : }
1649 : }
1650 9740 : return a;
1651 4870 : }
1652 :
1653 : void
1654 3 : JamiAccount::lookupName(const std::string& name)
1655 : {
1656 3 : std::lock_guard lock(configurationMutex_);
1657 3 : auto acc = getAccountID();
1658 3 : if (!accountManager_) {
1659 0 : JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, name);
1660 0 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
1661 : name,
1662 : (int) NameDirectory::Response::error,
1663 : "",
1664 : "");
1665 0 : return;
1666 : }
1667 6 : accountManager_->lookupUri(name,
1668 3 : config().nameServer,
1669 6 : [acc, name](const std::string& regName,
1670 : const std::string& address,
1671 : NameDirectory::Response response) {
1672 6 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
1673 3 : name,
1674 : (int) response,
1675 : address,
1676 : regName);
1677 3 : });
1678 3 : }
1679 :
1680 : void
1681 3 : JamiAccount::lookupAddress(const std::string& addr)
1682 : {
1683 3 : std::lock_guard lock(configurationMutex_);
1684 3 : auto acc = getAccountID();
1685 3 : if (!accountManager_) {
1686 0 : JAMI_WARNING("[Account {}] Unable to look up {}: account is not loaded", acc, addr);
1687 0 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc,
1688 : addr,
1689 : (int) NameDirectory::Response::error,
1690 : "",
1691 : "");
1692 0 : return;
1693 : }
1694 6 : accountManager_->lookupAddress(
1695 6 : addr, [acc, addr](const std::string& regName, const std::string& address, NameDirectory::Response response) {
1696 3 : emitSignal<libjami::ConfigurationSignal::RegisteredNameFound>(acc, addr, (int) response, address, regName);
1697 3 : });
1698 3 : }
1699 :
1700 : void
1701 1 : JamiAccount::registerName(const std::string& name, const std::string& scheme, const std::string& password)
1702 : {
1703 1 : std::lock_guard lock(configurationMutex_);
1704 1 : if (accountManager_)
1705 1 : accountManager_
1706 2 : ->registerName(name,
1707 : scheme,
1708 : password,
1709 2 : [acc = getAccountID(), name, w = weak()](NameDirectory::RegistrationResponse response,
1710 : const std::string& regName) {
1711 1 : auto res = (int) std::min(response, NameDirectory::RegistrationResponse::error);
1712 1 : if (response == NameDirectory::RegistrationResponse::success) {
1713 1 : if (auto this_ = w.lock()) {
1714 1 : if (this_->setRegisteredName(regName)) {
1715 2 : this_->editConfig(
1716 2 : [&](JamiAccountConfig& config) { config.registeredName = regName; });
1717 1 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(
1718 2 : this_->accountID_, this_->getVolatileAccountDetails());
1719 : }
1720 1 : }
1721 : }
1722 1 : emitSignal<libjami::ConfigurationSignal::NameRegistrationEnded>(acc, res, name);
1723 1 : });
1724 1 : }
1725 :
1726 : bool
1727 0 : JamiAccount::searchUser(const std::string& query)
1728 : {
1729 0 : if (accountManager_)
1730 0 : return accountManager_
1731 0 : ->searchUser(query,
1732 0 : [acc = getAccountID(), query](const jami::NameDirectory::SearchResult& result,
1733 : jami::NameDirectory::Response response) {
1734 0 : jami::emitSignal<libjami::ConfigurationSignal::UserSearchEnded>(acc,
1735 : (int) response,
1736 0 : query,
1737 : result);
1738 0 : });
1739 0 : return false;
1740 : }
1741 :
1742 : void
1743 186 : JamiAccount::forEachPendingCall(const DeviceId& deviceId, const std::function<void(const std::shared_ptr<SIPCall>&)>& cb)
1744 : {
1745 186 : std::vector<std::shared_ptr<SIPCall>> pc;
1746 : {
1747 186 : std::lock_guard lk(pendingCallsMutex_);
1748 186 : pc = std::move(pendingCalls_[deviceId]);
1749 186 : }
1750 278 : for (const auto& pendingCall : pc) {
1751 92 : cb(pendingCall);
1752 : }
1753 186 : }
1754 :
1755 : void
1756 746 : JamiAccount::registerAsyncOps()
1757 : {
1758 746 : loadCachedProxyServer([w = weak()](const std::string&) {
1759 746 : runOnMainThread([w] {
1760 746 : if (auto s = w.lock()) {
1761 746 : std::lock_guard lock(s->configurationMutex_);
1762 746 : s->doRegister_();
1763 1492 : }
1764 746 : });
1765 746 : });
1766 746 : }
1767 :
1768 : void
1769 1698 : JamiAccount::doRegister()
1770 : {
1771 1698 : std::lock_guard lock(configurationMutex_);
1772 1698 : if (not isUsable()) {
1773 134 : JAMI_WARNING("[Account {:s}] Account must be enabled and active to register, ignoring", getAccountID());
1774 134 : return;
1775 : }
1776 :
1777 1564 : JAMI_LOG("[Account {:s}] Starting account…", getAccountID());
1778 :
1779 : // invalid state transitions:
1780 : // INITIALIZING: generating/loading certificates, unable to register
1781 : // NEED_MIGRATION: old account detected, user needs to migrate
1782 1564 : if (registrationState_ == RegistrationState::INITIALIZING
1783 746 : || registrationState_ == RegistrationState::ERROR_NEED_MIGRATION)
1784 818 : return;
1785 :
1786 746 : convModule(); // Init conv module before passing in trying
1787 746 : setRegistrationState(RegistrationState::TRYING);
1788 746 : if (proxyServerCached_.empty()) {
1789 746 : registerAsyncOps();
1790 : } else {
1791 0 : doRegister_();
1792 : }
1793 1698 : }
1794 :
1795 : std::vector<std::string>
1796 746 : JamiAccount::loadBootstrap() const
1797 : {
1798 746 : std::vector<std::string> bootstrap;
1799 746 : std::string_view stream(config().hostname), node_addr;
1800 1492 : while (jami::getline(stream, node_addr, ';'))
1801 746 : bootstrap.emplace_back(node_addr);
1802 1492 : for (const auto& b : bootstrap)
1803 746 : JAMI_LOG("[Account {}] Bootstrap node: {}", getAccountID(), b);
1804 1492 : return bootstrap;
1805 0 : }
1806 :
1807 : void
1808 31 : JamiAccount::trackBuddyPresence(const std::string& buddy_id, bool track)
1809 : {
1810 31 : std::string buddyUri;
1811 : try {
1812 31 : buddyUri = parseJamiUri(buddy_id);
1813 0 : } catch (...) {
1814 0 : JAMI_ERROR("[Account {:s}] Failed to track presence: invalid URI {:s}", getAccountID(), buddy_id);
1815 0 : return;
1816 0 : }
1817 31 : JAMI_LOG("[Account {:s}] {:s} presence for {:s}", getAccountID(), track ? "Track" : "Untrack", buddy_id);
1818 :
1819 31 : if (!presenceManager_)
1820 0 : return;
1821 :
1822 31 : if (track) {
1823 31 : presenceManager_->trackBuddy(buddyUri);
1824 31 : std::lock_guard lock(presenceStateMtx_);
1825 31 : auto it = presenceState_.find(buddyUri);
1826 31 : if (it != presenceState_.end() && it->second != PresenceState::DISCONNECTED) {
1827 1 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1828 : buddyUri,
1829 1 : static_cast<int>(it->second),
1830 : "");
1831 : }
1832 31 : } else {
1833 0 : presenceManager_->untrackBuddy(buddyUri);
1834 : }
1835 31 : }
1836 :
1837 : std::map<std::string, bool>
1838 2 : JamiAccount::getTrackedBuddyPresence() const
1839 : {
1840 2 : if (!presenceManager_)
1841 0 : return {};
1842 2 : return presenceManager_->getTrackedBuddyPresence();
1843 : }
1844 :
1845 : void
1846 601 : JamiAccount::onTrackedBuddyOnline(const std::string& contactId)
1847 : {
1848 601 : JAMI_DEBUG("[Account {:s}] Buddy {} online", getAccountID(), contactId);
1849 601 : std::lock_guard lock(presenceStateMtx_);
1850 601 : auto& state = presenceState_[contactId];
1851 601 : if (state < PresenceState::AVAILABLE) {
1852 399 : state = PresenceState::AVAILABLE;
1853 399 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1854 : contactId,
1855 : static_cast<int>(PresenceState::AVAILABLE),
1856 : "");
1857 : }
1858 :
1859 601 : if (auto details = getContactInfo(contactId)) {
1860 75 : if (!details->confirmed) {
1861 33 : auto convId = convModule()->getOneToOneConversation(contactId);
1862 33 : if (convId.empty())
1863 1 : return;
1864 : // In this case, the TrustRequest was sent but never confirmed (cause the contact was
1865 : // offline maybe) To avoid the contact to never receive the conv request, retry there
1866 32 : std::lock_guard lock(configurationMutex_);
1867 32 : if (accountManager_) {
1868 : // Retrieve cached payload for trust request.
1869 32 : auto requestPath = cachePath_ / "requests" / contactId;
1870 32 : std::vector<uint8_t> payload;
1871 : try {
1872 38 : payload = fileutils::loadFile(requestPath);
1873 6 : } catch (...) {
1874 6 : }
1875 32 : if (payload.size() >= 64000) {
1876 0 : JAMI_WARNING("[Account {:s}] Trust request for contact {:s} is too big, reset payload",
1877 : getAccountID(),
1878 : contactId);
1879 0 : payload.clear();
1880 : }
1881 : // Reuse the original invite timestamp so this passive retry isn't treated as a
1882 : // fresh invitation; fall back to now() if no cached timestamp is found (e.g.
1883 : // cache predates this mechanism, or the sidecar failed to write previously).
1884 32 : auto tsPath = cachePath_ / "requests" / (contactId + ".ts");
1885 32 : TimePoint invited {};
1886 : try {
1887 38 : auto tsData = fileutils::loadFile(tsPath);
1888 26 : auto invitedMs = std::stoll(std::string(tsData.begin(), tsData.end()));
1889 26 : if (invitedMs > 0)
1890 26 : invited = timePointFromMilliseconds(invitedMs);
1891 32 : } catch (const std::exception& e) {
1892 6 : JAMI_WARNING("[Account {:s}] Unable to load invite timestamp for {:s}: {}",
1893 : getAccountID(),
1894 : contactId,
1895 : e.what());
1896 6 : }
1897 32 : if (invited == TimePoint {}) {
1898 6 : invited = nowMs();
1899 : // Persist the fallback so subsequent retries stay stable instead of minting
1900 : // a new timestamp (and thus looking like a fresh re-invitation) every time.
1901 6 : std::ofstream tsFile(tsPath, std::ios::trunc | std::ios::binary);
1902 6 : if (tsFile.is_open())
1903 0 : tsFile << toMillisecondsSinceEpoch(invited);
1904 : else
1905 6 : JAMI_WARNING("[Account {:s}] Unable to write invite timestamp for {:s}",
1906 : getAccountID(),
1907 : contactId);
1908 6 : }
1909 32 : accountManager_->sendTrustRequest(contactId, convId, payload, invited);
1910 32 : }
1911 33 : }
1912 601 : }
1913 601 : }
1914 :
1915 : void
1916 55 : JamiAccount::onTrackedBuddyOffline(const std::string& contactId)
1917 : {
1918 55 : JAMI_DEBUG("[Account {:s}] Buddy {} offline", getAccountID(), contactId);
1919 55 : std::lock_guard lock(presenceStateMtx_);
1920 55 : auto& state = presenceState_[contactId];
1921 55 : if (state > PresenceState::DISCONNECTED) {
1922 55 : if (state == PresenceState::CONNECTED) {
1923 0 : JAMI_WARNING("[Account {:s}] Buddy {} is not present on the DHT, but P2P connected",
1924 : getAccountID(),
1925 : contactId);
1926 0 : return;
1927 : }
1928 55 : state = PresenceState::DISCONNECTED;
1929 55 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
1930 : contactId,
1931 : static_cast<int>(PresenceState::DISCONNECTED),
1932 : "");
1933 : }
1934 55 : }
1935 :
1936 : void
1937 746 : JamiAccount::doRegister_()
1938 : {
1939 746 : if (registrationState_ != RegistrationState::TRYING) {
1940 0 : JAMI_ERROR("[Account {}] Already registered", getAccountID());
1941 0 : return;
1942 : }
1943 :
1944 746 : JAMI_DEBUG("[Account {}] Starting account…", getAccountID());
1945 746 : const auto& conf = config();
1946 :
1947 : try {
1948 746 : if (not accountManager_ or not accountManager_->getInfo())
1949 0 : throw std::runtime_error("No identity configured for this account.");
1950 :
1951 746 : if (dht_->isRunning()) {
1952 2 : JAMI_ERROR("[Account {}] DHT already running (stopping it first).", getAccountID());
1953 2 : dht_->join();
1954 : }
1955 :
1956 746 : convModule()->clearPendingFetch();
1957 :
1958 : // Look for registered name
1959 1492 : accountManager_->lookupAddress(accountManager_->getInfo()->accountId,
1960 1492 : [w = weak()](const std::string& regName,
1961 : const std::string& /*address*/,
1962 : const NameDirectory::Response& response) {
1963 746 : if (auto this_ = w.lock())
1964 746 : this_->lookupRegisteredName(regName, response);
1965 746 : });
1966 :
1967 746 : dht::DhtRunner::Config config = initDhtConfig(conf);
1968 :
1969 : // check if dht peer service is enabled
1970 746 : if (conf.accountPeerDiscovery or conf.accountPublish) {
1971 0 : peerDiscovery_ = std::make_shared<dht::PeerDiscovery>();
1972 0 : if (conf.accountPeerDiscovery) {
1973 0 : JAMI_LOG("[Account {}] Starting Jami account discovery…", getAccountID());
1974 0 : startAccountDiscovery();
1975 : }
1976 0 : if (conf.accountPublish)
1977 0 : startAccountPublish();
1978 : }
1979 :
1980 746 : dht::DhtRunner::Context context = initDhtContext();
1981 :
1982 746 : accountManager_->setDht(dht_);
1983 746 : dht_->run(conf.dhtPort, config, std::move(context));
1984 :
1985 746 : dhtBoundPort_ = dht_->getBoundPort();
1986 :
1987 : // Now that the DHT is running and we know the actual bound port,
1988 : // request a UPnP mapping for it. A bound port of 0 means the DHT has no
1989 : // socket to forward (proxy-only mode): requesting a mapping would make the
1990 : // UPnP context allocate an arbitrary port that nothing listens on.
1991 746 : if (upnpCtrl_ and dhtUpnpMapping_.getInternalPort() != dhtBoundPort_) {
1992 680 : releaseDhtUpnpMapping();
1993 :
1994 680 : if (dhtBoundPort_ != 0) {
1995 680 : JAMI_LOG("[Account {:s}] UPnP: requesting mapping for DHT port {}", getAccountID(), dhtBoundPort_);
1996 :
1997 680 : dhtUpnpMapping_.enableAutoUpdate(true);
1998 :
1999 680 : dhtnet::upnp::Mapping desired(dhtnet::upnp::PortType::UDP, dhtBoundPort_, dhtBoundPort_);
2000 680 : dhtUpnpMapping_.updateFrom(desired);
2001 :
2002 680 : dhtUpnpMapping_.setNotifyCallback([w = weak()](const dhtnet::upnp::Mapping::sharedPtr_t& mapRes) {
2003 694 : if (auto accPtr = w.lock()) {
2004 694 : auto& dhtMap = accPtr->dhtUpnpMapping_;
2005 694 : const auto& accId = accPtr->getAccountID();
2006 :
2007 694 : JAMI_LOG("[Account {:s}] DHT UPnP mapping changed to {:s}", accId, mapRes->toString(true));
2008 :
2009 694 : if (dhtMap.getMapKey() != mapRes->getMapKey() or dhtMap.getState() != mapRes->getState()) {
2010 673 : dhtMap.updateFrom(mapRes);
2011 673 : if (mapRes->getState() == dhtnet::upnp::MappingState::OPEN) {
2012 0 : JAMI_LOG("[Account {:s}] Mapping {:s} successfully allocated", accId, dhtMap.toString());
2013 0 : accPtr->dht_->connectivityChanged();
2014 673 : } else if (mapRes->getState() == dhtnet::upnp::MappingState::FAILED) {
2015 673 : JAMI_WARNING("[Account {:s}] UPnP mapping failed", accId);
2016 : }
2017 : } else {
2018 21 : dhtMap.updateFrom(mapRes);
2019 : }
2020 694 : }
2021 694 : });
2022 :
2023 680 : upnpCtrl_->reserveMapping(dhtUpnpMapping_);
2024 680 : }
2025 : }
2026 :
2027 1492 : for (const auto& bootstrap : loadBootstrap())
2028 1492 : dht_->bootstrap(bootstrap);
2029 :
2030 746 : if (conf.dhtProxyServerEnabled) {
2031 0 : dht::ProxyServerConfig proxyConfig;
2032 0 : proxyConfig.port = conf.dhtProxyServerPort;
2033 0 : proxyConfig.identity = id_;
2034 0 : dhtProxyServer_ = std::make_shared<dht::DhtProxyServer>(dht_, proxyConfig);
2035 0 : } else {
2036 746 : dhtProxyServer_.reset();
2037 : }
2038 :
2039 746 : std::unique_lock lkCM(connManagerMtx_);
2040 746 : initConnectionManager();
2041 746 : connectionManager_->dhtStarted();
2042 1538 : connectionManager_->onICERequest([this](const DeviceId& deviceId) { return onICERequest(deviceId); });
2043 746 : connectionManager_->onChannelRequest([this](const std::shared_ptr<dht::crypto::Certificate>& cert,
2044 4446 : const std::string& name) { return onChannelRequest(cert, name); });
2045 1492 : connectionManager_->onNewDeviceConnection(
2046 2007 : [this](const std::shared_ptr<dht::crypto::Certificate>& cert) { onNewDeviceConnection(cert); });
2047 1492 : connectionManager_->onConnectionReady(
2048 746 : [this](const DeviceId& deviceId, const std::string& name, std::shared_ptr<dhtnet::ChannelSocket> channel) {
2049 8670 : onConnectionReady(deviceId, name, std::move(channel));
2050 8672 : });
2051 746 : lkCM.unlock();
2052 :
2053 746 : if (!conf.managerUri.empty() && accountManager_) {
2054 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())->onDeviceRevoked([this]() {
2055 0 : JAMI_WARNING("[Account {}] Device revoked by server, deleting identity", getAccountID());
2056 0 : editConfig([&](JamiAccountConfig& conf) {
2057 : // Delete the revoked device's key and certificate files
2058 0 : std::error_code ec;
2059 0 : if (!conf.tlsPrivateKeyFile.empty())
2060 0 : std::filesystem::remove(idPath_ / conf.tlsPrivateKeyFile, ec);
2061 0 : if (!conf.tlsCertificateFile.empty())
2062 0 : std::filesystem::remove(idPath_ / conf.tlsCertificateFile, ec);
2063 0 : conf.tlsPrivateKeyFile.clear();
2064 0 : conf.tlsCertificateFile.clear();
2065 0 : conf.receipt.clear();
2066 0 : conf.receiptSignature.clear();
2067 0 : });
2068 0 : Migration::setState(accountID_, Migration::State::INVALID);
2069 0 : setRegistrationState(RegistrationState::ERROR_NEED_MIGRATION);
2070 0 : });
2071 0 : dynamic_cast<ServerAccountManager*>(accountManager_.get())
2072 0 : ->syncBlueprintConfig([this](const std::map<std::string, std::string>& config) {
2073 0 : editConfig([&](JamiAccountConfig& conf) { conf.fromMap(config); });
2074 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
2075 0 : });
2076 : }
2077 :
2078 746 : if (presenceManager_)
2079 746 : presenceManager_->refresh();
2080 746 : } catch (const std::exception& e) {
2081 0 : JAMI_ERROR("Error registering DHT account: {}", e.what());
2082 0 : setRegistrationState(RegistrationState::ERROR_GENERIC);
2083 0 : }
2084 : }
2085 :
2086 : void
2087 739 : JamiAccount::lookupRegisteredName(const std::string& regName, const NameDirectory::Response& response)
2088 : {
2089 739 : if (response == NameDirectory::Response::found or response == NameDirectory::Response::notFound) {
2090 1478 : const auto& nameResult = response == NameDirectory::Response::found ? regName : "";
2091 739 : if (setRegisteredName(nameResult)) {
2092 0 : editConfig([&](JamiAccountConfig& config) { config.registeredName = nameResult; });
2093 0 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
2094 : }
2095 739 : }
2096 739 : }
2097 :
2098 : dht::DhtRunner::Config
2099 746 : JamiAccount::initDhtConfig(const JamiAccountConfig& conf)
2100 : {
2101 746 : dht::DhtRunner::Config config {};
2102 746 : config.dht_config.node_config.network = 0;
2103 746 : config.dht_config.node_config.maintain_storage = false;
2104 746 : config.dht_config.node_config.persist_path = (cachePath_ / "dhtstate").string();
2105 746 : config.dht_config.id = id_;
2106 746 : config.dht_config.cert_cache_all = true;
2107 746 : config.push_node_id = getAccountID();
2108 746 : config.push_token = conf.deviceKey;
2109 746 : config.push_topic = conf.notificationTopic;
2110 746 : config.push_platform = conf.platform;
2111 746 : config.proxy_user_agent = jami::userAgent();
2112 746 : config.threaded = true;
2113 746 : config.peer_discovery = conf.dhtPeerDiscovery;
2114 746 : config.peer_publish = conf.dhtPeerDiscovery;
2115 746 : if (conf.proxyEnabled)
2116 0 : config.proxy_server = proxyServerCached_;
2117 :
2118 746 : if (not config.proxy_server.empty()) {
2119 0 : JAMI_LOG("[Account {}] Using proxy server {}", getAccountID(), config.proxy_server);
2120 0 : if (not config.push_token.empty()) {
2121 0 : JAMI_LOG("[Account {}] using push notifications with platform: {}, topic: {}, token: {}",
2122 : getAccountID(),
2123 : config.push_platform,
2124 : config.push_topic,
2125 : config.push_token);
2126 : }
2127 : }
2128 746 : return config;
2129 0 : }
2130 :
2131 : dht::DhtRunner::Context
2132 746 : JamiAccount::initDhtContext()
2133 : {
2134 746 : dht::DhtRunner::Context context {};
2135 746 : context.peerDiscovery = peerDiscovery_;
2136 746 : context.rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
2137 :
2138 746 : auto dht_log_level = Manager::instance().dhtLogLevel;
2139 746 : if (dht_log_level > 0) {
2140 0 : context.logger = logger_;
2141 : }
2142 :
2143 4094 : context.certificateStore = [&](const DeviceId& pk_id) {
2144 2602 : std::vector<std::shared_ptr<dht::crypto::Certificate>> ret;
2145 2602 : if (auto cert = certStore().getCertificate(pk_id.toString()))
2146 2602 : ret.emplace_back(std::move(cert));
2147 2602 : JAMI_LOG("[Account {}] Query for local certificate store: {}: {} found.",
2148 : getAccountID(),
2149 : pk_id.toString(),
2150 : ret.size());
2151 2602 : return ret;
2152 746 : };
2153 :
2154 3693 : context.statusChangedCallback = [this](dht::NodeStatus s4, dht::NodeStatus s6) {
2155 2201 : JAMI_LOG("[Account {}] DHT status: IPv4 {}; IPv6 {}", getAccountID(), dhtStatusStr(s4), dhtStatusStr(s6));
2156 : RegistrationState state;
2157 2201 : auto newStatus = std::max(s4, s6);
2158 2201 : switch (newStatus) {
2159 725 : case dht::NodeStatus::Connecting:
2160 725 : state = RegistrationState::TRYING;
2161 725 : break;
2162 1476 : case dht::NodeStatus::Connected:
2163 1476 : state = RegistrationState::REGISTERED;
2164 1476 : break;
2165 0 : case dht::NodeStatus::Disconnected:
2166 0 : state = RegistrationState::UNREGISTERED;
2167 0 : break;
2168 0 : default:
2169 0 : state = RegistrationState::ERROR_GENERIC;
2170 0 : break;
2171 : }
2172 :
2173 2201 : setRegistrationState(state);
2174 2947 : };
2175 :
2176 2230 : context.identityAnnouncedCb = [this](bool ok) {
2177 738 : if (!ok) {
2178 13 : JAMI_ERROR("[Account {}] Identity announcement failed", getAccountID());
2179 13 : return;
2180 : }
2181 725 : JAMI_WARNING("[Account {}] Identity announcement succeeded", getAccountID());
2182 725 : accountManager_
2183 2259 : ->startSync([this](const std::shared_ptr<dht::crypto::Certificate>& crt) { onAccountDeviceFound(crt); },
2184 1450 : [this] { onAccountDeviceAnnounced(); },
2185 725 : publishPresence_);
2186 746 : };
2187 :
2188 746 : return context;
2189 0 : }
2190 :
2191 : void
2192 809 : JamiAccount::onAccountDeviceFound(const std::shared_ptr<dht::crypto::Certificate>& crt)
2193 : {
2194 809 : if (jami::Manager::instance().syncOnRegister) {
2195 809 : if (!crt)
2196 0 : return;
2197 809 : auto deviceId = crt->getLongId().toString();
2198 809 : if (accountManager_->getInfo()->deviceId == deviceId)
2199 736 : return;
2200 :
2201 73 : dht::ThreadPool::io().run([w = weak(), crt] {
2202 73 : auto shared = w.lock();
2203 73 : if (!shared)
2204 0 : return;
2205 : // Only establish a sync connection if this device may be missing a
2206 : // local contact/conversation-list change. This avoids waking up
2207 : // devices (especially mobiles) when there is nothing new to sync.
2208 73 : if (auto* sm = shared->syncModule()) {
2209 73 : if (!sm->needsSync(crt->getLongId())) {
2210 3 : JAMI_DEBUG("[Account {}] [device {}] up to date, skipping sync connection",
2211 : shared->getAccountID(),
2212 : crt->getLongId());
2213 3 : return;
2214 : }
2215 : }
2216 : // Initiate a message connection to create the first TCP link.
2217 : // Once established, onNewDeviceConnection will set up sync and
2218 : // swarm channels.
2219 70 : shared->connectSyncDevice(crt->getLongId());
2220 73 : });
2221 809 : }
2222 : }
2223 :
2224 : void
2225 1118 : JamiAccount::connectSyncDevice(const DeviceId& deviceId)
2226 : {
2227 1118 : requestMessageConnection(getUsername(), deviceId, "sync");
2228 1118 : }
2229 :
2230 : void
2231 617 : JamiAccount::onSyncListChanged()
2232 : {
2233 617 : if (!jami::Manager::instance().syncOnRegister)
2234 0 : return;
2235 : // Coalesce bursts of changes (e.g. initial sync delivering many contacts
2236 : // and conversations) into a single propagation pass. A single version bump
2237 : // already marks every device out of date, and sync is full-state, so
2238 : // collapsing many changes into one pass is also semantically correct.
2239 617 : std::lock_guard lk(syncListChangedMtx_);
2240 617 : if (!syncListChangedTimer_)
2241 367 : syncListChangedTimer_ = std::make_shared<asio::steady_timer>(*jami::Manager::instance().ioContext());
2242 617 : syncListChangedTimer_->expires_after(std::chrono::seconds(1));
2243 617 : syncListChangedTimer_->async_wait([w = weak()](const std::error_code& ec) {
2244 617 : if (ec) // cancelled by a more recent change (debounce) or shutting down
2245 264 : return;
2246 353 : dht::ThreadPool::io().run([w] {
2247 353 : auto shared = w.lock();
2248 353 : if (!shared)
2249 0 : return;
2250 353 : auto* sm = shared->syncModule();
2251 353 : if (!sm)
2252 0 : return;
2253 : // A list change makes every device potentially out of date.
2254 353 : sm->bumpVersion();
2255 : // (Re)connect to the account's other devices that are not up to
2256 : // date so the change is pushed. Offline ones are reached on their
2257 : // next presence announcement (onAccountDeviceFound).
2258 353 : auto am = shared->accountManager();
2259 353 : if (am && am->getInfo()) {
2260 353 : auto currentDevice = shared->currentDeviceId();
2261 1754 : for (const auto& [deviceId, device] : am->getKnownDevices()) {
2262 1401 : if (deviceId.toString() == currentDevice)
2263 353 : continue;
2264 1048 : if (sm->needsSync(deviceId))
2265 1048 : shared->connectSyncDevice(deviceId);
2266 : }
2267 : }
2268 : // Push immediately to already-connected devices.
2269 353 : sm->syncWithConnected();
2270 353 : });
2271 : });
2272 617 : }
2273 :
2274 : void
2275 725 : JamiAccount::onAccountDeviceAnnounced()
2276 : {
2277 725 : if (jami::Manager::instance().syncOnRegister) {
2278 725 : deviceAnnounced_ = true;
2279 :
2280 : // Bootstrap at the end to avoid to be long to load.
2281 725 : dht::ThreadPool::io().run([w = weak()] {
2282 725 : if (auto shared = w.lock())
2283 2175 : shared->convModule()->bootstrap();
2284 725 : });
2285 725 : emitSignal<libjami::ConfigurationSignal::VolatileDetailsChanged>(accountID_, getVolatileAccountDetails());
2286 : }
2287 725 : }
2288 :
2289 : void
2290 1261 : JamiAccount::onNewDeviceConnection(const std::shared_ptr<dht::crypto::Certificate>& cert)
2291 : {
2292 1261 : if (!cert || !cert->issuer)
2293 0 : return;
2294 :
2295 1261 : dht::ThreadPool::io().run([w = weak(), cert] {
2296 1261 : auto shared = w.lock();
2297 1261 : if (!shared)
2298 0 : return;
2299 :
2300 1261 : JAMI_WARNING("[Account {}] New device connection: {}", shared->getAccountID(), cert->getLongId());
2301 :
2302 1261 : const auto peerId = cert->issuer->getId().toString();
2303 1261 : const auto deviceId = cert->getLongId();
2304 1261 : auto am = shared->accountManager();
2305 1261 : if (!am || am->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2306 2 : return;
2307 : }
2308 :
2309 1259 : const auto isSyncDevice = jami::Manager::instance().syncOnRegister && peerId == shared->getUsername();
2310 2518 : shared->requestMessageConnection(peerId, deviceId, isSyncDevice ? "sync" : "");
2311 :
2312 1259 : if (isSyncDevice) {
2313 70 : auto* sm = shared->syncModule();
2314 70 : if (sm && !sm->isConnected(deviceId)) {
2315 69 : std::shared_lock lk(shared->connManagerMtx_);
2316 69 : if (!shared->connectionManager_)
2317 0 : return;
2318 :
2319 69 : auto it = shared->channelHandlers_.find(Uri::Scheme::SYNC);
2320 69 : if (it != shared->channelHandlers_.end() && it->second)
2321 344 : it->second->connect(deviceId,
2322 : "",
2323 69 : [](const std::shared_ptr<dhtnet::ChannelSocket>& /*socket*/,
2324 69 : const DeviceId& /*deviceId*/) {});
2325 69 : }
2326 : }
2327 :
2328 : // Notify the DRT in all conversations where this peer is a member,
2329 : // so it can decide whether to open a swarm channel over the new connection.
2330 1259 : if (auto* cm = shared->convModule())
2331 1259 : cm->addKnownDevice(peerId, deviceId);
2332 :
2333 : // Proactively refresh the service cache for this device.
2334 : {
2335 1258 : std::shared_lock lk(shared->connManagerMtx_);
2336 1258 : auto it = shared->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
2337 1259 : if (it != shared->channelHandlers_.end() && it->second) {
2338 1259 : static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->refreshDevice(peerId, deviceId);
2339 : }
2340 1258 : }
2341 1265 : });
2342 : }
2343 :
2344 : void
2345 838 : JamiAccount::updateTrustedCa()
2346 : {
2347 838 : if (!accountManager_)
2348 0 : return;
2349 838 : const auto* info = accountManager_->getInfo();
2350 838 : if (!info || !info->identity.second)
2351 0 : return;
2352 :
2353 838 : auto accountCert = info->identity.second->issuer;
2354 838 : if (!accountCert)
2355 0 : return;
2356 838 : auto caCert = accountCert->issuer;
2357 838 : if (!caCert)
2358 0 : return;
2359 :
2360 838 : auto status = config().allowPeersFromTrusted ? dhtnet::tls::TrustStore::PermissionStatus::ALLOWED
2361 838 : : dhtnet::tls::TrustStore::PermissionStatus::UNDEFINED;
2362 838 : JAMI_LOG("[Account {}] {} organization CA {}",
2363 : getAccountID(),
2364 : config().allowPeersFromTrusted ? "Trusting" : "Untrusting",
2365 : caCert->getLongId());
2366 838 : setCertificateStatus(caCert, status, false);
2367 838 : }
2368 :
2369 : bool
2370 792 : JamiAccount::onICERequest(const DeviceId& deviceId)
2371 : {
2372 792 : std::promise<bool> accept;
2373 792 : std::future<bool> fut = accept.get_future();
2374 792 : accountManager_->findCertificate(deviceId, [this, &accept](const std::shared_ptr<dht::crypto::Certificate>& cert) {
2375 792 : if (!cert) {
2376 0 : accept.set_value(false);
2377 0 : return;
2378 : }
2379 792 : dht::InfoHash peer_account_id;
2380 792 : auto res = accountManager_->onPeerCertificate(cert, this->config().allowPublicIncoming, peer_account_id);
2381 792 : JAMI_LOG("[Account {}] [device {}] {} ICE request from {}",
2382 : getAccountID(),
2383 : cert->getLongId(),
2384 : res ? "Accepting" : "Discarding",
2385 : peer_account_id);
2386 792 : accept.set_value(res);
2387 : });
2388 792 : fut.wait();
2389 792 : auto result = fut.get();
2390 792 : return result;
2391 792 : }
2392 :
2393 : bool
2394 4449 : JamiAccount::onChannelRequest(const std::shared_ptr<dht::crypto::Certificate>& cert, const std::string& name)
2395 : {
2396 4449 : JAMI_LOG("[Account {}] [device {}] New channel requested: '{}'", getAccountID(), cert->getLongId(), name);
2397 :
2398 4458 : if (this->config().turnEnabled && turnCache_) {
2399 4454 : auto addr = turnCache_->getResolvedTurn();
2400 4458 : if (addr == std::nullopt) {
2401 : // If TURN is enabled, but no TURN cached, there can be a temporary
2402 : // resolution error to solve. Sometimes, a connectivity change is not
2403 : // enough, so even if this case is really rare, it should be easy to avoid.
2404 2 : turnCache_->refresh();
2405 : }
2406 : }
2407 :
2408 4457 : auto uri = Uri(name);
2409 4450 : std::shared_lock lk(connManagerMtx_);
2410 4450 : auto itHandler = channelHandlers_.find(uri.scheme());
2411 4449 : if (itHandler != channelHandlers_.end() && itHandler->second)
2412 4337 : return itHandler->second->onRequest(cert, name);
2413 112 : return name == "sip";
2414 4452 : }
2415 :
2416 : void
2417 20 : JamiAccount::connectYdocDevice(const DeviceId& deviceId, const std::string& documentId)
2418 : {
2419 20 : std::shared_lock lk(connManagerMtx_);
2420 20 : auto itHandler = channelHandlers_.find(Uri::Scheme::YDOC);
2421 20 : if (itHandler == channelHandlers_.end() || !itHandler->second)
2422 0 : return;
2423 40 : itHandler->second->connect(deviceId, documentId, [](std::shared_ptr<dhtnet::ChannelSocket>, const DeviceId&) {});
2424 20 : }
2425 :
2426 : void
2427 8677 : JamiAccount::onConnectionReady(const DeviceId& deviceId,
2428 : const std::string& name,
2429 : std::shared_ptr<dhtnet::ChannelSocket> channel)
2430 : {
2431 8677 : if (channel) {
2432 8676 : auto cert = channel->peerCertificate();
2433 8674 : if (!cert || !cert->issuer)
2434 0 : return;
2435 8675 : auto peerId = cert->issuer->getId().toString();
2436 : // A connection request can be sent just before member is banned and this must be ignored.
2437 8669 : if (accountManager()->getCertificateStatus(peerId) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2438 7 : channel->shutdown();
2439 7 : return;
2440 : }
2441 8670 : if (name == "sip") {
2442 186 : cacheSIPConnection(std::move(channel), peerId, deviceId);
2443 8484 : } else if (name.find("git://") == 0) {
2444 2085 : auto sep = name.find_last_of('/');
2445 2086 : auto conversationId = name.substr(sep + 1);
2446 2085 : auto targetDevice = name.substr(6, sep - 6);
2447 2085 : auto remoteDevice = deviceId.toString();
2448 :
2449 2085 : if (channel->isInitiator()) {
2450 : // Check if wanted remote is our side (git://targetDevice/conversationId)
2451 1043 : return;
2452 : }
2453 :
2454 1043 : if (targetDevice != currentDeviceId()) {
2455 0 : JAMI_WARNING(
2456 : "[Account {:s}] [Conversation {}] Git server requested for device {}, but this is not ours.",
2457 : getAccountID(),
2458 : conversationId,
2459 : targetDevice);
2460 0 : channel->shutdown();
2461 0 : return;
2462 : }
2463 :
2464 1043 : auto sock = convModule()->gitSocket(remoteDevice, conversationId);
2465 1043 : if (sock == channel) {
2466 : // The onConnectionReady is already used as client (for retrieving messages)
2467 : // So it's not the server socket
2468 0 : return;
2469 : }
2470 :
2471 1043 : auto startGitServer = [w = weak(), conversationId, remoteDevice, channel]() {
2472 1043 : auto shared = w.lock();
2473 1043 : if (!shared)
2474 0 : return;
2475 1043 : JAMI_LOG("[Account {:s}] [Conversation {}] [device {}] Git server requested",
2476 : shared->accountID_,
2477 : conversationId,
2478 : remoteDevice);
2479 1043 : auto gs = std::make_unique<GitServer>(shared->accountID_, conversationId, channel);
2480 1043 : shared->syncCnt_.fetch_add(1);
2481 : // Balances the increment above exactly once, whether the peer ends up
2482 : // fetching or the channel simply dies.
2483 4046 : auto endSync = [w, done = std::make_shared<std::once_flag>()] {
2484 3003 : std::call_once(*done, [&] {
2485 1043 : if (auto shared = w.lock())
2486 2086 : if (shared->syncCnt_.fetch_sub(1) == 1)
2487 991 : emitSignal<libjami::ConversationSignal::ConversationCloned>(
2488 2034 : shared->getAccountID().c_str());
2489 1043 : });
2490 4047 : };
2491 1043 : gs->setOnFetched([w, conversationId, remoteDevice, endSync](const std::string& commit) {
2492 1963 : dht::ThreadPool::computation().run([w, conversationId, remoteDevice, commit, endSync]() {
2493 1962 : if (auto shared = w.lock()) {
2494 1963 : shared->convModule()->setFetched(conversationId, remoteDevice, commit);
2495 1963 : endSync();
2496 1963 : }
2497 1963 : });
2498 1963 : });
2499 1043 : const dht::Value::Id serverId = ValueIdDist()(shared->rand);
2500 : {
2501 1043 : std::lock_guard lk(shared->gitServersMtx_);
2502 1043 : shared->gitServers_[serverId] = std::move(gs);
2503 1043 : }
2504 1043 : channel->onShutdown([w, serverId, endSync](const std::error_code&) {
2505 1038 : endSync();
2506 : // Run on main thread to avoid to be in mxSock's eventLoop
2507 1041 : runOnMainThread([serverId, w]() {
2508 1043 : if (auto sthis = w.lock()) {
2509 1043 : std::lock_guard lk(sthis->gitServersMtx_);
2510 1043 : sthis->gitServers_.erase(serverId);
2511 2086 : }
2512 1043 : });
2513 1042 : });
2514 2086 : };
2515 :
2516 1043 : if (!convModule()->isPeerAuthorized(conversationId, peerId, remoteDevice, true)) {
2517 : // Not a member — but if this is a collaborative document held
2518 : // here, a member of its parent conversation may still open it:
2519 : // the serving holder writes the add commit, then serves.
2520 : convModule()
2521 30 : ->authorizeDocumentPeer(conversationId,
2522 : peerId,
2523 : remoteDevice,
2524 30 : [w = weak(), conversationId, peerId, remoteDevice, channel, startGitServer](
2525 : bool authorized) {
2526 15 : if (authorized) {
2527 15 : startGitServer();
2528 15 : return;
2529 : }
2530 0 : if (auto shared = w.lock())
2531 0 : JAMI_WARNING("[Account {:s}] [Conversation {}] Git "
2532 : "server requested, but peer {}/{} is "
2533 : "not authorized",
2534 : shared->getAccountID(),
2535 : conversationId,
2536 : peerId,
2537 0 : remoteDevice);
2538 0 : channel->shutdown();
2539 : });
2540 15 : return;
2541 : }
2542 :
2543 1028 : startGitServer();
2544 4231 : } else {
2545 : // TODO move git://
2546 6396 : std::shared_lock lk(connManagerMtx_);
2547 6396 : auto uri = Uri(name);
2548 6394 : auto itHandler = channelHandlers_.find(uri.scheme());
2549 6394 : if (itHandler != channelHandlers_.end() && itHandler->second)
2550 6389 : itHandler->second->onReady(cert, name, std::move(channel));
2551 6400 : }
2552 9741 : }
2553 : }
2554 :
2555 : void
2556 2139 : JamiAccount::conversationNeedsSyncing(std::shared_ptr<SyncMsg>&& syncMsg)
2557 : {
2558 : // Decide from the message *content* whether it can require (re)opening sync
2559 : // connections. A change to the contact/conversation list (or requests) must
2560 : // reach devices that are not currently connected; a metadata-only update
2561 : // (read status, preferences) only rides the existing connections. Checking
2562 : // the content rather than merely "is syncMsg null" keeps this correct if
2563 : // list data is ever attached to a syncMsg in the future.
2564 2139 : if (syncMsg && !syncMsg->affectsList()) {
2565 : // Metadata-only update: ride the existing sync connections, never open
2566 : // new ones.
2567 1658 : dht::ThreadPool::computation().run([w = weak(), syncMsg = std::move(syncMsg)] {
2568 1658 : if (auto shared = w.lock())
2569 1658 : if (auto* sm = shared->syncModule())
2570 1658 : sm->syncWithConnected(syncMsg);
2571 1658 : });
2572 1658 : return;
2573 : }
2574 : // Contact/conversation-list change: for JAMS accounts, update the server;
2575 : // then bump the local sync version and (re)connect/push to other devices.
2576 : // A non-null syncMsg carrying list state is covered by the full-state push
2577 : // performed by onSyncListChanged().
2578 481 : dht::ThreadPool::computation().run([w = weak()] {
2579 481 : auto shared = w.lock();
2580 481 : if (!shared)
2581 0 : return;
2582 481 : const auto& config = shared->config();
2583 481 : if (!config.managerUri.empty())
2584 0 : if (auto am = shared->accountManager())
2585 0 : am->syncDevices();
2586 481 : shared->onSyncListChanged();
2587 481 : });
2588 : }
2589 :
2590 : uint64_t
2591 13358 : JamiAccount::conversationSendMessage(const std::string& uri,
2592 : const DeviceId& device,
2593 : const std::map<std::string, std::string>& msg,
2594 : uint64_t token)
2595 : {
2596 : // No need to retrigger, sendTextMessage will call
2597 : // messageEngine_.sendMessage, already retriggering on
2598 : // main thread.
2599 15512 : auto deviceId = device ? device.toString() : "";
2600 26673 : return sendTextMessage(uri, deviceId, msg, token);
2601 13342 : }
2602 :
2603 : void
2604 2120 : JamiAccount::onConversationNeedSocket(const std::string& convId,
2605 : const std::string& deviceId,
2606 : ChannelCb&& cb,
2607 : const std::string& type,
2608 : bool /*noNewSocket*/)
2609 : {
2610 2120 : dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::move(cb), type] {
2611 2120 : auto shared = w.lock();
2612 2120 : if (!shared)
2613 0 : return;
2614 2120 : if (auto socket = shared->convModule()->gitSocket(deviceId, convId)) {
2615 1014 : auto remoteCert = socket->peerCertificate();
2616 1014 : if (!remoteCert || !remoteCert->issuer
2617 3042 : || !shared->convModule()->isPeerAuthorized(convId,
2618 2028 : remoteCert->issuer->getId().toString(),
2619 2028 : socket->deviceId().toString(),
2620 : true)) {
2621 0 : socket->shutdown();
2622 0 : shared->convModule()->removeGitSocket(socket->deviceId().toString(), convId, socket);
2623 0 : cb({});
2624 0 : return;
2625 : }
2626 1013 : if (!cb(socket))
2627 0 : socket->shutdown();
2628 1014 : return;
2629 3134 : }
2630 1106 : std::shared_lock lkCM(shared->connManagerMtx_);
2631 1106 : if (!shared->connectionManager_) {
2632 10 : lkCM.unlock();
2633 10 : cb({});
2634 10 : return;
2635 : }
2636 :
2637 2192 : shared->connectionManager_->connectDevice(
2638 1096 : DeviceId(deviceId),
2639 3288 : fmt::format("git://{}/{}", deviceId, convId),
2640 2192 : [w, cb = std::move(cb), convId, requestedDeviceId = deviceId](std::shared_ptr<dhtnet::ChannelSocket> socket,
2641 : const DeviceId&) {
2642 2192 : dht::ThreadPool::io().run(
2643 2192 : [w, cb = std::move(cb), socket = std::move(socket), convId, requestedDeviceId] {
2644 1096 : if (socket) {
2645 1043 : auto shared = w.lock();
2646 1043 : auto remoteCert = socket->peerCertificate();
2647 1043 : auto remoteDeviceId = socket->deviceId().toString();
2648 1043 : if (!shared || !remoteCert || !remoteCert->issuer || remoteDeviceId != requestedDeviceId
2649 3129 : || !shared->convModule()->isPeerAuthorized(convId,
2650 2086 : remoteCert->issuer->getId().toString(),
2651 : remoteDeviceId,
2652 : true)) {
2653 0 : socket->shutdown();
2654 0 : cb({});
2655 0 : return;
2656 : }
2657 1043 : socket->onShutdown([w, deviceId = socket->deviceId(), convId, ws = std::weak_ptr(socket)](
2658 : const std::error_code&) {
2659 1043 : dht::ThreadPool::io().run([w, deviceId, convId, ws] {
2660 1042 : auto shared = w.lock();
2661 : // Identify the channel, so that one dying after it has already
2662 : // been replaced doesn't unregister its replacement.
2663 1041 : auto socket = ws.lock();
2664 1042 : if (shared && socket)
2665 1027 : shared->convModule()->removeGitSocket(deviceId.toString(), convId, socket);
2666 1038 : });
2667 1043 : });
2668 1043 : if (!cb(socket))
2669 7 : socket->shutdown();
2670 1043 : } else
2671 53 : cb({});
2672 : });
2673 1096 : },
2674 : false,
2675 : false,
2676 1096 : type);
2677 2130 : });
2678 2120 : }
2679 :
2680 : void
2681 1175 : JamiAccount::onConversationNeedSwarmSocket(const std::string& convId,
2682 : const std::string& deviceId,
2683 : ChannelCb&& cb,
2684 : const std::string& /*type*/,
2685 : bool noNewSocket)
2686 : {
2687 1175 : dht::ThreadPool::io().run([w = weak(), convId, deviceId, cb = std::forward<ChannelCb&&>(cb), noNewSocket] {
2688 1174 : auto shared = w.lock();
2689 1174 : if (!shared)
2690 0 : return;
2691 1173 : auto* cm = shared->convModule();
2692 1175 : std::shared_lock lkCM(shared->connManagerMtx_);
2693 1175 : if (!shared->connectionManager_ || !cm || cm->isDeviceBanned(convId, deviceId)) {
2694 90 : asio::post(*Manager::instance().ioContext(), [cb = std::move(cb)] { cb({}); });
2695 45 : return;
2696 : }
2697 1130 : DeviceId device(deviceId);
2698 1130 : auto swarmUri = fmt::format("swarm://{}", convId);
2699 1130 : dhtnet::ConnectDeviceOptions opts;
2700 1130 : opts.connType = "";
2701 1130 : opts.noNewSocket = noNewSocket;
2702 1130 : opts.uniqueName = true;
2703 2259 : shared->connectionManager_->connectDevice(
2704 : device,
2705 : swarmUri,
2706 3389 : [w,
2707 1130 : cb = std::move(cb),
2708 : wam = std::weak_ptr(shared->accountManager())](std::shared_ptr<dhtnet::ChannelSocket> socket,
2709 : const DeviceId&) {
2710 1130 : dht::ThreadPool::io().run([w, wam, cb = std::move(cb), socket = std::move(socket)] {
2711 1129 : if (socket) {
2712 828 : auto shared = w.lock();
2713 827 : auto am = wam.lock();
2714 827 : auto remoteCert = socket->peerCertificate();
2715 827 : if (!remoteCert || !remoteCert->issuer) {
2716 0 : cb(nullptr);
2717 0 : return;
2718 : }
2719 828 : auto uri = remoteCert->issuer->getId().toString();
2720 1646 : if (!shared || !am
2721 1649 : || am->getCertificateStatus(uri) == dhtnet::tls::TrustStore::PermissionStatus::BANNED) {
2722 0 : cb(nullptr);
2723 0 : return;
2724 : }
2725 829 : }
2726 1129 : cb(socket);
2727 : });
2728 1130 : },
2729 : opts);
2730 1220 : });
2731 1175 : }
2732 :
2733 : void
2734 1 : JamiAccount::conversationOneToOneReceive(const std::string& convId, const std::string& from)
2735 : {
2736 2 : accountManager_->findCertificate(dht::InfoHash(from),
2737 2 : [this, from, convId](const std::shared_ptr<dht::crypto::Certificate>& cert) {
2738 1 : const auto* info = accountManager_->getInfo();
2739 1 : if (!cert || !info)
2740 0 : return;
2741 2 : info->contacts->onTrustRequest(dht::InfoHash(from),
2742 : cert->getSharedPublicKey(),
2743 : nowMs(),
2744 : false,
2745 1 : convId,
2746 : {});
2747 : });
2748 1 : }
2749 :
2750 : ConversationModule*
2751 35004 : JamiAccount::convModule(bool noCreation)
2752 : {
2753 35004 : if (noCreation)
2754 6845 : return convModule_.get();
2755 28159 : if (!accountManager() || currentDeviceId() == "") {
2756 0 : JAMI_ERROR("[Account {}] Calling convModule() with an uninitialized account", getAccountID());
2757 0 : return nullptr;
2758 : }
2759 28153 : std::unique_lock lock(configurationMutex_);
2760 28159 : std::lock_guard lk(moduleMtx_);
2761 28156 : if (!convModule_) {
2762 1442 : convModule_ = std::make_unique<ConversationModule>(
2763 721 : shared(),
2764 721 : accountManager_,
2765 2139 : [this](auto&& syncMsg) { conversationNeedsSyncing(std::forward<std::shared_ptr<SyncMsg>>(syncMsg)); },
2766 0 : [this](auto&& uri, auto&& device, auto&& msg, auto token = 0) {
2767 13362 : return conversationSendMessage(uri, device, msg, token);
2768 : },
2769 0 : [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
2770 2120 : onConversationNeedSocket(convId, deviceId, std::forward<decltype(cb)>(cb), connectionType, noNewSocket);
2771 2120 : },
2772 0 : [this](const auto& convId, const auto& deviceId, auto&& cb, const auto& connectionType, bool noNewSocket) {
2773 1173 : onConversationNeedSwarmSocket(convId,
2774 : deviceId,
2775 1173 : std::forward<decltype(cb)>(cb),
2776 : connectionType,
2777 : noNewSocket);
2778 1175 : },
2779 722 : [this](const auto& convId, const auto& from) { conversationOneToOneReceive(convId, from); },
2780 1442 : autoLoadConversations_);
2781 : }
2782 28151 : return convModule_.get();
2783 28156 : }
2784 :
2785 : SyncModule*
2786 3112 : JamiAccount::syncModule()
2787 : {
2788 3112 : if (!accountManager() || currentDeviceId() == "") {
2789 0 : JAMI_ERROR("Calling syncModule() with an uninitialized account.");
2790 0 : return nullptr;
2791 : }
2792 3112 : std::lock_guard lk(moduleMtx_);
2793 3112 : if (!syncModule_)
2794 708 : syncModule_ = std::make_unique<SyncModule>(shared());
2795 3112 : return syncModule_.get();
2796 3112 : }
2797 :
2798 : std::shared_ptr<CollaborativeEditing>
2799 450 : JamiAccount::collaborativeEditing()
2800 : {
2801 450 : std::lock_guard lk(moduleMtx_);
2802 450 : if (!collaborativeEditing_)
2803 168 : collaborativeEditing_ = std::make_shared<CollaborativeEditing>(shared());
2804 900 : return collaborativeEditing_;
2805 450 : }
2806 :
2807 : void
2808 12386 : JamiAccount::onTextMessage(const std::string& id,
2809 : const std::string& from,
2810 : const std::shared_ptr<dht::crypto::Certificate>& peerCert,
2811 : const std::map<std::string, std::string>& payloads)
2812 : {
2813 : try {
2814 24780 : const std::string fromUri {parseJamiUri(from)};
2815 12384 : SIPAccountBase::onTextMessage(id, fromUri, peerCert, payloads);
2816 12394 : } catch (...) {
2817 0 : }
2818 12396 : }
2819 :
2820 : void
2821 0 : JamiAccount::loadConversation(const std::string& convId)
2822 : {
2823 0 : if (auto* cm = convModule(true))
2824 0 : cm->loadSingleConversation(convId);
2825 0 : }
2826 :
2827 : void
2828 995 : JamiAccount::doUnregister(bool forceShutdownConnections)
2829 : {
2830 995 : std::unique_lock lock(configurationMutex_);
2831 995 : if (registrationState_ >= RegistrationState::ERROR_GENERIC) {
2832 136 : return;
2833 : }
2834 :
2835 859 : std::mutex mtx;
2836 859 : std::condition_variable cv;
2837 859 : bool shutdown_complete {false};
2838 :
2839 859 : if (peerDiscovery_) {
2840 0 : peerDiscovery_->stopPublish(PEER_DISCOVERY_JAMI_SERVICE);
2841 0 : peerDiscovery_->stopDiscovery(PEER_DISCOVERY_JAMI_SERVICE);
2842 : }
2843 :
2844 : // Edits made here since the last checkpoint are pending for persistence only
2845 : // on this device. Online receivers may have merged them into live replicas,
2846 : // but they do not checkpoint remote updates, so those copies are volatile.
2847 : // Write the pending updates out before anything is torn down. Copy the
2848 : // pointer under its own lock: the manager is created lazily, and a channel
2849 : // request or a client call can still be assigning it on another thread.
2850 859 : std::shared_ptr<CollaborativeEditing> collab;
2851 : {
2852 859 : std::lock_guard lk(moduleMtx_);
2853 859 : collab = collaborativeEditing_;
2854 859 : }
2855 859 : if (collab) {
2856 : // flush() reaches convModule() and the CRDT document lock; both would
2857 : // deadlock against configurationMutex_, which we hold here.
2858 174 : lock.unlock();
2859 174 : collab->flush();
2860 174 : lock.lock();
2861 : // Another unregistration may have completed while the lock was down.
2862 174 : if (registrationState_ >= RegistrationState::ERROR_GENERIC)
2863 0 : return;
2864 : }
2865 :
2866 859 : JAMI_WARNING("[Account {}] Unregistering account {}", getAccountID(), fmt::ptr(this));
2867 1718 : dht_->shutdown(
2868 859 : [&] {
2869 859 : JAMI_WARNING("[Account {}] DHT shutdown complete", getAccountID());
2870 859 : std::lock_guard lock(mtx);
2871 859 : shutdown_complete = true;
2872 859 : cv.notify_all();
2873 859 : },
2874 : true);
2875 :
2876 : {
2877 859 : std::lock_guard lk(pendingCallsMutex_);
2878 859 : pendingCalls_.clear();
2879 859 : }
2880 :
2881 : // Stop all current P2P connections if account is disabled
2882 : // or if explicitly requested by the caller.
2883 : // NOTE: Leaving the connections open is useful when changing an account's config.
2884 859 : if (not isEnabled() || forceShutdownConnections)
2885 840 : shutdownConnections();
2886 :
2887 : // Keep the UPnP mapping across a plain config reload: the DHT will rebind the
2888 : // same port and doRegister_() then reuses the mapping instead of allocating a new one.
2889 859 : if (not isEnabled() || forceShutdownConnections)
2890 840 : releaseDhtUpnpMapping();
2891 :
2892 : {
2893 859 : std::unique_lock lock(mtx);
2894 2458 : cv.wait(lock, [&] { return shutdown_complete; });
2895 859 : }
2896 859 : dht_->join();
2897 859 : setRegistrationState(RegistrationState::UNREGISTERED);
2898 :
2899 859 : lock.unlock();
2900 :
2901 : #ifdef ENABLE_PLUGIN
2902 1718 : jami::Manager::instance().getJamiPluginManager().getChatServicesManager().cleanChatSubjects(getAccountID());
2903 : #endif
2904 995 : }
2905 :
2906 : void
2907 1679 : JamiAccount::releaseDhtUpnpMapping()
2908 : {
2909 1679 : if (dhtUpnpMapping_.getInternalPort() == 0)
2910 999 : return;
2911 :
2912 680 : if (upnpCtrl_)
2913 680 : upnpCtrl_->releaseMapping(dhtUpnpMapping_);
2914 680 : dhtUpnpMapping_.setNotifyCallback(nullptr);
2915 680 : dhtUpnpMapping_.updateFrom(dhtnet::upnp::Mapping(dhtnet::upnp::PortType::UDP));
2916 : }
2917 :
2918 : void
2919 5448 : JamiAccount::setRegistrationState(RegistrationState state, int detail_code, const std::string& detail_str)
2920 : {
2921 5448 : if (registrationState_ != state) {
2922 3868 : if (state == RegistrationState::REGISTERED) {
2923 740 : JAMI_WARNING("[Account {}] Connected", getAccountID());
2924 740 : turnCache_->refresh();
2925 740 : if (connectionManager_)
2926 733 : connectionManager_->storeActiveIpAddress();
2927 3128 : } else if (state == RegistrationState::TRYING) {
2928 746 : JAMI_WARNING("[Account {}] Connecting…", getAccountID());
2929 : } else {
2930 2382 : deviceAnnounced_ = false;
2931 2382 : JAMI_WARNING("[Account {}] Disconnected", getAccountID());
2932 : }
2933 : }
2934 : // Update registrationState_ & emit signals
2935 5448 : Account::setRegistrationState(state, detail_code, detail_str);
2936 5448 : }
2937 :
2938 : void
2939 0 : JamiAccount::reloadContacts()
2940 : {
2941 0 : accountManager_->reloadContacts();
2942 0 : }
2943 :
2944 : void
2945 0 : JamiAccount::connectivityChanged()
2946 : {
2947 0 : if (not isUsable()) {
2948 : // nothing to do
2949 0 : return;
2950 : }
2951 0 : JAMI_WARNING("[{}] connectivityChanged", getAccountID());
2952 :
2953 0 : if (auto* cm = convModule())
2954 0 : cm->connectivityChanged();
2955 0 : dht_->connectivityChanged();
2956 : {
2957 0 : std::shared_lock lkCM(connManagerMtx_);
2958 0 : if (connectionManager_) {
2959 0 : connectionManager_->connectivityChanged();
2960 : // reset cache
2961 0 : connectionManager_->setPublishedAddress({});
2962 : }
2963 0 : }
2964 : }
2965 :
2966 : bool
2967 0 : JamiAccount::findCertificate(const dht::InfoHash& h,
2968 : std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
2969 : {
2970 0 : if (accountManager_)
2971 0 : return accountManager_->findCertificate(h, std::move(cb));
2972 0 : return false;
2973 : }
2974 :
2975 : bool
2976 0 : JamiAccount::findCertificate(const dht::PkId& id,
2977 : std::function<void(const std::shared_ptr<dht::crypto::Certificate>&)>&& cb)
2978 : {
2979 0 : if (accountManager_)
2980 0 : return accountManager_->findCertificate(id, std::move(cb));
2981 0 : return false;
2982 : }
2983 :
2984 : bool
2985 924 : JamiAccount::findCertificate(const std::string& crt_id)
2986 : {
2987 924 : if (accountManager_)
2988 924 : return accountManager_->findCertificate(dht::InfoHash(crt_id));
2989 0 : return false;
2990 : }
2991 :
2992 : bool
2993 89 : JamiAccount::setCertificateStatus(const std::string& cert_id, dhtnet::tls::TrustStore::PermissionStatus status)
2994 : {
2995 89 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert_id, status) : false;
2996 89 : if (done) {
2997 86 : findCertificate(cert_id);
2998 86 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
2999 : cert_id,
3000 : dhtnet::tls::TrustStore::statusToStr(status));
3001 : }
3002 89 : return done;
3003 : }
3004 :
3005 : bool
3006 838 : JamiAccount::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
3007 : dhtnet::tls::TrustStore::PermissionStatus status,
3008 : bool local)
3009 : {
3010 838 : bool done = accountManager_ ? accountManager_->setCertificateStatus(cert, status, local) : false;
3011 838 : if (done) {
3012 838 : findCertificate(cert->getLongId().toString());
3013 1676 : emitSignal<libjami::ConfigurationSignal::CertificateStateChanged>(getAccountID(),
3014 1676 : cert->getLongId().toString(),
3015 : dhtnet::tls::TrustStore::statusToStr(status));
3016 : }
3017 838 : return done;
3018 : }
3019 :
3020 : std::vector<std::string>
3021 0 : JamiAccount::getCertificatesByStatus(dhtnet::tls::TrustStore::PermissionStatus status)
3022 : {
3023 0 : if (accountManager_)
3024 0 : return accountManager_->getCertificatesByStatus(status);
3025 0 : return {};
3026 : }
3027 :
3028 : bool
3029 0 : JamiAccount::isMessageTreated(dht::Value::Id id)
3030 : {
3031 0 : std::lock_guard lock(messageMutex_);
3032 0 : return !treatedMessages_.add(id);
3033 0 : }
3034 :
3035 : bool
3036 13 : JamiAccount::sha3SumVerify() const
3037 : {
3038 13 : return !noSha3sumVerification_;
3039 : }
3040 :
3041 : #ifdef LIBJAMI_TEST
3042 : void
3043 1 : JamiAccount::noSha3sumVerification(bool newValue)
3044 : {
3045 1 : noSha3sumVerification_ = newValue;
3046 1 : }
3047 : #endif
3048 :
3049 : std::map<std::string, std::string>
3050 0 : JamiAccount::getKnownDevices() const
3051 : {
3052 0 : std::lock_guard lock(configurationMutex_);
3053 0 : if (not accountManager_ or not accountManager_->getInfo())
3054 0 : return {};
3055 0 : std::map<std::string, std::string> ids;
3056 0 : for (const auto& d : accountManager_->getKnownDevices()) {
3057 0 : auto id = d.first.toString();
3058 0 : auto label = d.second.name.empty() ? id.substr(0, 8) : d.second.name;
3059 0 : ids.emplace(std::move(id), std::move(label));
3060 0 : }
3061 0 : return ids;
3062 0 : }
3063 :
3064 : void
3065 0 : JamiAccount::loadCachedUrl(const std::string& url,
3066 : const std::filesystem::path& cachePath,
3067 : const std::chrono::seconds& cacheDuration,
3068 : const std::function<void(const dht::http::Response& response)>& cb)
3069 : {
3070 0 : dht::ThreadPool::io().run([cb, url, cachePath, cacheDuration, w = weak()]() {
3071 : try {
3072 0 : std::string data;
3073 : {
3074 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3075 0 : data = fileutils::loadCacheTextFile(cachePath, cacheDuration);
3076 0 : }
3077 0 : dht::http::Response ret;
3078 0 : ret.body = std::move(data);
3079 0 : ret.status_code = 200;
3080 0 : cb(ret);
3081 0 : } catch (const std::exception& e) {
3082 0 : JAMI_LOG("Failed to load '{}' from '{}': {}", url, cachePath, e.what());
3083 :
3084 0 : if (auto sthis = w.lock()) {
3085 : auto req = std::make_shared<dht::http::Request>(
3086 0 : *Manager::instance().ioContext(), url, [cb, cachePath, w](const dht::http::Response& response) {
3087 0 : if (response.status_code == 200) {
3088 : try {
3089 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3090 0 : fileutils::saveFile(cachePath,
3091 0 : (const uint8_t*) response.body.data(),
3092 : response.body.size(),
3093 : 0600);
3094 0 : JAMI_LOG("Cached result to '{}'", cachePath);
3095 0 : } catch (const std::exception& ex) {
3096 0 : JAMI_WARNING("Failed to save result to '{}': {}", cachePath, ex.what());
3097 0 : }
3098 0 : cb(response);
3099 : } else {
3100 : try {
3101 0 : if (std::filesystem::exists(cachePath)) {
3102 0 : JAMI_WARNING("Failed to download URL, using cached data");
3103 0 : std::string data;
3104 : {
3105 0 : std::lock_guard lk(dhtnet::fileutils::getFileLock(cachePath));
3106 0 : data = fileutils::loadTextFile(cachePath);
3107 0 : }
3108 0 : dht::http::Response ret;
3109 0 : ret.body = std::move(data);
3110 0 : ret.status_code = 200;
3111 0 : cb(ret);
3112 0 : } else
3113 0 : throw std::runtime_error("No cached data");
3114 0 : } catch (...) {
3115 0 : cb(response);
3116 0 : }
3117 : }
3118 0 : if (auto req = response.request.lock())
3119 0 : if (auto sthis = w.lock())
3120 0 : sthis->requests_.erase(req);
3121 0 : });
3122 0 : sthis->requests_.emplace(req);
3123 0 : req->send();
3124 0 : }
3125 0 : }
3126 0 : });
3127 0 : }
3128 :
3129 : void
3130 746 : JamiAccount::loadCachedProxyServer(std::function<void(const std::string& proxy)> cb)
3131 : {
3132 746 : const auto& conf = config();
3133 746 : if (conf.proxyEnabled and proxyServerCached_.empty()) {
3134 0 : JAMI_DEBUG("[Account {:s}] Loading DHT proxy URL: {:s}", getAccountID(), conf.proxyListUrl);
3135 0 : if (conf.proxyListUrl.empty() or not conf.proxyListEnabled) {
3136 0 : cb(getDhtProxyServer(conf.proxyServer));
3137 : } else {
3138 0 : loadCachedUrl(conf.proxyListUrl,
3139 0 : cachePath_ / "dhtproxylist",
3140 0 : std::chrono::hours(24 * 3),
3141 0 : [w = weak(), cb = std::move(cb)](const dht::http::Response& response) {
3142 0 : if (auto sthis = w.lock()) {
3143 0 : if (response.status_code == 200) {
3144 0 : cb(sthis->getDhtProxyServer(response.body));
3145 : } else {
3146 0 : cb(sthis->getDhtProxyServer(sthis->config().proxyServer));
3147 : }
3148 0 : }
3149 0 : });
3150 : }
3151 : } else {
3152 746 : cb(proxyServerCached_);
3153 : }
3154 746 : }
3155 :
3156 : std::string
3157 0 : JamiAccount::getDhtProxyServer(const std::string& serverList)
3158 : {
3159 0 : if (proxyServerCached_.empty()) {
3160 0 : std::vector<std::string> proxys;
3161 : // Split the list of servers
3162 0 : std::sregex_iterator begin = {serverList.begin(), serverList.end(), PROXY_REGEX}, end;
3163 0 : for (auto it = begin; it != end; ++it) {
3164 0 : auto& match = *it;
3165 0 : if (match[5].matched and match[6].matched) {
3166 : try {
3167 0 : auto start = std::stoi(match[5]), end = std::stoi(match[6]);
3168 0 : for (auto p = start; p <= end; p++)
3169 0 : proxys.emplace_back(match[1].str() + match[2].str() + ":" + std::to_string(p));
3170 0 : } catch (...) {
3171 0 : JAMI_WARNING("Malformed proxy, ignore it");
3172 0 : continue;
3173 0 : }
3174 : } else {
3175 0 : proxys.emplace_back(match[0].str());
3176 : }
3177 0 : }
3178 0 : if (proxys.empty())
3179 0 : return {};
3180 : // Select one of the list as the current proxy.
3181 0 : auto randIt = proxys.begin();
3182 0 : std::advance(randIt, std::uniform_int_distribution<unsigned long>(0, proxys.size() - 1)(rand));
3183 0 : proxyServerCached_ = *randIt;
3184 : // Cache it!
3185 0 : dhtnet::fileutils::check_dir(cachePath_, 0700);
3186 0 : auto proxyCachePath = cachePath_ / "dhtproxy";
3187 0 : std::ofstream file(proxyCachePath);
3188 0 : JAMI_DEBUG("Cache DHT proxy server: {}", proxyServerCached_);
3189 0 : Json::Value node(Json::objectValue);
3190 0 : node[getProxyConfigKey()] = proxyServerCached_;
3191 0 : if (file.is_open())
3192 0 : file << node;
3193 : else
3194 0 : JAMI_WARNING("Unable to write into {}", proxyCachePath);
3195 0 : }
3196 0 : return proxyServerCached_;
3197 : }
3198 :
3199 : MatchRank
3200 0 : JamiAccount::matches(std::string_view userName, std::string_view server) const
3201 : {
3202 0 : if (not accountManager_ or not accountManager_->getInfo())
3203 0 : return MatchRank::NONE;
3204 :
3205 0 : if (userName == accountManager_->getInfo()->accountId || server == accountManager_->getInfo()->accountId
3206 0 : || userName == accountManager_->getInfo()->deviceId) {
3207 0 : JAMI_LOG("Matching account ID in request with username {}", userName);
3208 0 : return MatchRank::FULL;
3209 : } else {
3210 0 : return MatchRank::NONE;
3211 : }
3212 : }
3213 :
3214 : std::string
3215 341 : JamiAccount::getFromUri() const
3216 : {
3217 341 : std::string uri = "<sip:" + accountManager_->getInfo()->accountId + "@ring.dht>";
3218 343 : if (not config().displayName.empty())
3219 341 : return "\"" + config().displayName + "\" " + uri;
3220 0 : return uri;
3221 342 : }
3222 :
3223 : std::string
3224 292 : JamiAccount::getToUri(const std::string& to) const
3225 : {
3226 292 : auto username = to;
3227 876 : string_replace(username, "sip:", "");
3228 584 : return fmt::format("<sips:{};transport=tls>", username);
3229 292 : }
3230 :
3231 : std::string
3232 7 : getDisplayed(const std::string& conversationId, const std::string& messageId)
3233 : {
3234 : // implementing https://tools.ietf.org/rfc/rfc5438.txt
3235 : return fmt::format("<?xml version=\"1.0\" encoding=\"utf-8\" ?>\n"
3236 : "<imdn><message-id>{}</message-id>\n"
3237 : "{}"
3238 : "<display-notification><status><displayed/></status></display-notification>\n"
3239 : "</imdn>",
3240 : messageId,
3241 14 : conversationId.empty() ? "" : "<conversation>" + conversationId + "</conversation>");
3242 : }
3243 :
3244 : std::string
3245 6 : getPIDF(const std::string& note)
3246 : {
3247 : // implementing https://datatracker.ietf.org/doc/html/rfc3863
3248 : return fmt::format("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
3249 : "<presence xmlns=\"urn:ietf:params:xml:ns:pidf\">\n"
3250 : " <tuple>\n"
3251 : " <status>\n"
3252 : " <basic>{}</basic>\n"
3253 : " </status>\n"
3254 : " </tuple>\n"
3255 : "</presence>",
3256 12 : note);
3257 : }
3258 :
3259 : void
3260 5 : JamiAccount::setIsComposing(const std::string& conversationUri, bool isWriting)
3261 : {
3262 5 : Uri uri(conversationUri);
3263 5 : std::string conversationId = {};
3264 5 : if (uri.scheme() == Uri::Scheme::SWARM) {
3265 5 : conversationId = uri.authority();
3266 : } else {
3267 0 : return;
3268 : }
3269 :
3270 5 : if (auto* cm = convModule(true)) {
3271 5 : if (auto typer = cm->getTypers(conversationId)) {
3272 5 : if (isWriting)
3273 4 : typer->addTyper(getUsername(), true);
3274 : else
3275 1 : typer->removeTyper(getUsername(), true);
3276 5 : }
3277 : }
3278 5 : }
3279 :
3280 : bool
3281 9 : JamiAccount::setMessageDisplayed(const std::string& conversationUri, const std::string& messageId, int status)
3282 : {
3283 9 : Uri uri(conversationUri);
3284 9 : std::string conversationId = {};
3285 9 : if (uri.scheme() == Uri::Scheme::SWARM)
3286 9 : conversationId = uri.authority();
3287 9 : auto sendMessage = status == (int) libjami::Account::MessageStates::DISPLAYED && isReadReceiptEnabled();
3288 9 : if (!conversationId.empty())
3289 9 : sendMessage &= convModule()->onMessageDisplayed(getUsername(), conversationId, messageId);
3290 9 : if (sendMessage)
3291 21 : sendInstantMessage(uri.authority(), {{MIME_TYPE_IMDN, getDisplayed(conversationId, messageId)}});
3292 9 : return true;
3293 16 : }
3294 :
3295 : std::string
3296 194 : JamiAccount::getContactHeader(const std::shared_ptr<SipTransport>& sipTransport)
3297 : {
3298 194 : if (sipTransport and sipTransport->get() != nullptr) {
3299 194 : auto* transport = sipTransport->get();
3300 194 : auto* td = reinterpret_cast<tls::AbstractSIPTransport::TransportData*>(transport);
3301 194 : auto address = td->self->getLocalAddress().toString(true);
3302 194 : bool reliable = transport->flag & PJSIP_TRANSPORT_RELIABLE;
3303 : return fmt::format("\"{}\" <sips:{}{}{};transport={}>",
3304 194 : config().displayName,
3305 194 : id_.second->getId().toString(),
3306 194 : address.empty() ? "" : "@",
3307 : address,
3308 582 : reliable ? "tls" : "dtls");
3309 194 : } else {
3310 0 : JAMI_ERROR("getContactHeader: no SIP transport provided");
3311 0 : return fmt::format("\"{}\" <sips:{}@ring.dht>", config().displayName, id_.second->getId().toString());
3312 : }
3313 : }
3314 :
3315 : void
3316 48 : JamiAccount::addContact(const std::string& uri, bool confirmed)
3317 : {
3318 48 : dht::InfoHash h(uri);
3319 48 : if (not h) {
3320 1 : JAMI_ERROR("addContact: invalid contact URI");
3321 1 : return;
3322 : }
3323 47 : auto conversation = convModule()->getOneToOneConversation(uri);
3324 47 : if (!confirmed && conversation.empty())
3325 47 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
3326 47 : std::unique_lock lock(configurationMutex_);
3327 47 : if (accountManager_)
3328 47 : accountManager_->addContact(h, confirmed, conversation);
3329 : else
3330 0 : JAMI_WARNING("[Account {}] addContact: account not loaded", getAccountID());
3331 47 : }
3332 :
3333 : void
3334 8 : JamiAccount::removeContact(const std::string& uri, bool ban)
3335 : {
3336 8 : std::lock_guard lock(configurationMutex_);
3337 8 : if (accountManager_)
3338 8 : accountManager_->removeContact(uri, ban);
3339 : else
3340 0 : JAMI_WARNING("[Account {}] removeContact: account not loaded", getAccountID());
3341 8 : }
3342 :
3343 : std::map<std::string, std::string>
3344 4 : JamiAccount::getContactDetails(const std::string& uri) const
3345 : {
3346 4 : std::lock_guard lock(configurationMutex_);
3347 8 : return accountManager_ ? accountManager_->getContactDetails(uri) : std::map<std::string, std::string> {};
3348 4 : }
3349 :
3350 : std::optional<Contact>
3351 601 : JamiAccount::getContactInfo(const std::string& uri) const
3352 : {
3353 601 : std::lock_guard lock(configurationMutex_);
3354 1202 : return accountManager_ ? accountManager_->getContactInfo(uri) : std::nullopt;
3355 601 : }
3356 :
3357 : bool
3358 0 : JamiAccount::isContact(const std::string& peerAccountUri) const
3359 : {
3360 0 : auto info = getContactInfo(peerAccountUri);
3361 0 : return info && info->isActive();
3362 0 : }
3363 :
3364 : // ----------------------------------------------------------------------------
3365 : // Service-exposure: peer discovery & tunnel orchestration.
3366 : // ----------------------------------------------------------------------------
3367 :
3368 : struct JamiAccount::PendingSvcQuery
3369 : {
3370 : uint32_t requestId {0};
3371 : std::string peerUri;
3372 : };
3373 :
3374 : void
3375 1 : JamiAccount::finalizeSvcQuery(uint32_t requestId, int status, const std::string& servicesJson)
3376 : {
3377 1 : JAMI_LOG("[Account {}] finalizeSvcQuery req={} status={}", getAccountID(), requestId, status);
3378 1 : std::shared_ptr<PendingSvcQuery> q;
3379 : {
3380 1 : std::lock_guard lk(pendingSvcQueriesMtx_);
3381 1 : auto it = pendingSvcQueries_.find(requestId);
3382 1 : if (it == pendingSvcQueries_.end())
3383 0 : return;
3384 1 : q = std::move(it->second);
3385 1 : pendingSvcQueries_.erase(it);
3386 1 : }
3387 1 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(requestId, getAccountID(), q->peerUri, status, servicesJson);
3388 1 : }
3389 :
3390 : std::string
3391 1992 : JamiAccount::buildPeerServicesJson(const std::string& peerUri, const DeviceId* forceAvailableDevice)
3392 : {
3393 1992 : std::vector<SvcDiscoveryChannelHandler::CachedSvcInfo> services;
3394 : {
3395 1992 : std::shared_lock lk(connManagerMtx_);
3396 1992 : auto it = channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
3397 1992 : if (it == channelHandlers_.end() || !it->second)
3398 64 : return {};
3399 1928 : services = static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->getCachedServices(peerUri);
3400 1992 : }
3401 1928 : if (services.empty())
3402 1918 : return {};
3403 :
3404 10 : std::vector<DeviceId> onlineDevices;
3405 10 : if (presenceManager_)
3406 10 : onlineDevices = presenceManager_->getDevices(peerUri);
3407 :
3408 10 : Json::Value arr(Json::arrayValue);
3409 20 : for (const auto& s : services) {
3410 10 : Json::Value v(Json::objectValue);
3411 10 : v["id"] = s.info.id;
3412 10 : v["name"] = s.info.name;
3413 10 : v["description"] = s.info.description;
3414 10 : v["proto"] = s.info.proto;
3415 10 : v["scheme"] = s.info.scheme;
3416 10 : v["preferredPort"] = s.info.preferred_port;
3417 10 : v["device"] = s.deviceId.toString();
3418 19 : v["available"] = (forceAvailableDevice && *forceAvailableDevice == s.deviceId)
3419 29 : || std::find(onlineDevices.begin(), onlineDevices.end(), s.deviceId) != onlineDevices.end();
3420 10 : arr.append(std::move(v));
3421 10 : }
3422 10 : return json::toString(arr);
3423 1992 : }
3424 :
3425 : uint32_t
3426 1 : JamiAccount::queryPeerServices(const std::string& peerUri)
3427 : {
3428 : using PSStatus = libjami::ServiceSignal::PeerServicesStatus;
3429 :
3430 : static std::atomic<uint32_t> sQueryCounter {0};
3431 1 : const auto requestId = ++sQueryCounter;
3432 :
3433 1 : JAMI_LOG("[Account {}] queryPeerServices req={} peer={}", getAccountID(), requestId, peerUri);
3434 :
3435 1 : auto state = std::make_shared<PendingSvcQuery>();
3436 1 : state->requestId = requestId;
3437 1 : state->peerUri = peerUri;
3438 : {
3439 1 : std::lock_guard lk(pendingSvcQueriesMtx_);
3440 1 : pendingSvcQueries_.emplace(requestId, state);
3441 1 : }
3442 :
3443 1 : std::shared_lock lk(connManagerMtx_);
3444 1 : auto* handler = static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get());
3445 1 : if (!handler) {
3446 0 : return 0;
3447 : }
3448 :
3449 1 : runOnMainThread([w = weak(), requestId, peerUri] {
3450 1 : auto sthis = w.lock();
3451 1 : if (!sthis)
3452 0 : return;
3453 1 : auto servicesJson = sthis->buildPeerServicesJson(peerUri);
3454 2 : sthis->finalizeSvcQuery(requestId,
3455 1 : static_cast<int>(servicesJson.empty() ? PSStatus::NoDevices : PSStatus::OK),
3456 : servicesJson);
3457 1 : });
3458 1 : return requestId;
3459 1 : }
3460 :
3461 : std::string
3462 1 : JamiAccount::openServiceTunnel(const std::string& peerUri,
3463 : const std::string& deviceId,
3464 : const std::string& serviceId,
3465 : const std::string& serviceName,
3466 : uint16_t localPort)
3467 : {
3468 1 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3469 1 : if (!handler)
3470 0 : return {};
3471 1 : DeviceId dev;
3472 : try {
3473 1 : dev = DeviceId(deviceId);
3474 0 : } catch (...) {
3475 0 : return {};
3476 0 : }
3477 1 : auto accId = getAccountID();
3478 : return handler->openTunnel(
3479 : peerUri,
3480 : dev,
3481 : serviceId,
3482 : serviceName,
3483 : localPort,
3484 2 : [accId](const std::string& tunnelId, uint16_t port) {
3485 1 : emitSignal<libjami::ServiceSignal::TunnelOpened>(accId, tunnelId, port);
3486 1 : },
3487 1 : [accId](const std::string& tunnelId, const std::string& reason) {
3488 1 : emitSignal<libjami::ServiceSignal::TunnelClosed>(accId, tunnelId, reason);
3489 4 : });
3490 1 : }
3491 :
3492 : bool
3493 1 : JamiAccount::closeServiceTunnel(const std::string& tunnelId)
3494 : {
3495 1 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3496 1 : if (!handler)
3497 0 : return false;
3498 1 : return handler->closeTunnel(tunnelId);
3499 : }
3500 :
3501 : void
3502 0 : JamiAccount::closeServerTunnelsForService(const std::string& serviceId)
3503 : {
3504 0 : auto* handler = static_cast<SvcTunnelChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_TUNNEL].get());
3505 0 : if (!handler)
3506 0 : return;
3507 0 : handler->closeServerChannelsForService(serviceId);
3508 : }
3509 :
3510 : std::vector<std::map<std::string, std::string>>
3511 0 : JamiAccount::getActiveServiceTunnels() const
3512 : {
3513 0 : auto it = channelHandlers_.find(Uri::Scheme::SVC_TUNNEL);
3514 0 : if (it == channelHandlers_.end())
3515 0 : return {};
3516 0 : auto* handler = static_cast<SvcTunnelChannelHandler*>(it->second.get());
3517 0 : if (!handler)
3518 0 : return {};
3519 0 : auto tunnels = handler->activeTunnels();
3520 0 : std::vector<std::map<std::string, std::string>> out;
3521 0 : out.reserve(tunnels.size());
3522 0 : for (auto& t : tunnels) {
3523 0 : out.push_back({{"id", t.id},
3524 0 : {"peerUri", t.peerUri},
3525 0 : {"deviceId", t.peerDevice},
3526 0 : {"serviceId", t.serviceId},
3527 0 : {"serviceName", t.serviceName},
3528 0 : {"localPort", std::to_string(t.localPort)}});
3529 : }
3530 0 : return out;
3531 0 : }
3532 :
3533 : std::vector<std::map<std::string, std::string>>
3534 1 : JamiAccount::getContacts(bool includeRemoved) const
3535 : {
3536 1 : std::lock_guard lock(configurationMutex_);
3537 1 : if (not accountManager_)
3538 0 : return {};
3539 1 : const auto& contacts = accountManager_->getContacts(includeRemoved);
3540 1 : std::vector<std::map<std::string, std::string>> ret;
3541 1 : ret.reserve(contacts.size());
3542 2 : for (const auto& c : contacts) {
3543 1 : auto details = c.second.toMap();
3544 1 : if (not details.empty()) {
3545 3 : details["id"] = c.first.toString();
3546 1 : ret.emplace_back(std::move(details));
3547 : }
3548 1 : }
3549 1 : return ret;
3550 1 : }
3551 :
3552 : /* trust requests */
3553 :
3554 : std::vector<std::map<std::string, std::string>>
3555 736 : JamiAccount::getTrustRequests() const
3556 : {
3557 736 : std::lock_guard lock(configurationMutex_);
3558 1472 : return accountManager_ ? accountManager_->getTrustRequests() : std::vector<std::map<std::string, std::string>> {};
3559 736 : }
3560 :
3561 : bool
3562 21 : JamiAccount::acceptTrustRequest(const std::string& from, bool includeConversation)
3563 : {
3564 21 : dht::InfoHash h(from);
3565 21 : if (not h) {
3566 0 : JAMI_ERROR("addContact: invalid contact URI");
3567 0 : return false;
3568 : }
3569 21 : std::unique_lock lock(configurationMutex_);
3570 21 : if (accountManager_) {
3571 21 : if (!accountManager_->acceptTrustRequest(from, includeConversation)) {
3572 : // Note: unused for swarm
3573 : // Typically the case where the trust request doesn't exists, only incoming DHT messages
3574 0 : return accountManager_->addContact(h, true);
3575 : }
3576 21 : return true;
3577 : }
3578 0 : JAMI_WARNING("[Account {}] acceptTrustRequest: account not loaded", getAccountID());
3579 0 : return false;
3580 21 : }
3581 :
3582 : bool
3583 0 : JamiAccount::discardTrustRequest(const std::string& from)
3584 : {
3585 : // Remove 1:1 generated conv requests
3586 0 : auto requests = getTrustRequests();
3587 0 : for (const auto& req : requests) {
3588 0 : if (req.at(libjami::Account::TrustRequest::FROM) == from) {
3589 0 : convModule()->declineConversationRequest(req.at(libjami::Account::TrustRequest::CONVERSATIONID));
3590 : }
3591 : }
3592 :
3593 : // Remove trust request
3594 0 : std::lock_guard lock(configurationMutex_);
3595 0 : if (accountManager_)
3596 0 : return accountManager_->discardTrustRequest(from);
3597 0 : JAMI_WARNING("[Account {:s}] discardTrustRequest: account not loaded", getAccountID());
3598 0 : return false;
3599 0 : }
3600 :
3601 : void
3602 1 : JamiAccount::declineConversationRequest(const std::string& conversationId)
3603 : {
3604 1 : auto peerId = convModule()->peerFromConversationRequest(conversationId);
3605 1 : convModule()->declineConversationRequest(conversationId);
3606 1 : if (!peerId.empty()) {
3607 1 : std::lock_guard lock(configurationMutex_);
3608 1 : if (const auto* info = accountManager_->getInfo()) {
3609 : // Verify if we have a trust request with this peer + convId
3610 1 : auto req = info->contacts->getTrustRequest(dht::InfoHash(peerId));
3611 3 : if (req.find(libjami::Account::TrustRequest::CONVERSATIONID) != req.end()
3612 2 : && req.at(libjami::Account::TrustRequest::CONVERSATIONID) == conversationId) {
3613 0 : accountManager_->discardTrustRequest(peerId);
3614 0 : JAMI_DEBUG("[Account {:s}] Declined trust request with {:s}", getAccountID(), peerId);
3615 : }
3616 1 : }
3617 1 : }
3618 1 : }
3619 :
3620 : void
3621 40 : JamiAccount::sendTrustRequest(const std::string& to, const std::vector<uint8_t>& payload)
3622 : {
3623 40 : dht::InfoHash h(to);
3624 40 : if (not h) {
3625 0 : JAMI_ERROR("addContact: invalid contact URI");
3626 0 : return;
3627 : }
3628 :
3629 : // The invite timestamp is now a native field on the trust request itself (see
3630 : // jami::TrustRequestMsg), no need to stamp it into the vCard payload anymore.
3631 40 : auto invited = nowMs();
3632 :
3633 : // Cache the payload (and the invite timestamp alongside it) so retries reuse the same
3634 : // timestamp instead of being treated as a new re-invite.
3635 40 : auto requestPath = cachePath_ / "requests";
3636 40 : dhtnet::fileutils::recursive_mkdir(requestPath, 0700);
3637 40 : auto cachedFile = requestPath / to;
3638 40 : std::ofstream req(cachedFile, std::ios::trunc | std::ios::binary);
3639 40 : if (!req.is_open()) {
3640 0 : JAMI_ERROR("Unable to write data to {}", cachedFile);
3641 0 : return;
3642 : }
3643 :
3644 40 : if (not payload.empty()) {
3645 1 : req.write(reinterpret_cast<const char*>(payload.data()), static_cast<std::streamsize>(payload.size()));
3646 : }
3647 40 : req.close();
3648 :
3649 40 : auto cachedTsFile = requestPath / (to + ".ts");
3650 40 : std::ofstream tsFile(cachedTsFile, std::ios::trunc | std::ios::binary);
3651 40 : if (tsFile.is_open())
3652 40 : tsFile << toMillisecondsSinceEpoch(invited);
3653 : else
3654 0 : JAMI_WARNING("Unable to write invite timestamp cache to {}", cachedTsFile);
3655 :
3656 40 : if (payload.size() >= 64000) {
3657 0 : JAMI_WARNING("Trust request is too big. Remove payload");
3658 : }
3659 :
3660 40 : auto conversation = convModule()->getOneToOneConversation(to);
3661 40 : if (conversation.empty())
3662 0 : conversation = convModule()->startConversation(ConversationMode::ONE_TO_ONE, h);
3663 40 : if (not conversation.empty()) {
3664 40 : std::lock_guard lock(configurationMutex_);
3665 40 : if (accountManager_)
3666 80 : accountManager_->sendTrustRequest(to,
3667 : conversation,
3668 80 : payload.size() >= 64000 ? std::vector<uint8_t> {} : payload,
3669 : invited);
3670 : else
3671 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
3672 40 : } else
3673 0 : JAMI_WARNING("[Account {}] sendTrustRequest: account not loaded", getAccountID());
3674 40 : }
3675 :
3676 : void
3677 0 : JamiAccount::forEachDevice(const dht::InfoHash& to,
3678 : std::function<void(const std::shared_ptr<dht::crypto::PublicKey>&)>&& op,
3679 : std::function<void(bool)>&& end)
3680 : {
3681 0 : accountManager_->forEachDevice(to, std::move(op), std::move(end));
3682 0 : }
3683 :
3684 : uint64_t
3685 13346 : JamiAccount::sendTextMessage(const std::string& to,
3686 : const std::string& deviceId,
3687 : const std::map<std::string, std::string>& payloads,
3688 : uint64_t refreshToken,
3689 : bool onlyConnected)
3690 : {
3691 13346 : Uri uri(to);
3692 13325 : if (uri.scheme() == Uri::Scheme::SWARM) {
3693 0 : sendInstantMessage(uri.authority(), payloads);
3694 0 : return 0;
3695 : }
3696 :
3697 13331 : std::string toUri;
3698 : try {
3699 13327 : toUri = parseJamiUri(to);
3700 0 : } catch (...) {
3701 0 : JAMI_ERROR("Failed to send a text message due to an invalid URI {}", to);
3702 0 : return 0;
3703 0 : }
3704 13308 : if (payloads.size() != 1) {
3705 0 : JAMI_ERROR("Multi-part im is not supported yet by JamiAccount");
3706 0 : return 0;
3707 : }
3708 13347 : return SIPAccountBase::sendTextMessage(toUri, deviceId, payloads, refreshToken, onlyConnected);
3709 13347 : }
3710 :
3711 : void
3712 14341 : JamiAccount::sendMessage(const std::string& to,
3713 : const std::string& deviceId,
3714 : const std::map<std::string, std::string>& payloads,
3715 : uint64_t token,
3716 : bool retryOnTimeout,
3717 : bool onlyConnected)
3718 : {
3719 14341 : std::string toUri;
3720 : try {
3721 14341 : toUri = parseJamiUri(to);
3722 0 : } catch (...) {
3723 0 : JAMI_ERROR("[Account {}] Failed to send a text message due to an invalid URI {}", getAccountID(), to);
3724 0 : if (!onlyConnected)
3725 0 : messageEngine_.onMessageSent(to, token, false, deviceId);
3726 0 : return;
3727 0 : }
3728 14341 : if (payloads.size() != 1) {
3729 0 : JAMI_ERROR("Multi-part im is not supported");
3730 0 : if (!onlyConnected)
3731 0 : messageEngine_.onMessageSent(toUri, token, false, deviceId);
3732 0 : return;
3733 : }
3734 :
3735 : // Use the Message channel if available
3736 14341 : std::shared_lock clk(connManagerMtx_);
3737 14341 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
3738 14341 : if (!handler) {
3739 27 : clk.unlock();
3740 27 : if (!onlyConnected)
3741 27 : messageEngine_.onMessageSent(to, token, false, deviceId);
3742 27 : return;
3743 : }
3744 :
3745 : auto devices = std::make_shared<SendMessageContext>(
3746 28628 : [w = weak(), to, token, deviceId, onlyConnected, retryOnTimeout](bool success, bool sent) {
3747 14311 : if (auto acc = w.lock())
3748 14311 : acc->onMessageSent(to, token, deviceId, success, onlyConnected, sent && retryOnTimeout);
3749 28628 : });
3750 :
3751 12404 : auto completed = [w = weak(), to, devices](const DeviceId& device,
3752 : const std::shared_ptr<dhtnet::ChannelSocket>& conn,
3753 : bool success) {
3754 12404 : if (!success)
3755 4 : if (auto acc = w.lock()) {
3756 4 : std::shared_lock clk(acc->connManagerMtx_);
3757 4 : if (auto* handler = static_cast<MessageChannelHandler*>(
3758 4 : acc->channelHandlers_[Uri::Scheme::MESSAGE].get())) {
3759 4 : handler->closeChannel(to, device, conn);
3760 : }
3761 8 : }
3762 12404 : devices->complete(device, success);
3763 26716 : };
3764 :
3765 14314 : const auto& payload = *payloads.begin();
3766 14314 : auto msg = std::make_shared<MessageChannelHandler::Message>();
3767 14314 : msg->id = token;
3768 14314 : msg->t = payload.first;
3769 14313 : msg->c = payload.second;
3770 14314 : auto device = deviceId.empty() ? DeviceId() : DeviceId(deviceId);
3771 14314 : if (deviceId.empty()) {
3772 3094 : auto conns = handler->getChannels(toUri);
3773 3094 : clk.unlock();
3774 5241 : for (const auto& conn : conns) {
3775 2147 : auto connDevice = conn->deviceId();
3776 2147 : if (!devices->add(connDevice))
3777 954 : continue;
3778 1192 : dht::ThreadPool::io().run([completed, connDevice, conn, msg] {
3779 1193 : completed(connDevice, conn, MessageChannelHandler::sendMessage(conn, *msg));
3780 1193 : });
3781 : }
3782 3093 : } else {
3783 11220 : if (auto conn = handler->getChannel(toUri, device)) {
3784 11211 : clk.unlock();
3785 11211 : devices->add(device);
3786 11211 : dht::ThreadPool::io().run([completed, device, conn, msg] {
3787 11211 : completed(device, conn, MessageChannelHandler::sendMessage(conn, *msg));
3788 11209 : });
3789 11211 : devices->start();
3790 11211 : return;
3791 11220 : }
3792 : }
3793 3103 : if (clk)
3794 9 : clk.unlock();
3795 :
3796 3103 : devices->start();
3797 :
3798 3103 : if (onlyConnected)
3799 22 : return;
3800 : // We are unable to send the message directly, try connecting
3801 :
3802 : // Get conversation id, which will be used by the iOS notification extension
3803 : // to load the conversation.
3804 2460 : auto extractIdFromJson = [](const std::string& jsonData) -> std::string {
3805 2460 : Json::Value parsed;
3806 2460 : if (json::parse(jsonData, parsed)) {
3807 2460 : auto value = parsed.get("id", Json::nullValue);
3808 2460 : if (value && value.isString()) {
3809 2460 : return value.asString();
3810 : }
3811 2460 : } else {
3812 0 : JAMI_WARNING("Unable to parse jsonData to get conversation ID");
3813 : }
3814 0 : return "";
3815 2460 : };
3816 :
3817 : // get request type
3818 3081 : auto payload_type = msg->t;
3819 3081 : if (payload_type == MIME_TYPE_GIT) {
3820 2460 : std::string id = extractIdFromJson(msg->c);
3821 2460 : if (!id.empty()) {
3822 2460 : payload_type += "/" + id;
3823 : }
3824 2460 : }
3825 :
3826 3081 : if (deviceId.empty()) {
3827 3072 : auto toH = dht::InfoHash(toUri);
3828 : // Find listening devices for this account
3829 9215 : accountManager_->forEachDevice(toH,
3830 6144 : [this, to, devices, payload_type, currentDevice = DeviceId(currentDeviceId())](
3831 : const std::shared_ptr<dht::crypto::PublicKey>& dev) {
3832 : // Test if already sent
3833 3122 : auto deviceId = dev->getLongId();
3834 3122 : if (deviceId == currentDevice || devices->pending(deviceId)) {
3835 658 : return;
3836 : }
3837 :
3838 : // Else, ask for a channel to send the message
3839 2464 : dht::ThreadPool::io().run([this, to, deviceId, payload_type]() {
3840 2464 : requestMessageConnection(to, deviceId, payload_type);
3841 2464 : });
3842 : });
3843 : } else {
3844 9 : requestMessageConnection(to, device, payload_type);
3845 : }
3846 59300 : }
3847 :
3848 : void
3849 14311 : JamiAccount::onMessageSent(
3850 : const std::string& to, uint64_t id, const std::string& deviceId, bool success, bool onlyConnected, bool retry)
3851 : {
3852 14311 : if (!onlyConnected)
3853 14283 : messageEngine_.onMessageSent(to, id, success, deviceId);
3854 :
3855 14313 : if (!success) {
3856 1915 : if (retry)
3857 4 : messageEngine_.onPeerOnline(to, deviceId);
3858 : }
3859 14313 : }
3860 :
3861 : dhtnet::IceTransportOptions
3862 129 : JamiAccount::getIceOptions() const
3863 : {
3864 129 : return connectionManager_->getIceOptions();
3865 : }
3866 :
3867 : void
3868 11 : JamiAccount::getIceOptions(std::function<void(dhtnet::IceTransportOptions&&)> cb) const
3869 : {
3870 11 : return connectionManager_->getIceOptions(std::move(cb));
3871 : }
3872 :
3873 : dhtnet::IpAddr
3874 108 : JamiAccount::getPublishedIpAddress(uint16_t family) const
3875 : {
3876 108 : return connectionManager_->getPublishedIpAddress(family);
3877 : }
3878 :
3879 : bool
3880 0 : JamiAccount::setPushNotificationToken(const std::string& token)
3881 : {
3882 0 : if (SIPAccountBase::setPushNotificationToken(token)) {
3883 0 : JAMI_WARNING("[Account {:s}] setPushNotificationToken: {:s}", getAccountID(), token);
3884 0 : if (dht_)
3885 0 : dht_->setPushNotificationToken(token);
3886 0 : return true;
3887 : }
3888 0 : return false;
3889 : }
3890 :
3891 : bool
3892 0 : JamiAccount::setPushNotificationTopic(const std::string& topic)
3893 : {
3894 0 : if (SIPAccountBase::setPushNotificationTopic(topic)) {
3895 0 : if (dht_)
3896 0 : dht_->setPushNotificationTopic(topic);
3897 0 : return true;
3898 : }
3899 0 : return false;
3900 : }
3901 :
3902 : bool
3903 0 : JamiAccount::setPushNotificationConfig(const std::map<std::string, std::string>& data)
3904 : {
3905 0 : if (SIPAccountBase::setPushNotificationConfig(data)) {
3906 0 : if (dht_) {
3907 0 : dht_->setPushNotificationPlatform(config_->platform);
3908 0 : dht_->setPushNotificationTopic(config_->notificationTopic);
3909 0 : dht_->setPushNotificationToken(config_->deviceKey);
3910 : }
3911 0 : return true;
3912 : }
3913 0 : return false;
3914 : }
3915 :
3916 : /**
3917 : * To be called by clients with relevant data when a push notification is received.
3918 : */
3919 : void
3920 0 : JamiAccount::pushNotificationReceived(const std::string& /*from*/, const std::map<std::string, std::string>& data)
3921 : {
3922 0 : auto ret_future = dht_->pushNotificationReceived(data);
3923 0 : dht::ThreadPool::computation().run([id = getAccountID(), ret_future = ret_future.share()] {
3924 0 : JAMI_WARNING("[Account {:s}] pushNotificationReceived: {}", id, (uint8_t) ret_future.get());
3925 0 : });
3926 0 : }
3927 :
3928 : std::string
3929 9 : JamiAccount::getUserUri() const
3930 : {
3931 9 : if (not registeredName_.empty())
3932 0 : return JAMI_URI_PREFIX + registeredName_;
3933 9 : return JAMI_URI_PREFIX + config().username;
3934 : }
3935 :
3936 : std::vector<libjami::Message>
3937 0 : JamiAccount::getLastMessages(const uint64_t& base_timestamp)
3938 : {
3939 0 : return SIPAccountBase::getLastMessages(base_timestamp);
3940 : }
3941 :
3942 : void
3943 0 : JamiAccount::startAccountPublish()
3944 : {
3945 0 : AccountPeerInfo info_pub;
3946 0 : info_pub.accountId = dht::InfoHash(accountManager_->getInfo()->accountId);
3947 0 : info_pub.displayName = config().displayName;
3948 0 : peerDiscovery_->startPublish<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, info_pub);
3949 0 : }
3950 :
3951 : void
3952 0 : JamiAccount::startAccountDiscovery()
3953 : {
3954 0 : auto id = dht::InfoHash(accountManager_->getInfo()->accountId);
3955 0 : peerDiscovery_
3956 0 : ->startDiscovery<AccountPeerInfo>(PEER_DISCOVERY_JAMI_SERVICE, [this, id](AccountPeerInfo&& v, dht::SockAddr&&) {
3957 0 : std::lock_guard lc(discoveryMapMtx_);
3958 : // Make sure that account itself will not be recorded
3959 0 : if (v.accountId != id) {
3960 : // Create or find the old one
3961 0 : auto& dp = discoveredPeers_[v.accountId];
3962 0 : dp.displayName = v.displayName;
3963 0 : discoveredPeerMap_[v.accountId.toString()] = v.displayName;
3964 0 : if (!dp.cleanupTimer) {
3965 : // Avoid repeat reception of same peer
3966 0 : JAMI_LOG("Account discovered: {}: {}", v.displayName, v.accountId.to_c_str());
3967 : // Send Added Peer and corrsponding accoundID
3968 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(getAccountID(),
3969 0 : v.accountId.toString(),
3970 : 0,
3971 0 : v.displayName);
3972 0 : dp.cleanupTimer = std::make_unique<asio::steady_timer>(*Manager::instance().ioContext(),
3973 0 : PEER_DISCOVERY_EXPIRATION);
3974 : }
3975 0 : dp.cleanupTimer->expires_after(PEER_DISCOVERY_EXPIRATION);
3976 0 : dp.cleanupTimer->async_wait(
3977 0 : [w = weak(), p = v.accountId, a = v.displayName](const asio::error_code& ec) {
3978 0 : if (ec)
3979 0 : return;
3980 0 : if (auto this_ = w.lock()) {
3981 : {
3982 0 : std::lock_guard lc(this_->discoveryMapMtx_);
3983 0 : this_->discoveredPeers_.erase(p);
3984 0 : this_->discoveredPeerMap_.erase(p.toString());
3985 0 : }
3986 : // Send deleted peer
3987 0 : emitSignal<libjami::PresenceSignal::NearbyPeerNotification>(this_->getAccountID(),
3988 0 : p.toString(),
3989 : 1,
3990 0 : a);
3991 0 : }
3992 0 : JAMI_LOG("Account removed from discovery list: {}", a);
3993 : });
3994 : }
3995 0 : });
3996 0 : }
3997 :
3998 : std::map<std::string, std::string>
3999 0 : JamiAccount::getNearbyPeers() const
4000 : {
4001 0 : return discoveredPeerMap_;
4002 : }
4003 :
4004 : void
4005 0 : JamiAccount::sendProfileToPeers()
4006 : {
4007 0 : if (!connectionManager_)
4008 0 : return;
4009 0 : std::set<std::string> peers;
4010 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
4011 : // TODO: avoid using getConnectionList
4012 0 : for (const auto& connection : connectionManager_->getConnectionList()) {
4013 0 : const auto& device = connection.at("device");
4014 0 : const auto& peer = connection.at("peer");
4015 0 : if (!peers.emplace(peer).second)
4016 0 : continue;
4017 0 : if (peer == accountUri) {
4018 0 : sendProfile("", accountUri, device);
4019 0 : continue;
4020 : }
4021 0 : const auto& conversationId = convModule()->getOneToOneConversation(peer);
4022 0 : if (!conversationId.empty()) {
4023 0 : sendProfile(conversationId, peer, device);
4024 : }
4025 0 : }
4026 0 : }
4027 :
4028 : void
4029 0 : JamiAccount::updateProfile(const std::string& displayName,
4030 : const std::string& avatar,
4031 : const std::string& fileType,
4032 : const std::string& botOwner,
4033 : int32_t flag)
4034 : {
4035 : // if the fileType is empty then only the display name will be upated
4036 :
4037 0 : const auto& accountUri = accountManager_->getInfo()->accountId;
4038 0 : const auto& path = profilePath();
4039 0 : const auto& profiles = idPath_ / "profiles";
4040 :
4041 : try {
4042 0 : if (!std::filesystem::exists(profiles)) {
4043 0 : std::filesystem::create_directories(profiles);
4044 : }
4045 0 : } catch (const std::exception& e) {
4046 0 : JAMI_ERROR("Failed to create profiles directory: {}", e.what());
4047 0 : return;
4048 0 : }
4049 :
4050 0 : const auto& vCardPath = profiles / fmt::format("{}.vcf", base64::encode(accountUri));
4051 :
4052 0 : auto profile = getProfileVcard();
4053 0 : if (profile.empty()) {
4054 0 : profile = vCard::utils::initVcard();
4055 : }
4056 :
4057 0 : profile[std::string(vCard::Property::FORMATTED_NAME)] = displayName;
4058 0 : editConfig([&](JamiAccountConfig& config) { config.displayName = displayName; });
4059 0 : emitSignal<libjami::ConfigurationSignal::AccountDetailsChanged>(getAccountID(), getAccountDetails());
4060 :
4061 0 : if (!fileType.empty()) {
4062 0 : const std::string& key = "PHOTO;ENCODING=BASE64;TYPE=" + fileType;
4063 0 : if (flag == 0) {
4064 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4065 0 : const auto& avatarPath = std::filesystem::path(avatar);
4066 0 : if (std::filesystem::exists(avatarPath)) {
4067 : try {
4068 0 : profile[key] = base64::encode(fileutils::loadFile(avatarPath));
4069 0 : } catch (const std::exception& e) {
4070 0 : JAMI_ERROR("Failed to load avatar: {}", e.what());
4071 0 : }
4072 : }
4073 0 : } else if (flag == 1) {
4074 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4075 0 : profile[key] = avatar;
4076 : }
4077 0 : }
4078 0 : if (flag == 2) {
4079 0 : vCard::utils::removeByKey(profile, vCard::Property::PHOTO);
4080 : }
4081 0 : if (!botOwner.empty()) {
4082 : // See RFC 6473
4083 0 : profile[std::string(vCard::Property::KIND)] = "application";
4084 : // See RFC 6350
4085 0 : profile[std::string(vCard::Property::RELATED_OWNER)] = botOwner;
4086 : }
4087 : try {
4088 0 : vCard::utils::save(profile, vCardPath, path);
4089 0 : emitSignal<libjami::ConfigurationSignal::ProfileReceived>(getAccountID(), accountUri, path.string());
4090 :
4091 : // Delete all profile sent markers:
4092 0 : std::error_code ec;
4093 0 : std::filesystem::remove_all(cachePath_ / "vcard", ec);
4094 0 : sendProfileToPeers();
4095 0 : } catch (const std::exception& e) {
4096 0 : JAMI_ERROR("Error writing profile: {}", e.what());
4097 0 : }
4098 0 : }
4099 :
4100 : void
4101 840 : JamiAccount::setActiveCodecs(const std::vector<unsigned>& list)
4102 : {
4103 840 : Account::setActiveCodecs(list);
4104 840 : if (!hasActiveCodec(MEDIA_AUDIO))
4105 818 : setCodecActive(AV_CODEC_ID_OPUS);
4106 840 : if (!hasActiveCodec(MEDIA_VIDEO)) {
4107 818 : setCodecActive(AV_CODEC_ID_HEVC);
4108 818 : setCodecActive(AV_CODEC_ID_H264);
4109 818 : setCodecActive(AV_CODEC_ID_VP8);
4110 : }
4111 840 : config_->activeCodecs = getActiveCodecs(MEDIA_ALL);
4112 840 : }
4113 :
4114 : void
4115 11 : JamiAccount::sendInstantMessage(const std::string& convId, const std::map<std::string, std::string>& msg)
4116 : {
4117 11 : auto members = convModule()->getConversationMembers(convId);
4118 11 : if (convId.empty() && members.empty()) {
4119 : // TODO remove, it's for old API for contacts
4120 0 : sendTextMessage(convId, "", msg);
4121 0 : return;
4122 : }
4123 33 : for (const auto& m : members) {
4124 22 : const auto& uri = m.at("uri");
4125 22 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4126 : // Announce to all members that a new message is sent
4127 66 : sendMessage(uri, "", msg, token, false, true);
4128 : }
4129 11 : }
4130 :
4131 : bool
4132 12394 : JamiAccount::handleMessage(const std::shared_ptr<dht::crypto::Certificate>& cert,
4133 : const std::string& from,
4134 : const std::pair<std::string, std::string>& m)
4135 : {
4136 12394 : if (not cert or not cert->issuer)
4137 0 : return true; // stop processing message
4138 :
4139 12393 : if (cert->issuer->getId().to_view() != from) {
4140 0 : JAMI_WARNING("[Account {}] [device {}] handleMessage: invalid author {}",
4141 : getAccountID(),
4142 : cert->issuer->getId().to_view(),
4143 : from);
4144 0 : return true;
4145 : }
4146 12396 : if (m.first == MIME_TYPE_GIT) {
4147 11933 : Json::Value json;
4148 11933 : if (!json::parse(m.second, json)) {
4149 0 : return true;
4150 : }
4151 :
4152 : // fetchNewCommits will do heavy stuff like fetching, avoid to block SIP socket
4153 23864 : dht::ThreadPool::io().run([w = weak(),
4154 : from,
4155 11929 : deviceId = json["deviceId"].asString(),
4156 11930 : id = json["id"].asString(),
4157 11931 : commit = json["commit"].asString()] {
4158 11933 : if (auto shared = w.lock()) {
4159 11931 : if (auto* cm = shared->convModule())
4160 11931 : cm->fetchNewCommits(from, deviceId, id, commit);
4161 11930 : }
4162 11927 : });
4163 11931 : return true;
4164 12394 : } else if (m.first == MIME_TYPE_INVITE) {
4165 149 : convModule()->onNeedConversationRequest(from, m.second);
4166 149 : return true;
4167 314 : } else if (m.first == MIME_TYPE_INVITE_JSON) {
4168 295 : Json::Value json;
4169 295 : if (!json::parse(m.second, json)) {
4170 0 : return true;
4171 : }
4172 295 : convModule()->onConversationRequest(from, json);
4173 295 : return true;
4174 314 : } else if (m.first == MIME_TYPE_IM_COMPOSING) {
4175 : try {
4176 4 : static const std::regex COMPOSING_REGEX("<state>\\s*(\\w+)\\s*<\\/state>");
4177 4 : std::smatch matched_pattern;
4178 4 : std::regex_search(m.second, matched_pattern, COMPOSING_REGEX);
4179 4 : bool isComposing {false};
4180 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4181 4 : isComposing = matched_pattern[1] == "active";
4182 : }
4183 4 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
4184 4 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
4185 4 : std::string conversationId = "";
4186 4 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4187 4 : conversationId = matched_pattern[1];
4188 : }
4189 4 : if (!conversationId.empty()) {
4190 4 : if (auto* cm = convModule(true)) {
4191 4 : if (auto typer = cm->getTypers(conversationId)) {
4192 4 : if (isComposing)
4193 3 : typer->addTyper(from);
4194 : else
4195 1 : typer->removeTyper(from);
4196 4 : }
4197 : }
4198 : }
4199 4 : return true;
4200 4 : } catch (const std::exception& e) {
4201 0 : JAMI_WARNING("Error parsing composing state: {}", e.what());
4202 0 : }
4203 15 : } else if (m.first == MIME_TYPE_IMDN) {
4204 : try {
4205 9 : static const std::regex IMDN_MSG_ID_REGEX("<message-id>\\s*(\\w+)\\s*<\\/message-id>");
4206 9 : std::smatch matched_pattern;
4207 :
4208 9 : std::regex_search(m.second, matched_pattern, IMDN_MSG_ID_REGEX);
4209 9 : std::string messageId;
4210 9 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4211 9 : messageId = matched_pattern[1];
4212 : } else {
4213 0 : JAMI_WARNING("Message displayed: unable to parse message ID");
4214 0 : return true;
4215 : }
4216 :
4217 9 : static const std::regex STATUS_REGEX("<status>\\s*<(\\w+)\\/>\\s*<\\/status>");
4218 9 : std::regex_search(m.second, matched_pattern, STATUS_REGEX);
4219 9 : bool isDisplayed {false};
4220 9 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4221 9 : isDisplayed = matched_pattern[1] == "displayed";
4222 : } else {
4223 0 : JAMI_WARNING("Message displayed: unable to parse status");
4224 0 : return true;
4225 : }
4226 :
4227 9 : static const std::regex CONVID_REGEX("<conversation>\\s*(\\w+)\\s*<\\/conversation>");
4228 9 : std::regex_search(m.second, matched_pattern, CONVID_REGEX);
4229 9 : std::string conversationId = "";
4230 9 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4231 9 : conversationId = matched_pattern[1];
4232 : }
4233 :
4234 9 : if (!isReadReceiptEnabled())
4235 0 : return true;
4236 9 : if (isDisplayed) {
4237 9 : if (convModule()->onMessageDisplayed(from, conversationId, messageId)) {
4238 8 : JAMI_DEBUG("[message {}] Displayed by peer", messageId);
4239 16 : emitSignal<libjami::ConfigurationSignal::AccountMessageStatusChanged>(
4240 8 : accountID_,
4241 : conversationId,
4242 : from,
4243 : messageId,
4244 : static_cast<int>(libjami::Account::MessageStates::DISPLAYED));
4245 : }
4246 : }
4247 9 : return true;
4248 9 : } catch (const std::exception& e) {
4249 0 : JAMI_ERROR("Error parsing display notification: {}", e.what());
4250 0 : }
4251 6 : } else if (m.first == MIME_TYPE_PIDF) {
4252 6 : std::smatch matched_pattern;
4253 6 : static const std::regex BASIC_REGEX("<basic>([\\w\\s]+)<\\/basic>");
4254 6 : std::regex_search(m.second, matched_pattern, BASIC_REGEX);
4255 6 : std::string customStatus {};
4256 6 : if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
4257 6 : customStatus = matched_pattern[1];
4258 6 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(getAccountID(),
4259 : from,
4260 : static_cast<int>(PresenceState::CONNECTED),
4261 : customStatus);
4262 : } else {
4263 0 : JAMI_WARNING("Presence: unable to parse status");
4264 : }
4265 6 : return true;
4266 6 : }
4267 :
4268 0 : return false;
4269 : }
4270 :
4271 : void
4272 277 : JamiAccount::callConnectionClosed(const DeviceId& deviceId, bool eraseDummy)
4273 : {
4274 277 : std::function<void(const DeviceId&, bool)> cb;
4275 : {
4276 277 : std::lock_guard lk(onConnectionClosedMtx_);
4277 277 : auto it = onConnectionClosed_.find(deviceId);
4278 277 : if (it != onConnectionClosed_.end()) {
4279 89 : if (eraseDummy) {
4280 89 : cb = std::move(it->second);
4281 89 : onConnectionClosed_.erase(it);
4282 : } else {
4283 : // In this case a new subcall is created and the callback
4284 : // will be re-called once with eraseDummy = true
4285 0 : cb = it->second;
4286 : }
4287 : }
4288 277 : }
4289 277 : dht::ThreadPool::io().run([w = weak(), cb = std::move(cb), id = deviceId, erase = std::move(eraseDummy)] {
4290 277 : if (auto acc = w.lock()) {
4291 277 : if (cb)
4292 89 : cb(id, erase);
4293 277 : }
4294 277 : });
4295 277 : }
4296 :
4297 : void
4298 4849 : JamiAccount::requestMessageConnection(const std::string& peerId,
4299 : const DeviceId& deviceId,
4300 : const std::string& connectionType)
4301 : {
4302 4849 : std::shared_lock lk(connManagerMtx_);
4303 4850 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
4304 4849 : if (!handler)
4305 4 : return;
4306 4845 : if (deviceId) {
4307 4846 : if (auto connected = handler->getChannel(peerId, deviceId)) {
4308 1228 : return;
4309 4845 : }
4310 : } else {
4311 0 : auto connected = handler->getChannels(peerId);
4312 0 : if (!connected.empty()) {
4313 0 : return;
4314 : }
4315 0 : }
4316 : // Nothing to do on completion: the work that has to happen once the device is
4317 : // reachable is driven by onMessageChannelReady().
4318 10851 : handler->connect(deviceId, "", [](const std::shared_ptr<dhtnet::ChannelSocket>&, const DeviceId&) {}, connectionType);
4319 4850 : }
4320 :
4321 : void
4322 1256 : JamiAccount::onMessageChannelReady(const std::string& peerId, const DeviceId& deviceId)
4323 : {
4324 1256 : messageEngine_.onPeerOnline(peerId);
4325 1259 : messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
4326 1256 : if (!presenceNote_.empty()) {
4327 : // If a presence note is set, send it to this device.
4328 3 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4329 9 : std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
4330 3 : sendMessage(peerId, deviceId.toString(), msg, token, false, true);
4331 3 : }
4332 1257 : convModule()->syncConversations(peerId, deviceId.toString());
4333 1261 : }
4334 :
4335 : void
4336 95 : JamiAccount::requestSIPConnection(const std::string& peerId,
4337 : const DeviceId& deviceId,
4338 : const std::string& connectionType,
4339 : bool forceNewConnection,
4340 : const std::shared_ptr<SIPCall>& pc)
4341 : {
4342 95 : JAMI_LOG("[Account {}] Request SIP connection to peer {} on device {}", getAccountID(), peerId, deviceId);
4343 :
4344 : // If a connection already exists or is in progress, no need to do this
4345 95 : std::lock_guard lk(sipConnsMtx_);
4346 95 : auto id = std::make_pair(peerId, deviceId);
4347 :
4348 95 : if (sipConns_.find(id) != sipConns_.end()) {
4349 0 : JAMI_LOG("[Account {}] A SIP connection with {} already exists", getAccountID(), deviceId);
4350 0 : return;
4351 : }
4352 : // If not present, create it
4353 95 : std::shared_lock lkCM(connManagerMtx_);
4354 95 : if (!connectionManager_)
4355 0 : return;
4356 : // Note, Even if we send 50 "sip" request, the connectionManager_ will only use one socket.
4357 : // however, this will still ask for multiple channels, so only ask
4358 : // if there is no pending request
4359 95 : if (!forceNewConnection && connectionManager_->isConnecting(deviceId, "sip")) {
4360 0 : JAMI_LOG("[Account {}] Already connecting to {}", getAccountID(), deviceId);
4361 0 : return;
4362 : }
4363 95 : JAMI_LOG("[Account {}] Ask {} for a new SIP channel", getAccountID(), deviceId);
4364 95 : dhtnet::ConnectDeviceOptions options;
4365 95 : options.noNewSocket = false;
4366 95 : options.forceNewSocket = forceNewConnection;
4367 95 : options.connType = connectionType;
4368 95 : options.channelTimeout = 3s;
4369 95 : options.uniqueName = true;
4370 285 : connectionManager_->connectDevice(
4371 : deviceId,
4372 : "sip",
4373 190 : [w = weak(), id = std::move(id), pc = std::move(pc)](const std::shared_ptr<dhtnet::ChannelSocket>& socket,
4374 : const DeviceId&) {
4375 95 : if (socket)
4376 92 : return;
4377 3 : auto shared = w.lock();
4378 3 : if (!shared)
4379 0 : return;
4380 : // If this is triggered, this means that the
4381 : // connectDevice didn't get any response from the DHT.
4382 : // Stop searching pending call.
4383 3 : shared->callConnectionClosed(id.second, true);
4384 3 : if (pc)
4385 3 : pc->onFailure(PJSIP_SC_TEMPORARILY_UNAVAILABLE);
4386 3 : },
4387 : options);
4388 95 : }
4389 :
4390 : bool
4391 322 : JamiAccount::isConnectedWith(const DeviceId& deviceId) const
4392 : {
4393 322 : std::shared_lock lkCM(connManagerMtx_);
4394 322 : if (connectionManager_)
4395 321 : return connectionManager_->isConnected(deviceId);
4396 0 : return false;
4397 322 : }
4398 :
4399 : void
4400 3 : JamiAccount::sendPresenceNote(const std::string& note)
4401 : {
4402 3 : if (const auto* info = accountManager_->getInfo()) {
4403 3 : if (!info || !info->contacts)
4404 0 : return;
4405 3 : presenceNote_ = note;
4406 3 : auto contacts = info->contacts->getContacts();
4407 3 : std::vector<std::pair<std::string, DeviceId>> keys;
4408 : {
4409 3 : std::shared_lock lkCM(connManagerMtx_);
4410 3 : auto* handler = static_cast<MessageChannelHandler*>(channelHandlers_[Uri::Scheme::MESSAGE].get());
4411 3 : if (!handler)
4412 0 : return;
4413 5 : for (const auto& contact : contacts) {
4414 2 : auto peerId = contact.first.toString();
4415 2 : auto channels = handler->getChannels(peerId);
4416 5 : for (const auto& channel : channels) {
4417 3 : keys.emplace_back(peerId, channel->deviceId());
4418 : }
4419 2 : }
4420 3 : }
4421 3 : auto token = std::uniform_int_distribution<uint64_t> {1, JAMI_ID_MAX_VAL}(rand);
4422 9 : std::map<std::string, std::string> msg = {{MIME_TYPE_PIDF, getPIDF(presenceNote_)}};
4423 6 : for (auto& key : keys) {
4424 3 : sendMessage(key.first, key.second.toString(), msg, token, false, true);
4425 : }
4426 3 : }
4427 3 : }
4428 :
4429 : void
4430 1217 : JamiAccount::sendProfile(const std::string& convId, const std::string& peerUri, const std::string& deviceId)
4431 : {
4432 1217 : auto accProfilePath = profilePath();
4433 1217 : if (not std::filesystem::is_regular_file(accProfilePath))
4434 1213 : return;
4435 5 : auto currentSha3 = fileutils::sha3File(accProfilePath);
4436 : // VCard sync for peerUri
4437 5 : if (not needToSendProfile(peerUri, deviceId, currentSha3)) {
4438 0 : JAMI_DEBUG("[Account {}] [device {}] Peer {} already got an up-to-date vCard",
4439 : getAccountID(),
4440 : deviceId,
4441 : peerUri);
4442 0 : return;
4443 : }
4444 : // We need a new channel
4445 30 : transferFile(convId,
4446 10 : accProfilePath.string(),
4447 : deviceId,
4448 : "profile.vcf",
4449 : "",
4450 : 0,
4451 : 0,
4452 : currentSha3,
4453 : fileutils::lastWriteTimeInSeconds(accProfilePath),
4454 10 : [accId = getAccountID(), peerUri, deviceId]() {
4455 : // Mark the VCard as sent
4456 3 : auto sendDir = fileutils::get_cache_dir() / accId / "vcard" / peerUri;
4457 3 : auto path = sendDir / deviceId;
4458 3 : dhtnet::fileutils::recursive_mkdir(sendDir);
4459 3 : std::lock_guard lock(dhtnet::fileutils::getFileLock(path));
4460 3 : if (std::filesystem::is_regular_file(path))
4461 0 : return;
4462 3 : std::ofstream p(path);
4463 3 : });
4464 1218 : }
4465 :
4466 : bool
4467 5 : JamiAccount::needToSendProfile(const std::string& peerUri, const std::string& deviceId, const std::string& sha3Sum)
4468 : {
4469 5 : std::string previousSha3 {};
4470 5 : auto vCardPath = cachePath_ / "vcard";
4471 5 : auto sha3Path = vCardPath / "sha3";
4472 5 : dhtnet::fileutils::check_dir(vCardPath, 0700);
4473 : try {
4474 7 : previousSha3 = fileutils::loadTextFile(sha3Path);
4475 2 : } catch (...) {
4476 2 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
4477 2 : return true;
4478 2 : }
4479 3 : if (sha3Sum != previousSha3) {
4480 : // Incorrect sha3 stored. Update it
4481 0 : dhtnet::fileutils::removeAll(vCardPath, true);
4482 0 : dhtnet::fileutils::check_dir(vCardPath, 0700);
4483 0 : fileutils::saveFile(sha3Path, (const uint8_t*) sha3Sum.data(), sha3Sum.size(), 0600);
4484 0 : return true;
4485 : }
4486 3 : auto peerPath = vCardPath / peerUri;
4487 3 : dhtnet::fileutils::recursive_mkdir(peerPath);
4488 3 : return not std::filesystem::is_regular_file(peerPath / deviceId);
4489 5 : }
4490 :
4491 : void
4492 86 : JamiAccount::clearProfileCache(const std::string& peerUri)
4493 : {
4494 86 : std::error_code ec;
4495 86 : std::filesystem::remove_all(cachePath_ / "vcard" / peerUri, ec);
4496 86 : }
4497 :
4498 : std::filesystem::path
4499 1219 : JamiAccount::profilePath() const
4500 : {
4501 1219 : return idPath_ / "profile.vcf";
4502 : }
4503 :
4504 : void
4505 186 : JamiAccount::cacheSIPConnection(std::shared_ptr<dhtnet::ChannelSocket>&& socket,
4506 : const std::string& peerId,
4507 : const DeviceId& deviceId)
4508 : {
4509 186 : std::unique_lock lk(sipConnsMtx_);
4510 : // Verify that the connection is not already cached
4511 186 : SipConnectionKey key(peerId, deviceId);
4512 186 : auto& connections = sipConns_[key];
4513 186 : auto conn = std::find_if(connections.begin(), connections.end(), [&](const auto& v) { return v.channel == socket; });
4514 186 : if (conn != connections.end()) {
4515 0 : JAMI_WARNING("[Account {}] Channel socket already cached with this peer", getAccountID());
4516 0 : return;
4517 : }
4518 :
4519 : // Convert to SIP transport
4520 186 : auto onShutdown = [w = weak(), peerId, key, socket]() {
4521 186 : dht::ThreadPool::io().run([w = std::move(w), peerId, key, socket] {
4522 186 : auto shared = w.lock();
4523 186 : if (!shared)
4524 1 : return;
4525 185 : shared->shutdownSIPConnection(socket, key.first, key.second);
4526 : // The connection can be closed during the SIP initialization, so
4527 : // if this happens, the request should be re-sent to ask for a new
4528 : // SIP channel to make the call pass through
4529 185 : shared->callConnectionClosed(key.second, false);
4530 186 : });
4531 372 : };
4532 186 : auto sip_tr = link_.sipTransportBroker->getChanneledTransport(shared(), socket, std::move(onShutdown));
4533 186 : if (!sip_tr) {
4534 0 : JAMI_ERROR("No channeled transport found");
4535 0 : return;
4536 : }
4537 : // Store the connection
4538 186 : connections.emplace_back(SipConnection {sip_tr, socket});
4539 186 : JAMI_WARNING("[Account {:s}] [device {}] New SIP channel opened", getAccountID(), deviceId);
4540 186 : lk.unlock();
4541 :
4542 : // Retry messages
4543 186 : messageEngine_.onPeerOnline(peerId);
4544 186 : messageEngine_.onPeerOnline(peerId, deviceId.toString(), true);
4545 :
4546 : // Connect pending calls
4547 186 : forEachPendingCall(deviceId, [&](const auto& pc) {
4548 92 : if (pc->getConnectionState() != Call::ConnectionState::TRYING
4549 92 : and pc->getConnectionState() != Call::ConnectionState::PROGRESSING)
4550 0 : return;
4551 92 : pc->setSipTransport(sip_tr, getContactHeader(sip_tr));
4552 92 : pc->setState(Call::ConnectionState::PROGRESSING);
4553 92 : if (auto remote_address = socket->getRemoteAddress()) {
4554 : try {
4555 92 : onConnectedOutgoingCall(pc, peerId, remote_address);
4556 0 : } catch (const VoipLinkException&) {
4557 : // In this case, the main scenario is that SIPStartCall failed because
4558 : // the ICE is dead and the TLS session didn't send any packet on that dead
4559 : // link (connectivity change, killed by the os, etc)
4560 : // Here, we don't need to do anything, the TLS will fail and will delete
4561 : // the cached transport
4562 : }
4563 : }
4564 : });
4565 186 : }
4566 :
4567 : void
4568 185 : JamiAccount::shutdownSIPConnection(const std::shared_ptr<dhtnet::ChannelSocket>& channel,
4569 : const std::string& peerId,
4570 : const DeviceId& deviceId)
4571 : {
4572 185 : std::unique_lock lk(sipConnsMtx_);
4573 185 : SipConnectionKey key(peerId, deviceId);
4574 185 : auto it = sipConns_.find(key);
4575 185 : if (it != sipConns_.end()) {
4576 94 : auto& conns = it->second;
4577 188 : conns.erase(std::remove_if(conns.begin(), conns.end(), [&](auto v) { return v.channel == channel; }),
4578 94 : conns.end());
4579 94 : if (conns.empty()) {
4580 94 : sipConns_.erase(it);
4581 : }
4582 : }
4583 185 : lk.unlock();
4584 : // Shutdown after removal to let the callbacks do stuff if needed
4585 185 : if (channel)
4586 185 : channel->shutdown();
4587 185 : }
4588 :
4589 : std::string_view
4590 37783 : JamiAccount::currentDeviceId() const
4591 : {
4592 37783 : if (!accountManager_ or not accountManager_->getInfo())
4593 0 : return {};
4594 37786 : return accountManager_->getInfo()->deviceId;
4595 : }
4596 :
4597 : std::shared_ptr<TransferManager>
4598 146 : JamiAccount::dataTransfer(const std::string& id)
4599 : {
4600 146 : if (id.empty())
4601 66 : return nonSwarmTransferManager_;
4602 80 : if (auto* cm = convModule())
4603 80 : return cm->dataTransfer(id);
4604 0 : return {};
4605 : }
4606 :
4607 : void
4608 0 : JamiAccount::monitor()
4609 : {
4610 0 : JAMI_DEBUG("[Account {:s}] Monitor connections", getAccountID());
4611 0 : JAMI_DEBUG("[Account {:s}] Using proxy: {:s}", getAccountID(), proxyServerCached_);
4612 :
4613 0 : if (auto* cm = convModule())
4614 0 : cm->monitor();
4615 0 : std::shared_lock lkCM(connManagerMtx_);
4616 0 : if (connectionManager_)
4617 0 : connectionManager_->monitor();
4618 0 : }
4619 :
4620 : std::vector<std::map<std::string, std::string>>
4621 0 : JamiAccount::getConnectionList(const std::string& conversationId)
4622 : {
4623 0 : std::shared_lock lkCM(connManagerMtx_);
4624 0 : if (connectionManager_ && conversationId.empty()) {
4625 0 : return connectionManager_->getConnectionList();
4626 0 : } else if (connectionManager_ && convModule_) {
4627 0 : std::vector<std::map<std::string, std::string>> connectionList;
4628 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4629 0 : for (const auto& deviceId : conv->getDeviceIdList()) {
4630 0 : auto connections = connectionManager_->getConnectionList(deviceId);
4631 0 : connectionList.reserve(connectionList.size() + connections.size());
4632 0 : std::move(connections.begin(), connections.end(), std::back_inserter(connectionList));
4633 0 : }
4634 0 : }
4635 0 : return connectionList;
4636 0 : } else {
4637 0 : return {};
4638 : }
4639 0 : }
4640 :
4641 : std::vector<std::map<std::string, std::string>>
4642 0 : JamiAccount::getConversationConnectivity(const std::string& conversationId)
4643 : {
4644 0 : std::shared_lock lkCM(connManagerMtx_);
4645 0 : if (convModule_) {
4646 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4647 0 : return conv->getConnectivity();
4648 0 : }
4649 : }
4650 0 : return {};
4651 0 : }
4652 :
4653 : std::vector<std::map<std::string, std::string>>
4654 0 : JamiAccount::getConversationTrackedMembers(const std::string& conversationId)
4655 : {
4656 0 : std::shared_lock lkCM(connManagerMtx_);
4657 0 : if (convModule_) {
4658 0 : if (auto conv = convModule_->getConversation(conversationId)) {
4659 0 : return conv->getTrackedMembers();
4660 0 : }
4661 : }
4662 0 : return {};
4663 0 : }
4664 :
4665 : std::vector<std::map<std::string, std::string>>
4666 0 : JamiAccount::getChannelList(const std::string& connectionId)
4667 : {
4668 0 : std::shared_lock lkCM(connManagerMtx_);
4669 0 : if (!connectionManager_)
4670 0 : return {};
4671 0 : return connectionManager_->getChannelList(connectionId);
4672 0 : }
4673 :
4674 : void
4675 14 : JamiAccount::sendFile(const std::string& conversationId,
4676 : const std::filesystem::path& path,
4677 : const std::string& name,
4678 : const std::string& replyTo)
4679 : {
4680 14 : std::error_code ec;
4681 14 : if (!std::filesystem::is_regular_file(path, ec)) {
4682 0 : JAMI_ERROR("Invalid filename '{}'", path);
4683 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(getAccountID(),
4684 : conversationId,
4685 : EVALIDFETCH,
4686 : "Invalid filename.");
4687 0 : return;
4688 : }
4689 :
4690 14 : auto fileSize = std::filesystem::file_size(path, ec);
4691 14 : if (ec || fileSize == static_cast<decltype(fileSize)>(-1)) {
4692 0 : JAMI_ERROR("Negative file size, user probably doesn't have the appropriate permissions for '{}'", path);
4693 0 : emitSignal<libjami::ConversationSignal::OnConversationError>(
4694 0 : getAccountID(),
4695 : conversationId,
4696 : EVALIDFETCH,
4697 : "Negative file size, could be due to insufficient file permissions.");
4698 0 : return;
4699 : }
4700 :
4701 : // NOTE: this sendMessage is in a computation thread because
4702 : // sha3sum can take quite some time to computer if the user decide
4703 : // to send a big file
4704 14 : dht::ThreadPool::computation().run([w = weak(), conversationId, path, name, fileSize, replyTo]() {
4705 14 : if (auto shared = w.lock()) {
4706 14 : auto tid = jami::generateUID(shared->rand);
4707 14 : auto displayName = name.empty() ? path.filename().string() : name;
4708 14 : auto commitMessage = CommitMessage::fileSent(displayName, fileutils::sha3File(path), tid, fileSize, replyTo);
4709 :
4710 28 : shared->convModule()->createCommit(
4711 14 : conversationId,
4712 14 : std::move(commitMessage),
4713 : true,
4714 28 : [accId = shared->getAccountID(), conversationId, tid, displayName, path](const std::string& commitId) {
4715 : // Create a symlink to answer to re-ask
4716 28 : auto filelinkPath = fileutils::get_data_dir() / accId / "conversation_data" / conversationId
4717 42 : / getFileId(commitId, std::to_string(tid), displayName);
4718 14 : if (path != filelinkPath && !std::filesystem::is_symlink(filelinkPath)) {
4719 14 : if (!fileutils::createFileLink(filelinkPath, path, true)) {
4720 0 : JAMI_WARNING("Unable to create symlink for file transfer {} - {}. Copy file",
4721 : filelinkPath,
4722 : path);
4723 0 : std::error_code ec;
4724 0 : auto success = std::filesystem::copy_file(path, filelinkPath, ec);
4725 0 : if (ec || !success) {
4726 0 : JAMI_ERROR("Unable to copy file for file transfer {} - {}", filelinkPath, path);
4727 : // Signal to notify clients that the operation failed.
4728 : // The fileId field sends the filePath.
4729 : // libjami::DataTransferEventCode::unsupported (2) is unused elsewhere.
4730 0 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4731 0 : accId,
4732 0 : conversationId,
4733 : commitId,
4734 0 : path.string(),
4735 : uint32_t(libjami::DataTransferEventCode::invalid));
4736 : } else {
4737 : // Signal to notify clients that the file is copied and can be
4738 : // safely deleted. The fileId field sends the filePath.
4739 : // libjami::DataTransferEventCode::created (1) is unused elsewhere.
4740 0 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4741 0 : accId,
4742 0 : conversationId,
4743 : commitId,
4744 0 : path.string(),
4745 : uint32_t(libjami::DataTransferEventCode::created));
4746 : }
4747 : } else {
4748 28 : emitSignal<libjami::DataTransferSignal::DataTransferEvent>(
4749 14 : accId,
4750 14 : conversationId,
4751 : commitId,
4752 28 : path.string(),
4753 : uint32_t(libjami::DataTransferEventCode::created));
4754 : }
4755 : }
4756 14 : });
4757 28 : }
4758 14 : });
4759 : }
4760 :
4761 : void
4762 5 : JamiAccount::transferFile(const std::string& conversationId,
4763 : const std::string& path,
4764 : const std::string& deviceId,
4765 : const std::string& fileId,
4766 : const std::string& interactionId,
4767 : size_t start,
4768 : size_t end,
4769 : const std::string& sha3Sum,
4770 : uint64_t lastWriteTime,
4771 : std::function<void()> onFinished)
4772 : {
4773 5 : std::string modified;
4774 5 : if (lastWriteTime != 0) {
4775 10 : modified = fmt::format("&modified={}", lastWriteTime);
4776 : }
4777 10 : auto fid = fileId == "profile.vcf" ? fmt::format("profile.vcf?sha3={}{}", sha3Sum, modified) : fileId;
4778 5 : auto channelName = conversationId.empty()
4779 5 : ? fmt::format("{}profile.vcf?sha3={}{}", DATA_TRANSFER_SCHEME, sha3Sum, modified)
4780 10 : : fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fid);
4781 5 : std::shared_lock lkCM(connManagerMtx_);
4782 5 : if (!connectionManager_)
4783 0 : return;
4784 10 : connectionManager_->connectDevice(
4785 10 : DeviceId(deviceId),
4786 : channelName,
4787 15 : [this,
4788 : conversationId,
4789 5 : path = std::move(path),
4790 : fileId,
4791 : interactionId,
4792 : start,
4793 : end,
4794 5 : onFinished = std::move(onFinished)](std::shared_ptr<dhtnet::ChannelSocket> socket, const DeviceId&) {
4795 5 : if (!socket)
4796 0 : return;
4797 25 : dht::ThreadPool::io().run([w = weak(),
4798 5 : path = std::move(path),
4799 5 : socket = std::move(socket),
4800 5 : conversationId = std::move(conversationId),
4801 5 : fileId,
4802 5 : interactionId,
4803 : start,
4804 : end,
4805 5 : onFinished = std::move(onFinished)] {
4806 5 : if (auto shared = w.lock())
4807 5 : if (auto dt = shared->dataTransfer(conversationId))
4808 10 : dt->transferFile(socket, fileId, interactionId, path, start, end, std::move(onFinished));
4809 5 : });
4810 : });
4811 10 : }
4812 :
4813 : void
4814 13 : JamiAccount::askForFileChannel(const std::string& conversationId,
4815 : const std::string& deviceId,
4816 : const std::string& interactionId,
4817 : const std::string& fileId,
4818 : size_t start,
4819 : size_t end)
4820 : {
4821 29 : auto tryDevice = [=](const auto& did) {
4822 29 : std::shared_lock lkCM(connManagerMtx_);
4823 29 : if (!connectionManager_)
4824 0 : return;
4825 :
4826 29 : auto channelName = fmt::format("{}{}/{}/{}", DATA_TRANSFER_SCHEME, conversationId, currentDeviceId(), fileId);
4827 29 : if (start != 0 || end != 0) {
4828 6 : channelName += fmt::format("?start={}&end={}", start, end);
4829 : }
4830 : // We can avoid to negotiate new sessions, as the file notif
4831 : // probably came from an online device or last connected device.
4832 87 : connectionManager_->connectDevice(
4833 : did,
4834 : channelName,
4835 87 : [w = weak(),
4836 29 : conversationId,
4837 29 : fileId,
4838 29 : interactionId,
4839 : start](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
4840 29 : if (!channel)
4841 17 : return;
4842 12 : dht::ThreadPool::io().run([w, conversationId, channel, fileId, interactionId, start] {
4843 12 : auto shared = w.lock();
4844 12 : if (!shared)
4845 0 : return;
4846 12 : auto dt = shared->dataTransfer(conversationId);
4847 12 : if (!dt)
4848 0 : return;
4849 12 : if (interactionId.empty())
4850 0 : dt->onIncomingProfile(channel);
4851 : else
4852 12 : dt->onIncomingFileTransfer(fileId, channel, start);
4853 12 : });
4854 : },
4855 : false);
4856 42 : };
4857 :
4858 13 : if (!deviceId.empty()) {
4859 : // Only ask for device
4860 1 : tryDevice(DeviceId(deviceId));
4861 : } else {
4862 : // Only ask for connected devices. For others we will attempt
4863 : // with new peer online
4864 40 : for (const auto& m : convModule()->getConversationMembers(conversationId)) {
4865 140 : accountManager_->forEachDevice(dht::InfoHash(m.at("uri")),
4866 56 : [tryDevice](const std::shared_ptr<dht::crypto::PublicKey>& dev) {
4867 28 : tryDevice(dev->getLongId());
4868 28 : });
4869 12 : }
4870 : }
4871 13 : }
4872 :
4873 : void
4874 47 : JamiAccount::askForProfile(const std::string& conversationId, const std::string& deviceId, const std::string& memberUri)
4875 : {
4876 47 : std::shared_lock lkCM(connManagerMtx_);
4877 47 : if (!connectionManager_)
4878 0 : return;
4879 :
4880 47 : auto channelName = fmt::format("{}{}/profile/{}.vcf", DATA_TRANSFER_SCHEME, conversationId, memberUri);
4881 : // We can avoid to negotiate new sessions, as the file notif
4882 : // probably came from an online device or last connected device.
4883 188 : connectionManager_->connectDevice(
4884 94 : DeviceId(deviceId),
4885 : channelName,
4886 94 : [this, conversationId](const std::shared_ptr<dhtnet::ChannelSocket>& channel, const DeviceId&) {
4887 47 : if (!channel)
4888 1 : return;
4889 46 : dht::ThreadPool::io().run([w = weak(), conversationId, channel] {
4890 46 : if (auto shared = w.lock())
4891 46 : if (auto dt = shared->dataTransfer(conversationId))
4892 184 : dt->onIncomingProfile(channel);
4893 46 : });
4894 : },
4895 : false);
4896 47 : }
4897 :
4898 : void
4899 2489 : JamiAccount::onPeerConnected(const std::string& peerId, bool connected)
4900 : {
4901 2489 : auto isOnline = presenceManager_ && presenceManager_->isOnline(peerId);
4902 3732 : auto newState = connected ? PresenceState::CONNECTED
4903 1243 : : (isOnline ? PresenceState::AVAILABLE : PresenceState::DISCONNECTED);
4904 :
4905 2489 : runOnMainThread([w = weak(), peerId, newState] {
4906 2489 : if (auto sthis = w.lock()) {
4907 2489 : std::lock_guard lock(sthis->presenceStateMtx_);
4908 2489 : auto& state = sthis->presenceState_[peerId];
4909 2489 : if (state != newState) {
4910 2489 : state = newState;
4911 2489 : emitSignal<libjami::PresenceSignal::NewBuddyNotification>(sthis->getAccountID(),
4912 2489 : peerId,
4913 : static_cast<int>(newState),
4914 : "");
4915 : }
4916 4978 : }
4917 2489 : });
4918 2489 : }
4919 :
4920 : void
4921 746 : JamiAccount::initConnectionManager()
4922 : {
4923 746 : if (!nonSwarmTransferManager_)
4924 711 : nonSwarmTransferManager_ = std::make_shared<TransferManager>(accountID_,
4925 711 : config().username,
4926 : "",
4927 2133 : dht::crypto::getDerivedRandomEngine(rand));
4928 746 : if (!connectionManager_) {
4929 721 : auto connectionManagerConfig = std::make_shared<dhtnet::ConnectionManager::Config>();
4930 721 : connectionManagerConfig->ioContext = Manager::instance().ioContext();
4931 721 : connectionManagerConfig->dht = dht();
4932 721 : connectionManagerConfig->certStore = certStore_;
4933 721 : connectionManagerConfig->id = identity();
4934 721 : connectionManagerConfig->upnpCtrl = upnpCtrl_;
4935 721 : connectionManagerConfig->turnServer = config().turnServer;
4936 721 : connectionManagerConfig->upnpEnabled = config().upnpEnabled;
4937 721 : connectionManagerConfig->turnServerUserName = config().turnServerUserName;
4938 721 : connectionManagerConfig->turnServerPwd = config().turnServerPwd;
4939 721 : connectionManagerConfig->turnServerRealm = config().turnServerRealm;
4940 721 : connectionManagerConfig->turnEnabled = config().turnEnabled;
4941 721 : connectionManagerConfig->cachePath = cachePath_;
4942 721 : if (Manager::instance().dhtnetLogLevel > 0) {
4943 0 : connectionManagerConfig->logger = logger_;
4944 : }
4945 721 : connectionManagerConfig->factory = Manager::instance().getIceTransportFactory();
4946 721 : connectionManagerConfig->turnCache = turnCache_;
4947 721 : connectionManagerConfig->rng = std::make_unique<std::mt19937_64>(dht::crypto::getDerivedRandomEngine(rand));
4948 721 : connectionManagerConfig->legacyMode = dhtnet::LegacyMode::Disabled;
4949 721 : connectionManager_ = std::make_unique<dhtnet::ConnectionManager>(connectionManagerConfig);
4950 1442 : channelHandlers_[Uri::Scheme::SWARM] = std::make_unique<SwarmChannelHandler>(shared(),
4951 1442 : *connectionManager_.get());
4952 1442 : channelHandlers_[Uri::Scheme::GIT] = std::make_unique<ConversationChannelHandler>(shared(),
4953 1442 : *connectionManager_.get());
4954 721 : channelHandlers_[Uri::Scheme::YDOC] = std::make_unique<YdocChannelHandler>(shared(), *connectionManager_.get());
4955 721 : if (jami::Manager::instance().syncOnRegister) {
4956 1442 : channelHandlers_[Uri::Scheme::SYNC] = std::make_unique<SyncChannelHandler>(shared(),
4957 1442 : *connectionManager_.get());
4958 : }
4959 721 : channelHandlers_[Uri::Scheme::DATA_TRANSFER]
4960 1442 : = std::make_unique<TransferChannelHandler>(shared(), *connectionManager_.get());
4961 1442 : channelHandlers_[Uri::Scheme::MESSAGE] = std::make_unique<MessageChannelHandler>(
4962 721 : *connectionManager_.get(),
4963 721 : [this](const auto& cert, std::string& type, const std::string& content) {
4964 61954 : onTextMessage("", cert->issuer->getId().toString(), cert, {{type, content}});
4965 24786 : },
4966 1442 : [w = weak()](const std::string& peer, bool connected) {
4967 2490 : asio::post(*Manager::instance().ioContext(), [w, peer, connected] {
4968 2492 : if (auto acc = w.lock())
4969 2492 : acc->onPeerConnected(peer, connected);
4970 2492 : });
4971 2492 : },
4972 1442 : [w = weak()](const std::string& peer, const DeviceId& device) {
4973 1259 : dht::ThreadPool::io().run([w, peer, device] {
4974 1258 : if (auto acc = w.lock())
4975 1259 : acc->onMessageChannelReady(peer, device);
4976 1257 : });
4977 1980 : });
4978 721 : channelHandlers_[Uri::Scheme::AUTH] = std::make_unique<AuthChannelHandler>(shared(), *connectionManager_.get());
4979 :
4980 721 : if (!serviceManager_)
4981 711 : serviceManager_ = std::make_unique<ServiceManager>(idPath_);
4982 721 : channelHandlers_[Uri::Scheme::SVC_DISCOVERY]
4983 1442 : = std::make_unique<SvcDiscoveryChannelHandler>(shared(), *connectionManager_.get(), cachePath_);
4984 721 : static_cast<SvcDiscoveryChannelHandler*>(channelHandlers_[Uri::Scheme::SVC_DISCOVERY].get())
4985 721 : ->onCacheUpdated([w = weak()](const std::string& peerUri,
4986 : const DeviceId& deviceId,
4987 : const std::vector<svc_protocol::SvcInfo>&) {
4988 1258 : auto self = w.lock();
4989 1258 : if (!self)
4990 0 : return;
4991 : // Re-publish the peer's full service list. The device that just
4992 : // answered is flagged available immediately, even if its DHT
4993 : // presence announcement has not yet been observed. A cache
4994 : // update is always published, even when the peer's last service
4995 : // was removed (empty list), so listeners can clear it.
4996 1258 : auto servicesJson = self->buildPeerServicesJson(peerUri, &deviceId);
4997 2516 : emitSignal<libjami::ServiceSignal::PeerServicesReceived>(
4998 : 0u,
4999 1258 : self->getAccountID(),
5000 : peerUri,
5001 : static_cast<int>(libjami::ServiceSignal::PeerServicesStatus::OK),
5002 3765 : servicesJson.empty() ? "[]" : servicesJson);
5003 1258 : });
5004 721 : serviceManager_->setOnChanged([w = weak()]() {
5005 8 : auto self = w.lock();
5006 8 : if (!self)
5007 0 : return;
5008 8 : runOnMainThread([w]() {
5009 8 : auto self = w.lock();
5010 8 : if (!self)
5011 0 : return;
5012 8 : std::shared_lock lk(self->connManagerMtx_);
5013 8 : auto it = self->channelHandlers_.find(Uri::Scheme::SVC_DISCOVERY);
5014 8 : if (it != self->channelHandlers_.end() && it->second)
5015 8 : static_cast<SvcDiscoveryChannelHandler*>(it->second.get())->broadcastServiceUpdate();
5016 8 : });
5017 8 : });
5018 721 : channelHandlers_[Uri::Scheme::SVC_TUNNEL]
5019 1442 : = std::make_unique<SvcTunnelChannelHandler>(shared(),
5020 721 : *connectionManager_.get(),
5021 2163 : Manager::instance().ioContext());
5022 :
5023 : #if TARGET_OS_IOS
5024 : connectionManager_->oniOSConnected([&](const std::string& connType, dht::InfoHash peer_h) {
5025 : if ((connType == "videoCall" || connType == "audioCall") && jami::Manager::instance().isIOSExtension) {
5026 : bool hasVideo = connType == "videoCall";
5027 : emitSignal<libjami::ConversationSignal::CallConnectionRequest>("", peer_h.toString(), hasVideo);
5028 : return true;
5029 : }
5030 : return false;
5031 : });
5032 : #endif
5033 721 : }
5034 746 : }
5035 :
5036 : void
5037 1039 : JamiAccount::updateUpnpController()
5038 : {
5039 1039 : if (upnpCtrl_ and (not config().upnpEnabled or not isUsable()))
5040 159 : releaseDhtUpnpMapping();
5041 1039 : Account::updateUpnpController();
5042 1039 : if (connectionManager_) {
5043 44 : auto config = connectionManager_->getConfig();
5044 44 : if (config)
5045 44 : config->upnpCtrl = upnpCtrl_;
5046 44 : }
5047 1039 : }
5048 :
5049 : } // namespace jami
|