mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-04-21 21:52:03 +02:00

- Declares global logging categories for libclient and the app - Introduces some macros for categorized logging - Removes the noisy namedirectory logs by default - Logs file/line number URIs in debug mode Change-Id: I9dadadc6e93ef91cc70d206b7225aeb7a06f8773
1283 lines
46 KiB
C++
1283 lines
46 KiB
C++
/****************************************************************************
|
|
* Copyright (C) 2017-2024 Savoir-faire Linux Inc. *
|
|
* Author: Nicolas Jäger <nicolas.jager@savoirfairelinux.com> *
|
|
* Author: Sébastien Blin <sebastien.blin@savoirfairelinux.com> *
|
|
* Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> *
|
|
* Author: Hugo Lefeuvre <hugo.lefeuvre@savoirfairelinux.com> *
|
|
* Author: Kateryna Kostiuk <kateryna.kostiuk@savoirfairelinux.com> *
|
|
* Author: Andreas Traczyk <andreas.traczyk@savoirfairelinux.com> *
|
|
* *
|
|
* This library is free software; you can redistribute it and/or *
|
|
* modify it under the terms of the GNU Lesser General Public *
|
|
* License as published by the Free Software Foundation; either *
|
|
* version 2.1 of the License, or (at your option) any later version. *
|
|
* *
|
|
* This library is distributed in the hope that it will be useful, *
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of *
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU *
|
|
* Lesser General Public License for more details. *
|
|
* *
|
|
* You should have received a copy of the GNU General Public License *
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>. *
|
|
***************************************************************************/
|
|
|
|
#include "api/contactmodel.h"
|
|
|
|
#include <QThreadPool>
|
|
|
|
// LRC
|
|
#include "api/account.h"
|
|
#include "api/contact.h"
|
|
#include "api/conversationmodel.h"
|
|
#include "api/accountmodel.h"
|
|
#include "api/callmodel.h"
|
|
#include "callbackshandler.h"
|
|
#include "uri.h"
|
|
#include "vcard.h"
|
|
#include "typedefs.h"
|
|
|
|
#include "authority/daemon.h"
|
|
#include "authority/storagehelper.h"
|
|
|
|
// Dbus
|
|
#include "dbus/configurationmanager.h"
|
|
#include "dbus/presencemanager.h"
|
|
|
|
#include "account_const.h"
|
|
|
|
// Std
|
|
#include <algorithm>
|
|
|
|
namespace lrc {
|
|
|
|
using namespace api;
|
|
|
|
class ContactModelPimpl : public QObject
|
|
{
|
|
Q_OBJECT
|
|
public:
|
|
ContactModelPimpl(const ContactModel& linked,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController);
|
|
|
|
~ContactModelPimpl();
|
|
|
|
/**
|
|
* Fills the contacts based on database's conversations
|
|
* @return if the method succeeds
|
|
*/
|
|
bool fillWithSIPContacts();
|
|
|
|
/**
|
|
* Fills the contacts based on database's conversations
|
|
* @return if the method succeeds
|
|
*/
|
|
bool fillWithJamiContacts();
|
|
|
|
/**
|
|
* Add a contact::Info to contacts.
|
|
* @note: the contactId must corresponds to a profile in the database.
|
|
* @param contactId
|
|
* @param type
|
|
* @param displayName
|
|
* @param banned whether contact is banned or not
|
|
* @param conversationId linked swarm if one
|
|
*/
|
|
void addToContacts(const QString& contactId,
|
|
const profile::Type& type,
|
|
const QString& displayName = "",
|
|
bool banned = false,
|
|
const QString& conversationId = "");
|
|
/**
|
|
* Helpers for searchContact. Search for a given classic or SIP contact.
|
|
*/
|
|
void searchContact(const URI& query);
|
|
void searchSipContact(const URI& query);
|
|
|
|
/**
|
|
* Update temporary item to display a given message about a given uri.
|
|
*/
|
|
void updateTemporaryMessage(const QString& mes);
|
|
|
|
/**
|
|
* Check if equivalent uri exist in contact
|
|
*/
|
|
QString sipUriReceivedFilter(const QString& uri);
|
|
|
|
// Helpers
|
|
const BehaviorController& behaviorController;
|
|
const ContactModel& linked;
|
|
Database& db;
|
|
const CallbacksHandler& callbacksHandler;
|
|
|
|
// Containers
|
|
ContactModel::ContactInfoMap contacts;
|
|
ContactModel::ContactInfoMap searchResult;
|
|
QList<QString> bannedContacts;
|
|
QString searchQuery;
|
|
std::mutex contactsMtx_;
|
|
std::mutex bannedContactsMtx_;
|
|
QString searchStatus_ {};
|
|
QMap<QString, QString> nonContactLookup_;
|
|
|
|
QThreadPool profileThreadPool;
|
|
|
|
public Q_SLOTS:
|
|
/**
|
|
* Listen CallbacksHandler when a presence update occurs
|
|
* @param accountId
|
|
* @param contactUri
|
|
* @param status
|
|
*/
|
|
void slotNewBuddySubscription(const QString& accountId, const QString& uri, bool status);
|
|
|
|
/**
|
|
* Listen CallbacksHandler when a contact is added
|
|
* @param accountId
|
|
* @param contactUri
|
|
* @param confirmed
|
|
*/
|
|
void slotContactAdded(const QString& accountId, const QString& contactUri, bool confirmed);
|
|
|
|
/**
|
|
* Listen CallbacksHandler when a contact is removed
|
|
* @param accountId
|
|
* @param contactUri
|
|
* @param banned
|
|
*/
|
|
void slotContactRemoved(const QString& accountId, const QString& contactUri, bool banned);
|
|
|
|
/**
|
|
* Listen CallbacksHandler when a registeredName is found
|
|
* @param accountId account linked
|
|
* @param status (0 = SUCCESS, 1 = Not found, 2 = Network error)
|
|
* @param uri of the contact found
|
|
* @param registeredName of the contact found
|
|
*/
|
|
void slotRegisteredNameFound(const QString& accountId,
|
|
int status,
|
|
const QString& uri,
|
|
const QString& registeredName);
|
|
|
|
/**
|
|
* Listen from callModel when an new call is available.
|
|
* @param fromId
|
|
* @param callId
|
|
* @param displayName
|
|
* @param isOutgoing
|
|
* @param toUri
|
|
*/
|
|
void slotNewCall(const QString& fromId,
|
|
const QString& callId,
|
|
const QString& displayname,
|
|
bool isOutgoing,
|
|
const QString& toUri);
|
|
|
|
/**
|
|
* Listen from callbacksHandler for new account interaction and add pending contact if not present
|
|
* @param accountId
|
|
* @param msgId
|
|
* @param peerId
|
|
* @param payloads
|
|
*/
|
|
void slotNewAccountMessage(const QString& accountId,
|
|
const QString& peerId,
|
|
const QString& msgId,
|
|
const MapStringString& payloads);
|
|
|
|
/**
|
|
* Listen from callbacksHandler to know when a file transfer interaction is incoming
|
|
* @param fileId Daemon's ID for incoming transfer
|
|
* @param transferInfo DataTransferInfo structure from daemon
|
|
*/
|
|
void slotNewAccountTransfer(const QString& fileId, datatransfer::Info info);
|
|
|
|
/**
|
|
* Listen from daemon to know when a VCard is received
|
|
* @param accountId
|
|
* @param peer
|
|
* @param vCard
|
|
*/
|
|
void slotProfileReceived(const QString& accountId, const QString& peer, const QString& vCard);
|
|
|
|
/**
|
|
* Listen from daemon to know when a user search completed
|
|
* @param accountId
|
|
* @param status
|
|
* @param query
|
|
* @param result
|
|
*/
|
|
void slotUserSearchEnded(const QString& accountId,
|
|
int status,
|
|
const QString& query,
|
|
const VectorMapStringString& result);
|
|
};
|
|
|
|
using namespace authority;
|
|
|
|
ContactModel::ContactModel(const account::Info& owner,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController)
|
|
: owner(owner)
|
|
, pimpl_(std::make_unique<ContactModelPimpl>(*this, db, callbacksHandler, behaviorController))
|
|
{}
|
|
|
|
ContactModel::~ContactModel() {}
|
|
|
|
const ContactModel::ContactInfoMap&
|
|
ContactModel::getAllContacts() const
|
|
{
|
|
return pimpl_->contacts;
|
|
}
|
|
|
|
time_t
|
|
ContactModel::getAddedTs(const QString& contactUri) const
|
|
{
|
|
MapStringString details = ConfigurationManager::instance().getContactDetails(owner.id,
|
|
contactUri);
|
|
auto itAdded = details.find("added");
|
|
if (itAdded == details.end())
|
|
return 0;
|
|
return itAdded.value().toUInt();
|
|
}
|
|
|
|
void
|
|
ContactModel::addContact(contact::Info contactInfo)
|
|
{
|
|
auto& profile = contactInfo.profileInfo;
|
|
// If passed contact is a banned contact, call the daemon to unban it
|
|
auto it = std::find(pimpl_->bannedContacts.begin(), pimpl_->bannedContacts.end(), profile.uri);
|
|
if (it != pimpl_->bannedContacts.end()) {
|
|
LC_DBG << QString("Unban-ing contact %1").arg(profile.uri);
|
|
ConfigurationManager::instance().addContact(owner.id, profile.uri);
|
|
// bannedContacts will be updated in slotContactAdded
|
|
return;
|
|
}
|
|
|
|
if ((owner.profileInfo.type != profile.type)
|
|
and (profile.type == profile::Type::JAMI or profile.type == profile::Type::SIP)) {
|
|
LC_DBG << "ContactModel::addContact, types invalid.";
|
|
return;
|
|
}
|
|
|
|
MapStringString details = ConfigurationManager::instance()
|
|
.getContactDetails(owner.id, contactInfo.profileInfo.uri);
|
|
|
|
// if contactInfo is already a contact for the daemon, type should be equals to RING
|
|
// if the user add a temporary item for a SIP account, should be directly transformed
|
|
if ((!details.empty() && details.value("removed") == "0")
|
|
|| (profile.type == profile::Type::TEMPORARY
|
|
&& owner.profileInfo.type == profile::Type::SIP))
|
|
profile.type = owner.profileInfo.type;
|
|
|
|
switch (profile.type) {
|
|
case profile::Type::TEMPORARY: {
|
|
// make a temporary contact available for UI elements, it will be upgraded to
|
|
// its corresponding type after receiving contact added signal
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
contactInfo.profileInfo.type = profile::Type::PENDING;
|
|
pimpl_->contacts.insert(contactInfo.profileInfo.uri, contactInfo);
|
|
ConfigurationManager::instance().addContact(owner.id, profile.uri);
|
|
ConfigurationManager::instance()
|
|
.sendTrustRequest(owner.id,
|
|
profile.uri,
|
|
owner.accountModel->accountVCard(owner.id).toUtf8());
|
|
return;
|
|
}
|
|
case profile::Type::PENDING:
|
|
return;
|
|
case profile::Type::JAMI:
|
|
case profile::Type::SIP:
|
|
break;
|
|
case profile::Type::INVALID:
|
|
case profile::Type::COUNT__:
|
|
default:
|
|
LC_DBG << "ContactModel::addContact, cannot add contact with invalid type.";
|
|
return;
|
|
}
|
|
|
|
storage::createOrUpdateProfile(owner.id, profile, true);
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
auto iter = pimpl_->contacts.find(contactInfo.profileInfo.uri);
|
|
if (iter == pimpl_->contacts.end())
|
|
pimpl_->contacts.insert(iter, contactInfo.profileInfo.uri, contactInfo);
|
|
else {
|
|
// On non-DBus platform, contactInfo.profileInfo.type may be wrong as the contact
|
|
// may be trusted already. We must use Profile::Type from pimpl_->contacts
|
|
// and not from contactInfo so we cannot revert a contact back to PENDING.
|
|
contactInfo.profileInfo.type = iter->profileInfo.type;
|
|
iter->profileInfo = contactInfo.profileInfo;
|
|
}
|
|
}
|
|
Q_EMIT profileUpdated(profile.uri);
|
|
if (profile.type == profile::Type::SIP)
|
|
Q_EMIT contactAdded(profile.uri);
|
|
else {
|
|
PresenceManager::instance().subscribeBuddy(owner.id, profile.uri, true);
|
|
ConfigurationManager::instance().lookupAddress(owner.id, "", profile.uri);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModel::addToContacts(const QString& contactUri)
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
auto iter = pimpl_->contacts.find(contactUri);
|
|
if (iter != pimpl_->contacts.end())
|
|
return;
|
|
|
|
auto contactInfo = storage::buildContactFromProfile(owner.id,
|
|
contactUri,
|
|
profile::Type::PENDING);
|
|
pimpl_->contacts.insert(iter, contactUri, contactInfo);
|
|
ConfigurationManager::instance().lookupAddress(owner.id, "", contactUri);
|
|
}
|
|
|
|
void
|
|
ContactModel::removeContact(const QString& contactUri, bool banned)
|
|
{
|
|
try {
|
|
const auto& contact = getContact(contactUri);
|
|
if (contact.isBanned) {
|
|
LC_WARN << "Contact already banned";
|
|
return;
|
|
}
|
|
} catch (...) {
|
|
}
|
|
|
|
bool emitContactRemoved = false;
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
if (owner.profileInfo.type == profile::Type::SIP) {
|
|
// Remove contact from db
|
|
pimpl_->contacts.remove(contactUri);
|
|
storage::removeContactConversations(pimpl_->db, contactUri);
|
|
storage::removeProfile(owner.id, contactUri);
|
|
emitContactRemoved = true;
|
|
}
|
|
}
|
|
// hang up calls with the removed contact as peer
|
|
try {
|
|
auto callinfo = owner.callModel->getCallFromURI(contactUri, true);
|
|
owner.callModel->hangUp(callinfo.id);
|
|
} catch (std::out_of_range& e) {
|
|
}
|
|
if (emitContactRemoved) {
|
|
Q_EMIT contactRemoved(contactUri);
|
|
} else {
|
|
// NOTE: this method is asynchronous, the model will be updated
|
|
// in slotContactRemoved
|
|
daemon::removeContact(owner, contactUri, banned);
|
|
}
|
|
}
|
|
|
|
const contact::Info
|
|
ContactModel::getContact(const QString& contactUri) const
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
if (pimpl_->contacts.contains(contactUri)) {
|
|
return pimpl_->contacts.value(contactUri);
|
|
} else if (pimpl_->searchResult.contains(contactUri)) {
|
|
return pimpl_->searchResult.value(contactUri);
|
|
}
|
|
throw std::out_of_range("Contact out of range");
|
|
}
|
|
|
|
void
|
|
ContactModel::updateContact(const QString& uri, const MapStringString& infos)
|
|
{
|
|
std::unique_lock<std::mutex> lk(pimpl_->contactsMtx_);
|
|
auto ci = pimpl_->contacts.find(uri);
|
|
if (ci != pimpl_->contacts.end()) {
|
|
if (infos.contains("avatar")) {
|
|
ci->profileInfo.avatar = storage::vcard::compressedAvatar(infos["avatar"]);
|
|
} else {
|
|
// Else it will be resetted
|
|
ci->profileInfo.avatar = storage::avatar(owner.id, uri);
|
|
}
|
|
if (infos.contains("title"))
|
|
ci->profileInfo.alias = infos["title"];
|
|
storage::createOrUpdateProfile(owner.id, ci->profileInfo, true, true);
|
|
lk.unlock();
|
|
Q_EMIT profileUpdated(uri);
|
|
Q_EMIT modelUpdated(uri);
|
|
}
|
|
}
|
|
|
|
const QList<QString>&
|
|
ContactModel::getBannedContacts() const
|
|
{
|
|
return pimpl_->bannedContacts;
|
|
}
|
|
|
|
ContactModel::ContactInfoMap
|
|
ContactModel::getSearchResults() const
|
|
{
|
|
return pimpl_->searchResult;
|
|
}
|
|
|
|
void
|
|
ContactModel::searchContact(const QString& query)
|
|
{
|
|
LC_DBG << "query! " << query;
|
|
// always reset temporary contact
|
|
pimpl_->searchResult.clear();
|
|
|
|
auto uri = URI(query);
|
|
QString uriId = uri.format(URI::Section::USER_INFO | URI::Section::HOSTNAME
|
|
| URI::Section::PORT);
|
|
pimpl_->searchQuery = uriId;
|
|
|
|
auto uriScheme = uri.schemeType();
|
|
if (static_cast<int>(uriScheme) > 2 && owner.profileInfo.type == profile::Type::SIP) {
|
|
// sip account do not care if schemeType is NONE, or UNRECOGNIZED (enum value > 2)
|
|
uriScheme = URI::SchemeType::SIP;
|
|
} else if (uriScheme == URI::SchemeType::NONE && owner.profileInfo.type == profile::Type::JAMI) {
|
|
uriScheme = URI::SchemeType::RING;
|
|
}
|
|
|
|
if ((uriScheme == URI::SchemeType::SIP || uriScheme == URI::SchemeType::SIPS)
|
|
&& owner.profileInfo.type == profile::Type::SIP) {
|
|
pimpl_->searchSipContact(uri);
|
|
} else if (uriScheme == URI::SchemeType::RING && owner.profileInfo.type == profile::Type::JAMI) {
|
|
pimpl_->searchContact(uri);
|
|
} else {
|
|
pimpl_->updateTemporaryMessage(tr("Bad URI scheme"));
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::updateTemporaryMessage(const QString& mes)
|
|
{
|
|
if (searchStatus_ != mes) {
|
|
searchStatus_ = mes;
|
|
linked.owner.conversationModel->updateSearchStatus(mes);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::searchContact(const URI& query)
|
|
{
|
|
QString uriId = query.format(URI::Section::USER_INFO | URI::Section::HOSTNAME
|
|
| URI::Section::PORT);
|
|
if (query.isEmpty()) {
|
|
// This will remove the temporary item
|
|
Q_EMIT linked.modelUpdated(uriId);
|
|
updateTemporaryMessage("");
|
|
return;
|
|
}
|
|
|
|
if (query.protocolHint() == URI::ProtocolHint::RING) {
|
|
updateTemporaryMessage("");
|
|
// no lookup, this is a ring infoHash
|
|
for (auto& i : contacts)
|
|
if (i.profileInfo.uri == uriId)
|
|
return;
|
|
auto& temporaryContact = searchResult[uriId];
|
|
temporaryContact.profileInfo.uri = uriId;
|
|
temporaryContact.profileInfo.alias = uriId;
|
|
temporaryContact.profileInfo.type = profile::Type::TEMPORARY;
|
|
Q_EMIT linked.modelUpdated(uriId);
|
|
} else {
|
|
updateTemporaryMessage(tr("Searching…"));
|
|
|
|
// If the username contains an @ it's an exact match
|
|
bool isJamsAccount = !linked.owner.confProperties.managerUri.isEmpty();
|
|
if (isJamsAccount and not query.hasHostname())
|
|
ConfigurationManager::instance().searchUser(linked.owner.id, uriId);
|
|
else
|
|
ConfigurationManager::instance().lookupName(linked.owner.id, "", uriId);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::searchSipContact(const URI& query)
|
|
{
|
|
QString uriId = query.format(URI::Section::USER_INFO | URI::Section::HOSTNAME
|
|
| URI::Section::PORT);
|
|
if (query.isEmpty()) {
|
|
// This will remove the temporary item
|
|
Q_EMIT linked.modelUpdated(uriId);
|
|
updateTemporaryMessage("");
|
|
return;
|
|
}
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
if (contacts.find(uriId) == contacts.end()) {
|
|
auto& temporaryContact = searchResult[query];
|
|
|
|
temporaryContact.profileInfo.uri = uriId;
|
|
temporaryContact.profileInfo.alias = uriId;
|
|
temporaryContact.profileInfo.type = profile::Type::TEMPORARY;
|
|
}
|
|
}
|
|
Q_EMIT linked.modelUpdated(uriId);
|
|
}
|
|
|
|
uint64_t
|
|
ContactModel::sendDhtMessage(const QString& contactUri,
|
|
const QString& body,
|
|
const QString& mimeType,
|
|
int flag) const
|
|
{
|
|
// Send interaction
|
|
QMap<QString, QString> payloads;
|
|
if (mimeType.isEmpty())
|
|
payloads[TEXT_PLAIN] = body;
|
|
else
|
|
payloads[mimeType] = body;
|
|
auto msgId = ConfigurationManager::instance().sendTextMessage(QString(owner.id),
|
|
QString(contactUri),
|
|
payloads,
|
|
flag);
|
|
// NOTE: ConversationModel should store the interaction into the database
|
|
return msgId;
|
|
}
|
|
|
|
const QString
|
|
ContactModel::bestNameForContact(const QString& contactUri) const
|
|
{
|
|
if (contactUri.isEmpty())
|
|
return contactUri;
|
|
if (contactUri == owner.profileInfo.uri)
|
|
return owner.accountModel->bestNameForAccount(owner.id);
|
|
QString res = contactUri;
|
|
try {
|
|
auto contact = getContact(contactUri);
|
|
auto alias = contact.profileInfo.alias.simplified();
|
|
if (alias.isEmpty()) {
|
|
return bestIdFromContactInfo(contact);
|
|
}
|
|
return alias;
|
|
} catch (const std::out_of_range&) {
|
|
auto itContact = pimpl_->nonContactLookup_.find(contactUri);
|
|
if (itContact != pimpl_->nonContactLookup_.end()) {
|
|
return *itContact;
|
|
} else {
|
|
// This is not a contact, but we should get the registered name
|
|
ConfigurationManager::instance().lookupAddress(owner.id, "", contactUri);
|
|
}
|
|
}
|
|
return res;
|
|
}
|
|
|
|
QString
|
|
ContactModel::avatar(const QString& uri) const
|
|
{
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
// For search results it's loaded and not in storage yet.
|
|
if (pimpl_->searchResult.contains(uri)) {
|
|
auto contact = pimpl_->searchResult.value(uri);
|
|
return contact.profileInfo.avatar;
|
|
}
|
|
}
|
|
// Else search in storage (because not cached!)
|
|
return storage::avatar(owner.id, uri);
|
|
}
|
|
|
|
const QString
|
|
ContactModel::bestIdForContact(const QString& contactUri) const
|
|
{
|
|
std::lock_guard<std::mutex> lk(pimpl_->contactsMtx_);
|
|
if (pimpl_->contacts.contains(contactUri)) {
|
|
auto contact = pimpl_->contacts.value(contactUri);
|
|
return bestIdFromContactInfo(contact);
|
|
}
|
|
return contactUri;
|
|
}
|
|
|
|
const QString
|
|
ContactModel::bestIdFromContactInfo(const contact::Info& contactInfo) const
|
|
{
|
|
auto registeredName = contactInfo.registeredName.simplified();
|
|
auto infoHash = contactInfo.profileInfo.uri.simplified();
|
|
|
|
if (!registeredName.isEmpty()) {
|
|
return registeredName;
|
|
}
|
|
return infoHash;
|
|
}
|
|
|
|
ContactModelPimpl::ContactModelPimpl(const ContactModel& linked,
|
|
Database& db,
|
|
const CallbacksHandler& callbacksHandler,
|
|
const BehaviorController& behaviorController)
|
|
: linked(linked)
|
|
, db(db)
|
|
, behaviorController(behaviorController)
|
|
, callbacksHandler(callbacksHandler)
|
|
{
|
|
// Init contacts map
|
|
if (linked.owner.profileInfo.type == profile::Type::SIP)
|
|
fillWithSIPContacts();
|
|
else
|
|
fillWithJamiContacts();
|
|
|
|
// connect the signals
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::newBuddySubscription,
|
|
this,
|
|
&ContactModelPimpl::slotNewBuddySubscription);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::contactAdded,
|
|
this,
|
|
&ContactModelPimpl::slotContactAdded);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::contactRemoved,
|
|
this,
|
|
&ContactModelPimpl::slotContactRemoved);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::registeredNameFound,
|
|
this,
|
|
&ContactModelPimpl::slotRegisteredNameFound);
|
|
connect(&*linked.owner.callModel, &CallModel::newCall, this, &ContactModelPimpl::slotNewCall);
|
|
connect(&callbacksHandler,
|
|
&lrc::CallbacksHandler::newAccountMessage,
|
|
this,
|
|
&ContactModelPimpl::slotNewAccountMessage);
|
|
connect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusCreated,
|
|
this,
|
|
&ContactModelPimpl::slotNewAccountTransfer);
|
|
connect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::profileReceived,
|
|
this,
|
|
&ContactModelPimpl::slotProfileReceived);
|
|
connect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::userSearchEnded,
|
|
this,
|
|
&ContactModelPimpl::slotUserSearchEnded);
|
|
}
|
|
|
|
ContactModelPimpl::~ContactModelPimpl()
|
|
{
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::newBuddySubscription,
|
|
this,
|
|
&ContactModelPimpl::slotNewBuddySubscription);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::contactAdded,
|
|
this,
|
|
&ContactModelPimpl::slotContactAdded);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::contactRemoved,
|
|
this,
|
|
&ContactModelPimpl::slotContactRemoved);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::registeredNameFound,
|
|
this,
|
|
&ContactModelPimpl::slotRegisteredNameFound);
|
|
disconnect(&*linked.owner.callModel, &CallModel::newCall, this, &ContactModelPimpl::slotNewCall);
|
|
disconnect(&callbacksHandler,
|
|
&lrc::CallbacksHandler::newAccountMessage,
|
|
this,
|
|
&ContactModelPimpl::slotNewAccountMessage);
|
|
disconnect(&callbacksHandler,
|
|
&CallbacksHandler::transferStatusCreated,
|
|
this,
|
|
&ContactModelPimpl::slotNewAccountTransfer);
|
|
disconnect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::profileReceived,
|
|
this,
|
|
&ContactModelPimpl::slotProfileReceived);
|
|
disconnect(&ConfigurationManager::instance(),
|
|
&ConfigurationManagerInterface::userSearchEnded,
|
|
this,
|
|
&ContactModelPimpl::slotUserSearchEnded);
|
|
profileThreadPool.waitForDone();
|
|
}
|
|
|
|
bool
|
|
ContactModelPimpl::fillWithSIPContacts()
|
|
{
|
|
auto conversationsForAccount = storage::getAllConversations(db);
|
|
for (const auto& convId : conversationsForAccount) {
|
|
auto otherParticipants = storage::getPeerParticipantsForConversation(db, convId);
|
|
for (const auto& participant : otherParticipants) {
|
|
// for each conversations get the other profile id
|
|
auto contactInfo = storage::buildContactFromProfile(linked.owner.id,
|
|
participant,
|
|
profile::Type::SIP);
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
contacts.insert(contactInfo.profileInfo.uri, contactInfo);
|
|
}
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool
|
|
ContactModelPimpl::fillWithJamiContacts()
|
|
{
|
|
// Add contacts from daemon
|
|
const VectorMapStringString& contacts_vector = ConfigurationManager::instance().getContacts(
|
|
linked.owner.id);
|
|
for (auto contact_info : contacts_vector) {
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
bool banned = contact_info["banned"] == "true" ? true : false;
|
|
addToContacts(contact_info["id"],
|
|
linked.owner.profileInfo.type,
|
|
"",
|
|
banned,
|
|
contact_info["conversationId"]);
|
|
}
|
|
|
|
// Add pending contacts
|
|
const VectorMapStringString& pending_tr {
|
|
ConfigurationManager::instance().getTrustRequests(linked.owner.id)};
|
|
for (const auto& tr_info : pending_tr) {
|
|
// Get pending requests.
|
|
auto payload = tr_info[libjami::Account::TrustRequest::PAYLOAD].toUtf8();
|
|
auto contactUri = tr_info[libjami::Account::TrustRequest::FROM];
|
|
auto convId = tr_info[libjami::Account::TrustRequest::CONVERSATIONID];
|
|
if (!convId.isEmpty())
|
|
continue; // This will be added via getConversationsRequests
|
|
|
|
auto contactInfo = storage::buildContactFromProfile(linked.owner.id,
|
|
contactUri,
|
|
profile::Type::PENDING);
|
|
|
|
const auto vCard = lrc::vCard::utils::toHashMap(payload);
|
|
const auto alias = vCard["FN"];
|
|
contactInfo.profileInfo.type = profile::Type::PENDING;
|
|
if (!alias.isEmpty())
|
|
contactInfo.profileInfo.alias = alias.constData();
|
|
contactInfo.registeredName = "";
|
|
contactInfo.isBanned = false;
|
|
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
contacts.insert(contactUri, contactInfo);
|
|
}
|
|
|
|
// create profile vcard for contact
|
|
storage::createOrUpdateProfile(linked.owner.id, contactInfo.profileInfo, true);
|
|
}
|
|
|
|
// Update presence
|
|
// TODO fix this map. This is dumb for now. The map contains values as keys, and empty values.
|
|
const VectorMapStringString& subscriptions {
|
|
PresenceManager::instance().getSubscriptions(linked.owner.id)};
|
|
for (const auto& subscription : subscriptions) {
|
|
auto first = true;
|
|
QString uri = "";
|
|
for (const auto& key : subscription) {
|
|
if (first) {
|
|
first = false;
|
|
uri = key;
|
|
} else {
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
auto it = contacts.find(uri);
|
|
if (it != contacts.end()) {
|
|
it->isPresent = key == "Online";
|
|
linked.modelUpdated(uri);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotNewBuddySubscription(const QString& accountId,
|
|
const QString& contactUri,
|
|
bool status)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
auto it = contacts.find(contactUri);
|
|
if (it != contacts.end()) {
|
|
it->isPresent = status;
|
|
} else
|
|
return;
|
|
}
|
|
Q_EMIT linked.modelUpdated(contactUri);
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotContactAdded(const QString& accountId, const QString& contactUri, bool)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
auto contact = contacts.find(contactUri);
|
|
if (contact != contacts.end()) {
|
|
if (contact->isBanned) {
|
|
// Continue
|
|
} else if (contact->profileInfo.type == profile::Type::PENDING) {
|
|
Q_EMIT behaviorController.trustRequestTreated(linked.owner.id, contactUri);
|
|
// Continue
|
|
} else {
|
|
return;
|
|
}
|
|
}
|
|
|
|
// for jams account we already have profile with avatar, use it to save to vCard
|
|
bool isJamsAccount = !linked.owner.confProperties.managerUri.isEmpty();
|
|
if (isJamsAccount) {
|
|
auto result = searchResult.find(contactUri);
|
|
if (result != searchResult.end()) {
|
|
storage::createOrUpdateProfile(linked.owner.id, result->profileInfo, true);
|
|
}
|
|
}
|
|
|
|
bool isBanned = false;
|
|
|
|
{
|
|
// Always get contactsMtx_ lock before bannedContactsMtx_.
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
|
|
{
|
|
// Check whether contact is banned or not
|
|
std::lock_guard<std::mutex> lk(bannedContactsMtx_);
|
|
auto it = std::find(bannedContacts.begin(), bannedContacts.end(), contactUri);
|
|
|
|
isBanned = (it != bannedContacts.end());
|
|
|
|
// If contact is banned, do not re-add it, simply update its flag and the banned contacts list
|
|
if (isBanned) {
|
|
bannedContacts.erase(it);
|
|
}
|
|
|
|
MapStringString details = ConfigurationManager::instance()
|
|
.getContactDetails(linked.owner.id, contactUri);
|
|
addToContacts(contactUri,
|
|
linked.owner.profileInfo.type,
|
|
"",
|
|
false,
|
|
details["conversationId"]);
|
|
}
|
|
}
|
|
if (isBanned) {
|
|
// Update the smartlist
|
|
linked.owner.conversationModel->refreshFilter();
|
|
Q_EMIT linked.bannedStatusChanged(contactUri, false);
|
|
} else {
|
|
Q_EMIT linked.contactAdded(contactUri);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotContactRemoved(const QString& accountId,
|
|
const QString& contactUri,
|
|
bool banned)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
{
|
|
// Always get contactsMtx_ lock before bannedContactsMtx_.
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
|
|
auto contact = contacts.find(contactUri);
|
|
if (contact == contacts.end()) {
|
|
return;
|
|
}
|
|
|
|
if (contact->profileInfo.type == profile::Type::PENDING) {
|
|
Q_EMIT behaviorController.trustRequestTreated(linked.owner.id, contactUri);
|
|
}
|
|
|
|
if (contact->profileInfo.type != profile::Type::SIP)
|
|
PresenceManager::instance().subscribeBuddy(linked.owner.id, contactUri, false);
|
|
|
|
if (banned) {
|
|
contact->isBanned = true;
|
|
// Update bannedContacts index
|
|
bannedContacts.append(contact->profileInfo.uri);
|
|
} else {
|
|
if (contact->isBanned) {
|
|
// Contact was banned, update bannedContacts
|
|
std::lock_guard<std::mutex> lk(bannedContactsMtx_);
|
|
auto it = std::find(bannedContacts.begin(),
|
|
bannedContacts.end(),
|
|
contact->profileInfo.uri);
|
|
if (it == bannedContacts.end()) {
|
|
// should not happen
|
|
LC_DBG << "Contact is banned but not present in bannedContacts. This is most "
|
|
"likely the result of an earlier bug.";
|
|
} else {
|
|
bannedContacts.erase(it);
|
|
}
|
|
}
|
|
storage::removeContactConversations(db, contactUri);
|
|
storage::removeProfile(linked.owner.id, contactUri);
|
|
contacts.remove(contactUri);
|
|
}
|
|
}
|
|
|
|
// Update the smartlist
|
|
linked.owner.conversationModel->refreshFilter();
|
|
if (banned) {
|
|
Q_EMIT linked.bannedStatusChanged(contactUri, true);
|
|
} else {
|
|
Q_EMIT linked.contactRemoved(contactUri);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::addToContacts(const QString& contactUri,
|
|
const profile::Type& type,
|
|
const QString& displayName,
|
|
bool banned,
|
|
const QString& conversationId)
|
|
{
|
|
// create a vcard if necessary
|
|
profile::Info profileInfo {contactUri, {}, displayName, linked.owner.profileInfo.type};
|
|
auto contactInfo = storage::buildContactFromProfile(linked.owner.id, contactUri, type);
|
|
auto updateProfile = false;
|
|
if (!profileInfo.alias.isEmpty() && contactInfo.profileInfo.alias != profileInfo.alias) {
|
|
updateProfile = true;
|
|
contactInfo.profileInfo.alias = profileInfo.alias;
|
|
}
|
|
auto oldAvatar = storage::avatar(linked.owner.id, contactUri);
|
|
if (!profileInfo.avatar.isEmpty() && oldAvatar != profileInfo.avatar) {
|
|
updateProfile = true;
|
|
contactInfo.profileInfo.avatar = profileInfo.avatar;
|
|
}
|
|
if (updateProfile)
|
|
storage::vcard::setProfile(linked.owner.id, contactInfo.profileInfo, true);
|
|
|
|
contactInfo.isBanned = banned;
|
|
contactInfo.conversationId = conversationId;
|
|
contactInfo.profileInfo.avatar.clear();
|
|
|
|
if (type == profile::Type::JAMI) {
|
|
ConfigurationManager::instance().lookupAddress(linked.owner.id, "", contactUri);
|
|
PresenceManager::instance().subscribeBuddy(linked.owner.id, contactUri, !banned);
|
|
} else {
|
|
contactInfo.profileInfo.alias = displayName;
|
|
}
|
|
|
|
contactInfo.profileInfo.type = type; // Because PENDING should not be stored in the database
|
|
auto iter = contacts.find(contactInfo.profileInfo.uri);
|
|
if (iter != contacts.end()) {
|
|
auto info = iter.value();
|
|
contactInfo.registeredName = info.registeredName;
|
|
contactInfo.isPresent = info.isPresent;
|
|
iter.value() = contactInfo;
|
|
} else
|
|
contacts.insert(iter, contactInfo.profileInfo.uri, contactInfo);
|
|
|
|
if (banned) {
|
|
bannedContacts.append(contactUri);
|
|
}
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotRegisteredNameFound(const QString& accountId,
|
|
int status,
|
|
const QString& uri,
|
|
const QString& registeredName)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
if (status == 0 /* SUCCESS */) {
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
if (contacts.find(uri) != contacts.end()) {
|
|
// update contact and remove temporary item
|
|
contacts[uri].registeredName = registeredName;
|
|
searchResult.clear();
|
|
} else {
|
|
nonContactLookup_[uri] = registeredName;
|
|
if ((searchQuery != uri && searchQuery != registeredName) || searchQuery.isEmpty()) {
|
|
// we are notified that a previous lookup ended
|
|
return;
|
|
}
|
|
auto& temporaryContact = searchResult[uri];
|
|
lrc::api::profile::Info profileInfo = {uri, "", "", profile::Type::TEMPORARY};
|
|
temporaryContact = {profileInfo, registeredName, false, false};
|
|
}
|
|
} else {
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
if (contacts.find(uri) != contacts.end()) {
|
|
// it was lookup for contact
|
|
return;
|
|
}
|
|
}
|
|
if ((searchQuery != uri && searchQuery != registeredName) || searchQuery.isEmpty()) {
|
|
// we are notified that a previous lookup ended
|
|
return;
|
|
}
|
|
switch (status) {
|
|
case 1 /* INVALID */:
|
|
updateTemporaryMessage(tr("Invalid ID"));
|
|
break;
|
|
case 2 /* NOT FOUND */:
|
|
updateTemporaryMessage(tr("Username not found"));
|
|
break;
|
|
case 3 /* ERROR */:
|
|
updateTemporaryMessage(tr("Couldn't lookup…"));
|
|
break;
|
|
}
|
|
return;
|
|
}
|
|
updateTemporaryMessage("");
|
|
Q_EMIT linked.modelUpdated(uri);
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotNewCall(const QString& fromId,
|
|
const QString& callId,
|
|
const QString& displayname,
|
|
bool isOutgoing,
|
|
const QString& toUri)
|
|
{
|
|
if (!isOutgoing && toUri == linked.owner.profileInfo.uri) {
|
|
bool emitContactAdded = false;
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
auto it = contacts.find(fromId);
|
|
if (it == contacts.end()) {
|
|
// Contact not found, load profile from database.
|
|
// The conversation model will create an entry and link the incomingCall.
|
|
auto type = (linked.owner.profileInfo.type == profile::Type::JAMI)
|
|
? profile::Type::PENDING
|
|
: profile::Type::SIP;
|
|
addToContacts(fromId, type, displayname, false);
|
|
emitContactAdded = true;
|
|
} else {
|
|
// Update the display name
|
|
if (!displayname.isEmpty()) {
|
|
it->profileInfo.alias = displayname;
|
|
storage::createOrUpdateProfile(linked.owner.id, it->profileInfo, true);
|
|
}
|
|
}
|
|
}
|
|
if (emitContactAdded) {
|
|
if (linked.owner.profileInfo.type == profile::Type::SIP)
|
|
Q_EMIT linked.contactAdded(fromId);
|
|
else if (linked.owner.profileInfo.type == profile::Type::JAMI)
|
|
Q_EMIT behaviorController.newTrustRequest(linked.owner.id, "", fromId);
|
|
} else
|
|
Q_EMIT linked.profileUpdated(fromId);
|
|
}
|
|
Q_EMIT linked.newCall(fromId, callId, isOutgoing, toUri);
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotNewAccountMessage(const QString& accountId,
|
|
const QString& peerId,
|
|
const QString& msgId,
|
|
const MapStringString& payloads)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
QString peerId2(peerId);
|
|
|
|
auto emitNewTrust = false;
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
if (contacts.find(peerId) == contacts.end()) {
|
|
// Contact not found, load profile from database.
|
|
// The conversation model will create an entry and link the incomingCall.
|
|
|
|
if (linked.owner.profileInfo.type == profile::Type::SIP) {
|
|
QString potentialContact = sipUriReceivedFilter(peerId);
|
|
if (potentialContact.isEmpty()) {
|
|
addToContacts(peerId, profile::Type::SIP, "", false);
|
|
} else {
|
|
// equivalent uri exist, use that uri
|
|
peerId2 = potentialContact;
|
|
}
|
|
} else {
|
|
addToContacts(peerId, profile::Type::PENDING, "", false);
|
|
emitNewTrust = true;
|
|
}
|
|
}
|
|
}
|
|
if (emitNewTrust) {
|
|
Q_EMIT behaviorController.newTrustRequest(linked.owner.id, "", peerId);
|
|
}
|
|
Q_EMIT linked.newAccountMessage(accountId, peerId2, msgId, payloads);
|
|
}
|
|
|
|
QString
|
|
ContactModelPimpl::sipUriReceivedFilter(const QString& uri)
|
|
{
|
|
// this function serves when the uri is not found in the contact list
|
|
// return "" means need to add new contact, else means equivalent uri exist
|
|
std::string uriCopy = uri.toStdString();
|
|
|
|
auto pos = uriCopy.find("@");
|
|
auto ownerHostName = linked.owner.confProperties.hostname.toStdString();
|
|
|
|
if (pos != std::string::npos) {
|
|
// "@" is found, separate username and hostname
|
|
std::string hostName = uriCopy.substr(pos + 1);
|
|
uriCopy.erase(uriCopy.begin() + pos, uriCopy.end());
|
|
std::string remoteUser = std::move(uriCopy);
|
|
|
|
if (hostName.compare(ownerHostName) == 0) {
|
|
auto remoteUserQStr = QString::fromStdString(remoteUser);
|
|
if (contacts.find(remoteUserQStr) != contacts.end()) {
|
|
return remoteUserQStr;
|
|
}
|
|
if (remoteUser.at(0) == '+') {
|
|
// "+" - country dial-in codes
|
|
// maximum 3 digits
|
|
for (int i = 2; i <= 4; i++) {
|
|
QString tempUserName = QString::fromStdString(remoteUser.substr(i));
|
|
if (contacts.find(tempUserName) != contacts.end()) {
|
|
return tempUserName;
|
|
}
|
|
}
|
|
return "";
|
|
} else {
|
|
// if not "+" from incoming
|
|
// sub "+" char from contacts to see if user exit
|
|
for (auto& contactUri : contacts.keys()) {
|
|
if (!contactUri.isEmpty()) {
|
|
for (int j = 2; j <= 4; j++) {
|
|
if (QString(contactUri).remove(0, j) == remoteUserQStr) {
|
|
return contactUri;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "";
|
|
}
|
|
}
|
|
// different hostname means not a phone number
|
|
// no need to check country dial-in codes
|
|
return "";
|
|
}
|
|
// "@" is not found -> not possible since all response uri has one
|
|
return "";
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotNewAccountTransfer(const QString& fileId, datatransfer::Info info)
|
|
{
|
|
if (info.accountId != linked.owner.id)
|
|
return;
|
|
|
|
bool emitNewTrust = false;
|
|
{
|
|
std::lock_guard<std::mutex> lk(contactsMtx_);
|
|
// Note: just add a contact for compatibility (so not for swarm).
|
|
if (info.conversationId.isEmpty() && !info.peerUri.isEmpty()
|
|
&& contacts.find(info.peerUri) == contacts.end()) {
|
|
// Contact not found, load profile from database.
|
|
// The conversation model will create an entry and link the incomingCall.
|
|
auto type = (linked.owner.profileInfo.type == profile::Type::JAMI)
|
|
? profile::Type::PENDING
|
|
: profile::Type::SIP;
|
|
addToContacts(info.peerUri, type, "", false);
|
|
emitNewTrust = (linked.owner.profileInfo.type == profile::Type::JAMI);
|
|
}
|
|
}
|
|
if (emitNewTrust) {
|
|
Q_EMIT behaviorController.newTrustRequest(linked.owner.id, "", info.peerUri);
|
|
}
|
|
|
|
Q_EMIT linked.newAccountTransfer(fileId, info);
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotProfileReceived(const QString& accountId,
|
|
const QString& peer,
|
|
const QString& path)
|
|
{
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
|
|
profileThreadPool.start([=] {
|
|
QFile vCardFile(path);
|
|
if (!vCardFile.open(QIODevice::ReadOnly | QIODevice::Text))
|
|
return;
|
|
QTextStream in(&vCardFile);
|
|
|
|
auto vCard = in.readAll();
|
|
|
|
profile::Info profileInfo;
|
|
profileInfo.uri = peer;
|
|
profileInfo.type = profile::Type::JAMI;
|
|
|
|
for (auto& e : QString(vCard).split("\n")) {
|
|
if (e.contains("PHOTO")) {
|
|
auto splitted = e.split(":");
|
|
if (splitted.size() > 1)
|
|
profileInfo.avatar = e.split(":")[1];
|
|
} else if (e.contains("FN")) {
|
|
auto splitted = e.split(":");
|
|
if (splitted.size() > 1)
|
|
profileInfo.alias = e.split(":")[1];
|
|
}
|
|
}
|
|
|
|
if (peer == linked.owner.profileInfo.uri) {
|
|
if (profileInfo.avatar.isEmpty())
|
|
return; // In this case, probably a new device without avatar.
|
|
// Profile is saved by daemon, just update client
|
|
linked.owner.accountModel->setAlias(linked.owner.id, profileInfo.alias, false);
|
|
linked.owner.accountModel->setAvatar(linked.owner.id, profileInfo.avatar, false);
|
|
return;
|
|
}
|
|
vCardFile.remove();
|
|
|
|
contact::Info contactInfo;
|
|
contactInfo.profileInfo = profileInfo;
|
|
|
|
linked.owner.contactModel->addContact(contactInfo);
|
|
contactInfo.profileInfo.avatar.clear(); // Do not store after update
|
|
});
|
|
}
|
|
|
|
void
|
|
ContactModelPimpl::slotUserSearchEnded(const QString& accountId,
|
|
int status,
|
|
const QString& query,
|
|
const VectorMapStringString& result)
|
|
{
|
|
if (searchQuery != query)
|
|
return;
|
|
if (accountId != linked.owner.id)
|
|
return;
|
|
searchResult.clear();
|
|
switch (status) {
|
|
case 0: /* SUCCESS */
|
|
for (auto& resultInfo : result) {
|
|
if (contacts.find(resultInfo.value("id")) != contacts.end()) {
|
|
continue;
|
|
}
|
|
profile::Info profileInfo;
|
|
profileInfo.uri = resultInfo.value("id");
|
|
profileInfo.type = profile::Type::TEMPORARY;
|
|
profileInfo.avatar = resultInfo.value("profilePicture");
|
|
profileInfo.alias = resultInfo.value("firstName") + " " + resultInfo.value("lastName");
|
|
contact::Info contactInfo;
|
|
contactInfo.profileInfo = profileInfo;
|
|
contactInfo.registeredName = resultInfo.value("username");
|
|
searchResult.insert(profileInfo.uri, contactInfo);
|
|
}
|
|
updateTemporaryMessage("");
|
|
break;
|
|
case 3: /* ERROR */
|
|
updateTemporaryMessage("could not find contact matching search");
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
Q_EMIT linked.modelUpdated(query);
|
|
}
|
|
|
|
} // namespace lrc
|
|
|
|
#include "api/moc_contactmodel.cpp"
|
|
#include "contactmodel.moc"
|