Ring Daemon
Loading...
Searching...
No Matches
contact_list.cpp
Go to the documentation of this file.
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#include "contact_list.h"
18#include "logger.h"
19#include "jamiaccount.h"
20#include "fileutils.h"
21
22#include "manager.h"
23#ifdef ENABLE_PLUGIN
25#endif
26
27#include "account_const.h"
28
29#include <fstream>
30#include <gnutls/ocsp.h>
31
32namespace jami {
33
34ContactList::ContactList(const std::string& accountId,
35 const std::shared_ptr<crypto::Certificate>& cert,
36 const std::filesystem::path& path,
38 : accountId_(accountId)
39 , path_(path)
40 , callbacks_(std::move(cb))
41{
42 if (cert) {
43 trust_ = std::make_unique<dhtnet::tls::TrustStore>(jami::Manager::instance().certStore(accountId_));
44 accountTrust_.add(*cert);
45 }
46}
47
49
50void
52{
53 loadContacts();
54 loadTrustRequests();
55 loadKnownDevices();
56}
57
58void
60{
62 saveTrustRequests();
63 saveKnownDevices();
64}
65
66bool
67ContactList::setCertificateStatus(const std::string& cert_id, const dhtnet::tls::TrustStore::PermissionStatus status)
68{
69 std::unique_lock lk(mutex_);
70 if (contacts_.find(dht::InfoHash(cert_id)) != contacts_.end()) {
71 JAMI_LOG("[Account {}] [Contacts] Unable to set certificate status for existing contacts {}",
72 accountId_,
73 cert_id);
74 return false;
75 }
76 return trust_->setCertificateStatus(cert_id, status);
77}
78
79bool
80ContactList::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
81 dhtnet::tls::TrustStore::PermissionStatus status,
82 bool local)
83{
84 return trust_->setCertificateStatus(cert, status, local);
85}
86
87bool
88ContactList::addContact(const dht::InfoHash& h, bool confirmed, const std::string& conversationId)
89{
90 std::unique_lock lk(mutex_);
91 JAMI_WARNING("[Account {}] [Contacts] addContact: {}, conversation: {}", accountId_, h, conversationId);
92 auto c = contacts_.find(h);
93 if (c == contacts_.end())
94 c = contacts_.emplace(h, Contact {}).first;
95 else if (c->second.isActive() and c->second.confirmed == confirmed && c->second.conversationId == conversationId)
96 return false;
97 c->second.added = std::time(nullptr);
98 // NOTE: because we can re-add a contact after removing it
99 // we should reset removed (as not removed anymore). This fix isActive()
100 // if addContact is called just after removeContact during the same second
101 c->second.removed = 0;
102 c->second.conversationId = conversationId;
103 c->second.confirmed |= confirmed;
104 auto hStr = h.toString();
105 trust_->setCertificateStatus(hStr, dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
106 saveContacts();
107 lk.unlock();
108 callbacks_.contactAdded(hStr, c->second.confirmed);
109 return true;
110}
111
112bool
113ContactList::updateConversation(const dht::InfoHash& h, const std::string& conversationId, bool added)
114{
115 std::lock_guard lk(mutex_);
116 auto c = contacts_.find(h);
117 if (c != contacts_.end() && c->second.conversationId != conversationId) {
118 c->second.conversationId = conversationId;
119 if (added) {
120 c->second.added = std::time(nullptr);
121 }
122 saveContacts();
123 return true;
124 }
125 return false;
126}
127
128bool
129ContactList::removeContact(const dht::InfoHash& h, bool ban)
130{
131 std::unique_lock lk(mutex_);
132 JAMI_WARNING("[Account {}] [Contacts] removeContact: {} (banned: {})", accountId_, h, ban);
133 auto c = contacts_.find(h);
134 if (c == contacts_.end())
135 c = contacts_.emplace(h, Contact {}).first;
136 c->second.removed = std::time(nullptr);
137 c->second.confirmed = false;
138 c->second.banned = ban;
139 c->second.conversationId = "";
140 auto uri = h.toString();
141 trust_->setCertificateStatus(uri,
142 ban ? dhtnet::tls::TrustStore::PermissionStatus::BANNED
143 : dhtnet::tls::TrustStore::PermissionStatus::UNDEFINED);
144 if (trustRequests_.erase(h) > 0)
145 saveTrustRequests();
146 saveContacts();
147 lk.unlock();
148#ifdef ENABLE_PLUGIN
149 auto filename = path_.filename().string();
150 jami::Manager::instance().getJamiPluginManager().getChatServicesManager().cleanChatSubjects(filename, uri);
151#endif
152 callbacks_.contactRemoved(uri, ban);
153 return true;
154}
155
156bool
158{
159 std::unique_lock lk(mutex_);
160 auto c = contacts_.find(h);
161 if (c == contacts_.end())
162 return false;
163 c->second.conversationId = "";
164 saveContacts();
165 return true;
166}
167
168std::map<std::string, std::string>
169ContactList::getContactDetails(const dht::InfoHash& h) const
170{
171 std::unique_lock lk(mutex_);
172 const auto c = contacts_.find(h);
173 if (c == std::end(contacts_)) {
174 JAMI_WARNING("[Account {}] [Contacts] Contact '{}' not found", accountId_, h.to_view());
175 return {};
176 }
177
178 auto details = c->second.toMap();
179 if (not details.empty())
180 details["id"] = c->first.toString();
181
182 return details;
183}
184
185std::optional<Contact>
186ContactList::getContactInfo(const dht::InfoHash& h) const
187{
188 const auto c = contacts_.find(h);
189 if (c == std::end(contacts_)) {
190 JAMI_WARNING("[Account {}] [Contacts] Contact '{}' not found", accountId_, h.to_view());
191 return {};
192 }
193 return c->second;
194}
195
196const std::map<dht::InfoHash, Contact>&
198{
199 return contacts_;
200}
201
202void
203ContactList::setContacts(const std::map<dht::InfoHash, Contact>& contacts)
204{
205 JAMI_LOG("[Account {}] [Contacts] replacing contact list (old: {} new: {})",
206 accountId_,
207 contacts_.size(),
208 contacts.size());
209 contacts_ = contacts;
210 saveContacts();
211 // Set contacts is used when creating a new device, so just announce new contacts
212 for (auto& peer : contacts)
213 if (peer.second.isActive())
214 callbacks_.contactAdded(peer.first.toString(), peer.second.confirmed);
215}
216
217void
218ContactList::updateContact(const dht::InfoHash& id, const Contact& contact, bool emit)
219{
220 if (not id) {
221 JAMI_ERROR("[Account {}] [Contacts] updateContact: invalid contact ID", accountId_);
222 return;
223 }
224 bool stateChanged {false};
225 auto c = contacts_.find(id);
226 if (c == contacts_.end()) {
227 // JAMI_DBG("[Contacts] New contact: %s", id.toString().c_str());
228 c = contacts_.emplace(id, contact).first;
229 stateChanged = c->second.isActive() or c->second.isBanned();
230 } else {
231 // JAMI_DBG("[Contacts] Updated contact: %s", id.toString().c_str());
232 stateChanged = c->second.update(contact);
233 }
234 if (stateChanged) {
235 {
236 std::lock_guard lk(mutex_);
237 if (trustRequests_.erase(id) > 0)
238 saveTrustRequests();
239 }
240 if (c->second.isActive()) {
241 trust_->setCertificateStatus(id.toString(), dhtnet::tls::TrustStore::PermissionStatus::ALLOWED);
242 if (emit)
243 callbacks_.contactAdded(id.toString(), c->second.confirmed);
244 } else {
245 if (c->second.banned)
246 trust_->setCertificateStatus(id.toString(), dhtnet::tls::TrustStore::PermissionStatus::BANNED);
247 if (emit)
248 callbacks_.contactRemoved(id.toString(), c->second.banned);
249 }
250 }
251}
252
253void
254ContactList::loadContacts()
255{
256 decltype(contacts_) contacts;
257 try {
258 std::lock_guard fileLock(dhtnet::fileutils::getFileLock(path_ / "contacts"));
259 // read file
260 auto file = fileutils::loadFile("contacts", path_);
261 // load values
262 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
263 oh.get().convert(contacts);
264 } catch (const std::exception& e) {
265 JAMI_WARNING("[Account {}] [Contacts] Error loading contacts: {}", accountId_, e.what());
266 return;
267 }
268
269 JAMI_WARNING("[Account {}] [Contacts] Loaded {} contacts", accountId_, contacts.size());
270 for (auto& peer : contacts)
271 updateContact(peer.first, peer.second, false);
272}
273
274void
276{
277 JAMI_LOG("[Account {}] [Contacts] saving {} contacts", accountId_, contacts_.size());
278 std::lock_guard fileLock(dhtnet::fileutils::getFileLock(path_ / "contacts"));
279 std::ofstream file(path_ / "contacts", std::ios::trunc | std::ios::binary);
280 msgpack::pack(file, contacts_);
281}
282
283void
284ContactList::saveTrustRequests() const
285{
286 // mutex_ MUST BE locked
287 std::ofstream file(path_ / "incomingTrustRequests", std::ios::trunc | std::ios::binary);
288 msgpack::pack(file, trustRequests_);
289}
290
291void
292ContactList::loadTrustRequests()
293{
294 if (!std::filesystem::is_regular_file(fileutils::getFullPath(path_, "incomingTrustRequests")))
295 return;
296 std::map<dht::InfoHash, TrustRequest> requests;
297 try {
298 // read file
299 auto file = fileutils::loadFile("incomingTrustRequests", path_);
300 // load values
301 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
302 oh.get().convert(requests);
303 } catch (const std::exception& e) {
304 JAMI_WARNING("[Account {}] [Contacts] Error loading trust requests: {}", accountId_, e.what());
305 return;
306 }
307
308 JAMI_WARNING("[Account {}] [Contacts] Loaded {} contact requests", accountId_, requests.size());
309 for (auto& tr : requests)
311 tr.second.device,
312 tr.second.received,
313 false,
314 tr.second.conversationId,
315 std::move(tr.second.payload));
316}
317
318bool
320 const std::shared_ptr<dht::crypto::PublicKey>& peer_device,
321 time_t received,
322 bool confirm,
323 const std::string& conversationId,
324 std::vector<uint8_t>&& payload)
325{
326 bool accept = false;
327 // Check existing contact
328 std::unique_lock lk(mutex_);
329 auto contact = contacts_.find(peer_account);
330 bool active = false;
331 if (contact != contacts_.end()) {
332 // Banned contact: discard request
333 if (contact->second.isBanned())
334 return false;
335
336 if (contact->second.isActive()) {
337 active = true;
338 // Send confirmation
339 if (not confirm)
340 accept = true;
341 if (not contact->second.confirmed) {
342 contact->second.confirmed = true;
343 saveContacts();
344 callbacks_.contactAdded(peer_account.toString(), true);
345 }
346 }
347 }
348 if (not active) {
349 auto req = trustRequests_.find(peer_account);
350 if (req == trustRequests_.end()) {
351 // Add trust request
352 req = trustRequests_.emplace(peer_account, TrustRequest {peer_device, conversationId, received, payload})
353 .first;
354 } else {
355 // Update trust request
356 if (received > req->second.received) {
357 req->second.device = peer_device;
358 req->second.conversationId = conversationId;
359 req->second.received = received;
360 req->second.payload = payload;
361 } else {
362 JAMI_LOG("[Account {}] [Contacts] Ignoring outdated trust request from {}", accountId_, peer_account);
363 }
364 }
365 saveTrustRequests();
366 }
367 lk.unlock();
368 // Note: call JamiAccount's callback to build ConversationRequest anyway
369 if (!confirm)
370 callbacks_.trustRequest(peer_account.toString(), conversationId, std::move(payload), received);
371 else if (active) {
372 // Only notify if confirmed + not removed
373 callbacks_.onConfirmation(peer_account.toString(), conversationId);
374 }
375 return accept;
376}
377
378/* trust requests */
379
380std::vector<std::map<std::string, std::string>>
382{
383 using Map = std::map<std::string, std::string>;
384 std::vector<Map> ret;
385 std::lock_guard lk(mutex_);
386 ret.reserve(trustRequests_.size());
387 for (const auto& r : trustRequests_) {
388 ret.emplace_back(Map {{libjami::Account::TrustRequest::FROM, r.first.toString()},
389 {libjami::Account::TrustRequest::RECEIVED, std::to_string(r.second.received)},
390 {libjami::Account::TrustRequest::CONVERSATIONID, r.second.conversationId},
392 std::string(r.second.payload.begin(), r.second.payload.end())}});
393 }
394 return ret;
395}
396
397std::map<std::string, std::string>
398ContactList::getTrustRequest(const dht::InfoHash& from) const
399{
400 using Map = std::map<std::string, std::string>;
401 std::lock_guard lk(mutex_);
402 auto r = trustRequests_.find(from);
403 if (r == trustRequests_.end())
404 return {};
405 return Map {{libjami::Account::TrustRequest::FROM, r->first.toString()},
406 {libjami::Account::TrustRequest::RECEIVED, std::to_string(r->second.received)},
407 {libjami::Account::TrustRequest::CONVERSATIONID, r->second.conversationId},
409 std::string(r->second.payload.begin(), r->second.payload.end())}};
410}
411
412bool
413ContactList::acceptTrustRequest(const dht::InfoHash& from)
414{
415 // The contact sent us a TR so we are in its contact list
416 std::unique_lock lk(mutex_);
417 auto i = trustRequests_.find(from);
418 if (i == trustRequests_.end())
419 return false;
420 auto convId = i->second.conversationId;
421 // Clear trust request
422 trustRequests_.erase(i);
423 saveTrustRequests();
424 lk.unlock();
425 addContact(from, true, convId);
426 return true;
427}
428
429void
430ContactList::acceptConversation(const std::string& convId, const std::string& deviceId)
431{
432 if (callbacks_.acceptConversation)
433 callbacks_.acceptConversation(convId, deviceId);
434}
435
436bool
437ContactList::discardTrustRequest(const dht::InfoHash& from)
438{
439 std::lock_guard lk(mutex_);
440 if (trustRequests_.erase(from) > 0) {
441 saveTrustRequests();
442 return true;
443 }
444 return false;
445}
446
447void
448ContactList::loadKnownDevices()
449{
450 auto& certStore = jami::Manager::instance().certStore(accountId_);
451 try {
452 // read file
453 auto file = fileutils::loadFile("knownDevices", path_);
454 // load values
455 msgpack::object_handle oh = msgpack::unpack((const char*) file.data(), file.size());
456
457 std::map<dht::PkId, std::pair<std::string, uint64_t>> knownDevices;
458 oh.get().convert(knownDevices);
459 for (const auto& d : knownDevices) {
460 if (auto crt = certStore.getCertificate(d.first.toString())) {
461 if (not foundAccountDevice(crt, d.second.first, clock::from_time_t(d.second.second), false))
462 JAMI_WARNING("[Account {}] [Contacts] Unable to add device {}", accountId_, d.first);
463 } else {
464 JAMI_WARNING("[Account {}] [Contacts] Unable to find certificate for device {}", accountId_, d.first);
465 }
466 }
467 if (not knownDevices.empty()) {
468 callbacks_.devicesChanged(knownDevices_);
469 }
470 } catch (const std::exception& e) {
471 JAMI_WARNING("[Account {}] [Contacts] Error loading devices: {}", accountId_, e.what());
472 return;
473 }
474}
475
476void
477ContactList::saveKnownDevices() const
478{
479 std::ofstream file(path_ / "knownDevices", std::ios::trunc | std::ios::binary);
480
481 std::map<dht::PkId, std::pair<std::string, uint64_t>> devices;
482 for (const auto& id : knownDevices_) {
483 devices.emplace(id.first, std::make_pair(id.second.name, clock::to_time_t(id.second.last_sync)));
484 }
485
486 msgpack::pack(file, devices);
487}
488
489void
490ContactList::foundAccountDevice(const dht::PkId& device, const std::string& name, const time_point& updated)
491{
492 // insert device
493 auto it = knownDevices_.emplace(device, KnownDevice {{}, name, updated});
494 if (it.second) {
495 JAMI_LOG("[Account {}] [Contacts] Found account device: {} {}", accountId_, name, device);
496 saveKnownDevices();
497 callbacks_.devicesChanged(knownDevices_);
498 } else {
499 // update device name
500 if (not name.empty() and it.first->second.name != name) {
501 JAMI_LOG("[Account {}] [Contacts] Updating device name: {} {}", accountId_, name, device);
502 it.first->second.name = name;
503 saveKnownDevices();
504 callbacks_.devicesChanged(knownDevices_);
505 }
506 }
507}
508
509bool
510ContactList::foundAccountDevice(const std::shared_ptr<dht::crypto::Certificate>& crt,
511 const std::string& name,
512 const time_point& updated,
513 bool notify)
514{
515 if (not crt)
516 return false;
517
518 auto id = crt->getLongId();
519
520 // match certificate chain
521 auto verifyResult = accountTrust_.verify(*crt);
522 if (not verifyResult) {
523 JAMI_WARNING("[Account {}] [Contacts] Found invalid account device: {:s}: {:s}",
524 accountId_,
525 id,
526 verifyResult.toString());
527 return false;
528 }
529
530 // insert device
531 auto it = knownDevices_.emplace(id, KnownDevice {crt, name, updated});
532 if (it.second) {
533 JAMI_LOG("[Account {}] [Contacts] Found account device: {} {}", accountId_, name, id);
534 jami::Manager::instance().certStore(accountId_).pinCertificate(crt);
535 if (crt->ocspResponse) {
536 unsigned int status = crt->ocspResponse->getCertificateStatus();
537 if (status == GNUTLS_OCSP_CERT_REVOKED) {
538 JAMI_ERROR("[Account {}] Certificate {} has revoked OCSP status", accountId_, id);
539 trust_->setCertificateStatus(crt, dhtnet::tls::TrustStore::PermissionStatus::BANNED, false);
540 }
541 }
542 if (notify) {
543 saveKnownDevices();
544 callbacks_.devicesChanged(knownDevices_);
545 }
546 } else {
547 // update device name
548 if (not name.empty() and it.first->second.name != name) {
549 JAMI_LOG("[Account {}] [Contacts] updating device name: {} {}", accountId_, name, id);
550 it.first->second.name = name;
551 if (notify) {
552 saveKnownDevices();
553 callbacks_.devicesChanged(knownDevices_);
554 }
555 }
556 }
557 return true;
558}
559
560bool
561ContactList::removeAccountDevice(const dht::PkId& device)
562{
563 if (knownDevices_.erase(device) > 0) {
564 saveKnownDevices();
565 return true;
566 }
567 return false;
568}
569
570void
571ContactList::setAccountDeviceName(const dht::PkId& device, const std::string& name)
572{
573 auto dev = knownDevices_.find(device);
574 if (dev != knownDevices_.end()) {
575 if (dev->second.name != name) {
576 dev->second.name = name;
577 saveKnownDevices();
578 callbacks_.devicesChanged(knownDevices_);
579 }
580 }
581}
582
583std::string
584ContactList::getAccountDeviceName(const dht::PkId& device) const
585{
586 auto dev = knownDevices_.find(device);
587 if (dev != knownDevices_.end()) {
588 return dev->second.name;
589 }
590 return {};
591}
592
595{
597 sync_data.date = clock::now().time_since_epoch().count();
598 // sync_data.device_name = deviceName_;
599 sync_data.peers = getContacts();
600
601 static constexpr size_t MAX_TRUST_REQUESTS = 20;
602 std::lock_guard lk(mutex_);
603 if (trustRequests_.size() <= MAX_TRUST_REQUESTS)
604 for (const auto& req : trustRequests_)
605 sync_data.trust_requests
606 .emplace(req.first,
607 TrustRequest {req.second.device, req.second.conversationId, req.second.received, {}});
608 else {
609 size_t inserted = 0;
610 auto req = trustRequests_.lower_bound(dht::InfoHash::getRandom());
611 while (inserted++ < MAX_TRUST_REQUESTS) {
612 if (req == trustRequests_.end())
613 req = trustRequests_.begin();
614 sync_data.trust_requests
615 .emplace(req->first,
616 TrustRequest {req->second.device, req->second.conversationId, req->second.received, {}});
617 ++req;
618 }
619 }
620
621 for (const auto& dev : knownDevices_) {
622 if (!dev.second.certificate) {
623 JAMI_WARNING("[Account {}] [Contacts] No certificate found for {}", accountId_, dev.first);
624 continue;
625 }
626 sync_data.devices.emplace(dev.second.certificate->getLongId(), KnownDeviceSync {dev.second.name});
627 }
628 return sync_data;
629}
630
631bool
632ContactList::syncDevice(const dht::PkId& device, const time_point& syncDate)
633{
634 auto it = knownDevices_.find(device);
635 if (it == knownDevices_.end()) {
636 JAMI_WARNING("[Account {}] [Contacts] Dropping sync data from unknown device", accountId_);
637 return false;
638 }
639 if (it->second.last_sync >= syncDate) {
640 JAMI_LOG("[Account {}] [Contacts] Dropping outdated sync data", accountId_);
641 return false;
642 }
643 it->second.last_sync = syncDate;
644 return true;
645}
646
647} // namespace jami
bool removeAccountDevice(const dht::PkId &device)
std::vector< std::map< std::string, std::string > > getTrustRequests() const
ContactList(const std::string &accountId, const std::shared_ptr< crypto::Certificate > &cert, const std::filesystem::path &path, OnChangeCallback cb)
void foundAccountDevice(const dht::PkId &device, const std::string &name={}, const time_point &last_sync=time_point::min())
bool removeContactConversation(const dht::InfoHash &)
bool addContact(const dht::InfoHash &, bool confirmed=false, const std::string &conversationId="")
clock::time_point time_point
void setContacts(const std::map< dht::InfoHash, Contact > &)
void saveContacts() const
Should be called only after updateContact.
bool onTrustRequest(const dht::InfoHash &peer_account, const std::shared_ptr< dht::crypto::PublicKey > &peer_device, time_t received, bool confirm, const std::string &conversationId, std::vector< uint8_t > &&payload)
Inform of a new contact request.
std::string getAccountDeviceName(const dht::PkId &device) const
DeviceSync getSyncData() const
void setAccountDeviceName(const dht::PkId &device, const std::string &name)
bool acceptTrustRequest(const dht::InfoHash &from)
bool discardTrustRequest(const dht::InfoHash &from)
std::map< std::string, std::string > getContactDetails(const dht::InfoHash &) const
void updateContact(const dht::InfoHash &, const Contact &, bool emit=true)
bool setCertificateStatus(const std::string &cert_id, const dhtnet::tls::TrustStore::PermissionStatus status)
std::optional< Contact > getContactInfo(const dht::InfoHash &) const
const std::map< dht::InfoHash, Contact > & getContacts() const
void acceptConversation(const std::string &convId, const std::string &deviceId="")
std::map< std::string, std::string > getTrustRequest(const dht::InfoHash &from) const
bool updateConversation(const dht::InfoHash &h, const std::string &conversationId, bool added=false)
bool removeContact(const dht::InfoHash &, bool ban)
static LIBJAMI_TEST_EXPORT Manager & instance()
Definition manager.cpp:694
dhtnet::tls::CertificateStore & certStore(const std::string &accountId) const
Definition manager.cpp:3339
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
#define JAMI_LOG(formatstr,...)
Definition logger.h:237
Definition Address.h:25
std::vector< uint8_t > loadFile(const std::filesystem::path &path, const std::filesystem::path &default_dir)
Read the full content of a file at path.
std::filesystem::path getFullPath(const std::filesystem::path &base, const std::filesystem::path &path)
If path is relative, it is appended to base.
static constexpr std::string_view toString(AuthDecodingState state)
void emitSignal(Args... args)
Definition jami_signal.h:64
static constexpr const char PAYLOAD[]
static constexpr const char RECEIVED[]
static constexpr const char CONVERSATIONID[]
static constexpr const char FROM[]
OnIncomingTrustRequest trustRequest
OnAcceptConversation acceptConversation
std::shared_ptr< dht::crypto::PublicKey > device