/**************************************************************************** * Copyright (C) 2017-2023 Savoir-faire Linux Inc. * * Author: Nicolas Jäger * * Author: Sébastien Blin * * Author: Guillaume Roguez * * Author: Hugo Lefeuvre * * Author: Kateryna Kostiuk * * Author: Andreas Traczyk * * * * 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 . * ***************************************************************************/ #include "api/contactmodel.h" // LRC #include "api/account.h" #include "api/contact.h" #include "api/conversationmodel.h" #include "api/interaction.h" #include "api/lrc.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 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 bannedContacts; QString searchQuery; std::mutex contactsMtx_; std::mutex bannedContactsMtx_; QString searchStatus_ {}; QMap nonContactLookup_; 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 incoming call arrives. * @param fromId * @param callId * @param displayName */ void slotIncomingCall(const QString& fromId, const QString& callId, const QString& displayname); /** * 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(*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()) { qDebug() << 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)) { qDebug() << "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() || (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 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: qDebug() << "ContactModel::addContact, cannot add contact with invalid type."; return; } storage::createOrUpdateProfile(owner.id, profile, true); { std::lock_guard 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 ConfigurationManager::instance().lookupAddress(owner.id, "", profile.uri); } void ContactModel::addToContacts(const QString& contactUri) { std::lock_guard 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) { qWarning() << "Contact already banned"; return; } } catch (...) { } bool emitContactRemoved = false; { std::lock_guard 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 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 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 if (!lrc::api::Lrc::cacheAvatars.load()) { // Else it will be reseted 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& ContactModel::getBannedContacts() const { return pimpl_->bannedContacts; } ContactModel::ContactInfoMap ContactModel::getSearchResults() const { return pimpl_->searchResult; } void ContactModel::searchContact(const QString& 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(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 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 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 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 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::newIncomingCall, this, &ContactModelPimpl::slotIncomingCall); 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::newIncomingCall, this, &ContactModelPimpl::slotIncomingCall); 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); } 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 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 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"]; QByteArray photo; for (const auto& key : vCard.keys()) { if (key.contains("PHOTO") && lrc::api::Lrc::cacheAvatars.load()) photo = vCard[key]; } contactInfo.profileInfo.type = profile::Type::PENDING; if (!alias.isEmpty()) contactInfo.profileInfo.alias = alias.constData(); if (!photo.isEmpty()) contactInfo.profileInfo.avatar = photo.constData(); contactInfo.registeredName = ""; contactInfo.isBanned = false; { std::lock_guard 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 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 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() && contact->profileInfo.type == profile::Type::PENDING) Q_EMIT behaviorController.trustRequestTreated(linked.owner.id, contactUri); // 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 lk(contactsMtx_); { // Check whether contact is banned or not std::lock_guard 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 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 lk(bannedContactsMtx_); auto it = std::find(bannedContacts.begin(), bannedContacts.end(), contact->profileInfo.uri); if (it == bannedContacts.end()) { // should not happen qDebug("ContactModel::slotContactsRemoved(): 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 = lrc::api::Lrc::cacheAvatars.load() ? contactInfo.profileInfo.avatar : 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; if (!lrc::api::Lrc::cacheAvatars.load()) 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 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 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::slotIncomingCall(const QString& fromId, const QString& callId, const QString& displayname) { bool emitContactAdded = false; { std::lock_guard 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.incomingCall(fromId, callId); } 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 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 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; 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")) profileInfo.avatar = e.split(":")[1]; else if (e.contains("FN")) profileInfo.alias = e.split(":")[1]; if (peer == linked.owner.profileInfo.uri) { if (!profileInfo.avatar.isEmpty()) { auto dest = storage::getPath() + accountId + "/profile.vcf"; QFile oldvCard(dest); if (oldvCard.exists()) oldvCard.remove(); vCardFile.rename(dest); linked.owner.accountModel->setAlias(linked.owner.id, profileInfo.alias); linked.owner.accountModel->setAvatar(linked.owner.id, profileInfo.avatar); Q_EMIT linked.profileUpdated(peer); } return; } vCardFile.remove(); contact::Info contactInfo; contactInfo.profileInfo = profileInfo; linked.owner.contactModel->addContact(contactInfo); if (!lrc::api::Lrc::cacheAvatars.load()) 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"