// Copyright (c) 2017-2024, Mudita Sp. z.o.o. All rights reserved. // For licensing, see https://github.com/mudita/MuditaOS/LICENSE.md #include "ContactRecord.hpp" #include "queries/phonebook/QueryContactAdd.hpp" #include "queries/phonebook/QueryContactGetByID.hpp" #include "queries/phonebook/QueryContactGetByNumberID.hpp" #include "queries/phonebook/QueryContactUpdate.hpp" #include "queries/phonebook/QueryContactRemove.hpp" #include "queries/phonebook/QueryMergeContactsList.hpp" #include "queries/phonebook/QueryCheckContactsListDuplicates.hpp" #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr auto NumberMatcherPageSize = 100; } // namespace ContactRecordInterface::ContactRecordInterface(ContactsDB *db) : contactDB(db), favouritesGroupId(db->groups.favouritesId()) {} auto ContactRecordInterface::Add(ContactRecord &rec) -> bool { if (rec.numbers.size() > 2) { LOG_WARN("Contact has more than 2 numbers"); } if (hasContactRecordSameNumbers(rec)) { LOG_ERROR("New record can not have 2 same numbers"); return false; } bool result = contactDB->contacts.add(ContactsTableRow{Record(DB_ID_NONE), .speedDial = rec.speeddial}); if (!result) { return false; } uint32_t contactID = contactDB->getLastInsertRowId(); LOG_DEBUG("New contact with ID %" PRIu32 " created", contactID); rec.ID = contactID; const auto numbersIDs = addNumbers(contactID, rec.numbers); if (!numbersIDs.has_value()) { return false; } auto oldNumberIDs = splitNumberIDs(numbersIDs.value()); auto newNumbers = rec.numbers; if (!changeNumberRecordInPlaceIfCountryCodeIsOnlyDifferent(oldNumberIDs, newNumbers)) { return false; } if (!rec.isTemporary()) { const auto nameID = addOrUpdateName(contactID, DB_ID_NONE, rec); if (!nameID.has_value()) { return false; } const auto addressID = addOrUpdateAddress(contactID, DB_ID_NONE, rec); if (!addressID.has_value()) { return false; } const auto ringtoneID = addOrUpdateRingtone(contactID, DB_ID_NONE, rec); if (!ringtoneID.has_value()) { return false; } result = contactDB->contacts.update(ContactsTableRow{Record(contactID), .nameID = nameID.value(), .numbersID = numbersIDs.value(), .ringID = ringtoneID.value(), .addressID = addressID.value(), .speedDial = rec.speeddial}); } else { result = contactDB->contacts.update(ContactsTableRow{Record(contactID), .nameID = DB_ID_NONE, .numbersID = numbersIDs.value(), .ringID = DB_ID_NONE, .addressID = DB_ID_NONE, .speedDial = rec.speeddial}); } for (const auto &group : rec.groups) { contactDB->groups.addContactToGroup(contactID, group.ID); } return result; } auto ContactRecordInterface::BlockByID(uint32_t id, const bool shouldBeBlocked) -> bool { return contactDB->contacts.BlockByID(id, shouldBeBlocked); } auto ContactRecordInterface::RemoveByID(uint32_t id) -> bool { auto contact = contactDB->contacts.getByIdWithTemporary(id); if (contact.isValid()) { auto currentGroups = contactDB->groups.getGroupsForContact(id); // Proceed if a contact is NOT a temporary contact. if (currentGroups.find(ContactsDB::temporaryGroupId()) == currentGroups.end()) { // Clear all the contact's data besides the phone numbers. contactDB->name.removeById(contact.nameID); contactDB->address.removeById(contact.addressID); contactDB->ringtones.removeById(contact.ringID); for (const auto &group : currentGroups) { contactDB->groups.removeContactFromGroup(id, group.ID); } contactDB->groups.addContactToGroup(id, ContactsDB::temporaryGroupId()); contact.namePrimary.clear(); contact.nameAlternative.clear(); contact.speedDial.clear(); contact.nameID = DB_ID_NONE; contact.addressID = DB_ID_NONE; contact.ringID = DB_ID_NONE; contactDB->contacts.update(contact); } // It's already a temporary contact, so it's already deleted. } return true; } auto ContactRecordInterface::Update(const ContactRecord &rec) -> bool { if (rec.numbers.size() > 2) { LOG_WARN("Contact has more than 2 numbers"); } ContactsTableRow contact = contactDB->contacts.getByIdWithTemporary(rec.ID); if (!contact.isValid()) { return false; } auto oldNumberIDs = splitNumberIDs(contact.numbersID); auto newNumbers = rec.numbers; if (hasContactRecordSameNumbers(rec)) { LOG_ERROR("Updated record can not have 2 same numbers"); return false; } if (!changeNumberRecordInPlaceIfCountryCodeIsOnlyDifferent(oldNumberIDs, newNumbers)) { return false; } auto newNumbersIDs = getNumbersIDs(contact.ID, rec, utils::PhoneNumber::Match::EXACT); for (auto oldNumberID : oldNumberIDs) { if (std::find(std::begin(newNumbersIDs), std::end(newNumbersIDs), oldNumberID) == std::end(newNumbersIDs)) { // If any oldNumberID is NOT one of the newNumbersID, which will be assigned to this contact, then // make temporary contact with this oldNumberID auto numberRecord = contactDB->number.getById(oldNumberID); if (!numberRecord.isValid()) { return false; } ContactRecord::Number number; try { number = ContactRecord::Number(numberRecord.numberUser, numberRecord.numbere164); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR("Exception in ContactRecord::Number while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return false; } // make temporary contact with old number const auto tmpContactRecord = addTemporaryContactForNumber(number); if (!tmpContactRecord.has_value()) { return false; } numberRecord.contactID = tmpContactRecord.value().ID; if (!contactDB->number.update(numberRecord)) { return false; } } } unbindSpeedDialKeyFromOtherContacts(rec.speeddial); const auto nameID = addOrUpdateName(contact.ID, contact.nameID, rec); if (!nameID.has_value()) { return false; } const auto addressID = addOrUpdateAddress(contact.ID, contact.addressID, rec); if (!addressID.has_value()) { return false; } const auto ringtoneID = addOrUpdateRingtone(contact.ID, contact.ringID, rec); if (!ringtoneID.has_value()) { return false; } ContactsTableRow row{Record(contact.ID), .nameID = nameID.value(), .numbersID = joinNumberIDs(newNumbersIDs), .ringID = ringtoneID.value(), .addressID = addressID.value(), .speedDial = rec.speeddial, .namePrimary = rec.primaryName, .nameAlternative = rec.alternativeName}; bool result = false; bool recordExists = [&]() { auto record = contactDB->contacts.getById(contact.ID); return record.isValid(); }(); if (recordExists) { result = contactDB->contacts.update(row); } else { result = contactDB->contacts.add(row); } if (!result) { LOG_ERROR("Failed to update contact."); return false; } contactDB->groups.updateGroups(rec.ID, rec.groups); return true; } auto ContactRecordInterface::getNumbersIDs(std::uint32_t contactID, const ContactRecord &contact, utils::PhoneNumber::Match matchLevel) -> std::vector { std::vector result; auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); for (const auto &number : contact.numbers) { utils::PhoneNumber phoneNumber; try { phoneNumber = utils::PhoneNumber(number.number); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR("Exception in utils::PhoneNumber while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return {}; } auto numberMatch = numberMatcher.bestMatch(phoneNumber, matchLevel); if (!numberMatch.has_value()) { // number does not exist in the DB yet. Let's add it. if (!contactDB->number.add(ContactsNumberTableRow{Record(DB_ID_NONE), .contactID = contactID, .numberUser = number.number.getEntered(), .numbere164 = number.number.getE164(), .type = number.numberType})) { error_db_data("Failed to add new number for contact"); return {}; } result.push_back(contactDB->getLastInsertRowId()); } else { // number already exists in the DB. if (const auto oldContactId = numberMatch->getContactID(); oldContactId != contact.ID) { // It's assigned to another contact. Unbind it from the old contact and bind it the new one. if (!unbindNumber(oldContactId, numberMatch->getNumberID())) { LOG_ERROR("Failed to unbind number %" PRIu32 " from contact %" PRIu32, numberMatch->getNumberID(), oldContactId); continue; } auto numberRecord = contactDB->number.getById(numberMatch->getNumberID()); numberRecord.contactID = contact.ID; if (!contactDB->number.update(numberRecord)) { LOG_ERROR("Failed to re-assign number %" PRIu32 " to contact %" PRIu32, numberMatch->getNumberID(), contact.ID); continue; } } result.push_back(numberMatch->getNumberID()); } } return result; } auto ContactRecordInterface::addNumbers(std::uint32_t contactID, const std::vector &numbers) -> std::optional { std::string numbersIDs; auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); for (const auto &number : numbers) { utils::PhoneNumber phoneNumber; try { phoneNumber = utils::PhoneNumber(number.number); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR("Exception in utils::PhoneNumber while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return std::nullopt; } auto numberMatch = numberMatcher.bestMatch(phoneNumber, utils::PhoneNumber::Match::POSSIBLE); if (!numberMatch.has_value()) { auto result = contactDB->number.add(ContactsNumberTableRow{Record(DB_ID_NONE), .contactID = contactID, .numberUser = number.number.getEntered(), .numbere164 = number.number.getE164(), .type = number.numberType}); if (!result) { return std::nullopt; } numbersIDs += std::to_string(contactDB->getLastInsertRowId()) + " "; } else { auto numberRecord = contactDB->number.getById(numberMatch->getNumberID()); if (!unbindNumber(numberRecord.contactID, numberRecord.ID)) { return std::nullopt; } numberRecord.contactID = contactID; if (!contactDB->number.update(numberRecord)) { return std::nullopt; } numbersIDs += std::to_string(numberRecord.ID) + " "; } } if (!numbersIDs.empty()) { numbersIDs.pop_back(); // remove trailing space } return numbersIDs; } auto ContactRecordInterface::addOrUpdateName(std::uint32_t contactID, std::uint32_t nameID, const ContactRecord &contact) -> std::optional { if (nameID == DB_ID_NONE) { bool result = contactDB->name.add(ContactsNameTableRow{Record(DB_ID_NONE), .contactID = contactID, .namePrimary = contact.primaryName, .nameAlternative = contact.alternativeName}); if (!result) { error_db_data("Failed to add contact name"); return std::nullopt; } nameID = contactDB->getLastInsertRowId(); } else { bool result = contactDB->name.update(ContactsNameTableRow{Record(nameID), .contactID = contactID, .namePrimary = contact.primaryName, .nameAlternative = contact.alternativeName}); if (!result) { error_db_data("Failed to update contact name"); return std::nullopt; } } return nameID; } auto ContactRecordInterface::addOrUpdateAddress(std::uint32_t contactID, std::uint32_t addressID, const ContactRecord &contact) -> std::optional { if (addressID == DB_ID_NONE) { bool result = contactDB->address.add(ContactsAddressTableRow{Record(DB_ID_NONE), .contactID = contactID, .address = contact.address, .note = contact.note, .mail = contact.mail}); if (!result) { error_db_data("Failed to add contact address"); return std::nullopt; } addressID = contactDB->getLastInsertRowId(); } else { bool result = contactDB->address.update(ContactsAddressTableRow{Record(addressID), .contactID = contactID, .address = contact.address, .note = contact.note, .mail = contact.mail}); if (!result) { error_db_data("Failed to update contact address"); return std::nullopt; } } return addressID; } auto ContactRecordInterface::addOrUpdateRingtone(std::uint32_t contactID, std::uint32_t ringtoneID, const ContactRecord &contact) -> std::optional { if (ringtoneID == DB_ID_NONE) { bool result = contactDB->ringtones.add(ContactsRingtonesTableRow{contact.ID, contact.assetPath}); if (!result) { error_db_data("Failed to add contact ringtone"); return std::nullopt; } ringtoneID = contactDB->getLastInsertRowId(); } else { bool result = contactDB->ringtones.update(ContactsRingtonesTableRow{ringtoneID, contact.ID, contact.assetPath}); if (!result) { error_db_data("Failed to update contact ringtone"); return std::nullopt; } } return ringtoneID; } auto ContactRecordInterface::runQuery(std::shared_ptr query) -> std::unique_ptr { if (typeid(*query) == typeid(db::query::ContactGet)) { return getQuery(query); } else if (typeid(*query) == typeid(db::query::ContactGetWithTotalCount)) { return getQueryWithTotalCount(query); } else if (typeid(*query) == typeid(db::query::ContactGetLetterMap)) { return getLetterMapQuery(query); } else if (typeid(*query) == typeid(db::query::ContactGetByID)) { return getByIDQuery(query); } else if (typeid(*query) == typeid(db::query::ContactGetByNumberID)) { return getByNumberIDQuery(query); } else if (typeid(*query) == typeid(db::query::ContactGetSize)) { return getSizeQuery(query); } else if (typeid(*query) == typeid(db::query::ContactAdd)) { return addQuery(query); } else if (typeid(*query) == typeid(db::query::ContactUpdate)) { return updateQuery(query); } else if (typeid(*query) == typeid(db::query::ContactRemove)) { return removeQuery(query); } else if (typeid(*query) == typeid(db::query::NumberGetByID)) { return numberGetByIdQuery(query); } else if (typeid(*query) == typeid(db::query::MergeContactsList)) { return mergeContactsListQuery(query); } else if (typeid(*query) == typeid(db::query::CheckContactsListDuplicates)) { return checkContactsListDuplicatesQuery(query); } error_db_data("Unexpected query type."); return nullptr; } auto ContactRecordInterface::getQueryRecords(const std::shared_ptr &query) -> std::vector { auto textFilter = dynamic_cast(query.get()); assert(query != nullptr); bool searchByNumber = false; if (textFilter != nullptr && textFilter->isFilterPresent() && utils::is_number(textFilter->getFilterData())) { searchByNumber = true; debug_db_data("Filtering by number: %s", textFilter->getFilterData().c_str()); } auto readQuery = static_cast(query.get()); debug_db_data("Contact read query, filter: \"%s\", offset=%lu, limit=%lu", readQuery->getFilterData().c_str(), static_cast(readQuery->getOffset()), static_cast(readQuery->getLimit())); auto [limit, offset] = readQuery->getLimitOffset(); auto matchType = searchByNumber ? ContactsTable::MatchType::TextNumber : ContactsTable::MatchType::Name; uint32_t groupID = readQuery->getGroupFilterData(); if (groupID != DB_ID_NONE) { matchType = ContactsTable::MatchType::Group; } else { groupID = favouritesGroupId; } debug_db_data("Contact match Type: %lu", static_cast(matchType)); std::vector ids; if (readQuery->getContactDisplayMode() == static_cast(ContactDisplayMode::Regular)) { ids = contactDB->contacts.GetIDsSortedByField(matchType, readQuery->getFilterData(), groupID, limit, offset); } else { ids = contactDB->contacts.GetIDsSortedByName(limit, offset); } debug_db_data("Received records: %lu", static_cast(ids.size())); std::vector result(ids.size()); std::transform(std::begin(ids), std::end(ids), std::begin(result), [this](uint32_t id) { return GetByID(id); }); for (uint32_t idx = 0; idx < static_cast(ids.size()); idx++) { result[idx].contactPosOnList = offset + idx; } return result; } auto ContactRecordInterface::getQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto response = std::make_unique(getQueryRecords(query)); response->setRequestQuery(query); return response; } auto ContactRecordInterface::getQueryWithTotalCount(const std::shared_ptr &query) -> const std::unique_ptr { if (auto queryContacts = dynamic_cast(query.get())) { auto querySize = std::make_shared(queryContacts->getFilterData(), queryContacts->getGroupFilterData(), queryContacts->getContactDisplayMode()); auto response = std::make_unique(getQueryRecords(query), getContactsSize(querySize)); response->setRequestQuery(query); return response; } return nullptr; } auto ContactRecordInterface::getLetterMapQuery(const std::shared_ptr &query) -> const std::unique_ptr { ContactsMapData result = contactDB->contacts.GetPosOfFirstLetters(); auto response = std::make_unique(result); response->setRequestQuery(query); return response; } auto ContactRecordInterface::getByIDQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto readQuery = static_cast(query.get()); ContactRecord record; if (readQuery->getWithTemporary()) { record = ContactRecordInterface::GetByIdWithTemporary(readQuery->getID()); } else { record = ContactRecordInterface::GetByID(readQuery->getID()); } auto response = std::make_unique(record); response->setRequestQuery(query); return response; } auto ContactRecordInterface::getByNumberIDQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto readQuery = static_cast(query.get()); ContactRecord contactRecord; auto numberRecord = contactDB->number.getById(readQuery->numberID); if (numberRecord.isValid()) { auto row = contactDB->contacts.getById(numberRecord.contactID); contactRecord = getByIdCommon(row); } auto response = std::make_unique(contactRecord); response->setRequestQuery(query); return response; } auto ContactRecordInterface::getContactsSize(const std::shared_ptr &query) -> std::size_t { auto textFilter = dynamic_cast(query.get()); assert(query != nullptr); bool searchByNumber = false; if (textFilter != nullptr && textFilter->isFilterPresent() && utils::is_number(textFilter->getFilterData())) { searchByNumber = true; debug_db_data("Filtering by number: %s", textFilter->getFilterData().c_str()); } auto countQuery = static_cast(query.get()); debug_db_data("Contact count query, filter: \"%s\"", countQuery->getFilterData().c_str()); std::size_t count = 0; if (!countQuery->isFilterPresent()) { uint32_t groupID = countQuery->getGroupFilterData(); if (groupID != DB_ID_NONE) { count = contactDB->contacts .GetIDsSortedByField(ContactsTable::MatchType::Group, countQuery->getFilterData(), groupID) .size(); } else { if (countQuery->getContactDisplayMode() == static_cast(ContactDisplayMode::Regular)) { count = contactDB->contacts.count(); } else { count = contactDB->contacts.GetIDsSortedByName().size(); } } } else if (searchByNumber) { uint32_t groupID = countQuery->getGroupFilterData(); // incl search by letter if (groupID != DB_ID_NONE) { count = contactDB->contacts .GetIDsSortedByField(ContactsTable::MatchType::Group, countQuery->getFilterData(), groupID) .size(); } else { count = contactDB->contacts.count(); } } else { count = contactDB->name.GetCountByName(countQuery->getFilterData()); } return count; } auto ContactRecordInterface::getSizeQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto count = getContactsSize(query); debug_db_data("Contact count query result: %lu", static_cast(count)); auto response = std::make_unique(count); response->setRequestQuery(query); return response; } auto ContactRecordInterface::getForListQuery(const std::shared_ptr &query) -> const std::unique_ptr { return nullptr; } auto ContactRecordInterface::addQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto addQuery = dynamic_cast(query.get()); if (addQuery == nullptr) { LOG_ERROR("Dynamic casting db::query::ContactAdd has failed!"); return std::unique_ptr(); } auto result = false; auto duplicates = verifyDuplicate(addQuery->rec); if (duplicates.empty()) { result = ContactRecordInterface::Add(addQuery->rec); } auto response = std::make_unique(result, addQuery->rec.ID, duplicates); response->setRequestQuery(query); if (addQuery->rec.ID != DB_ID_NONE) { response->setRecordID(addQuery->rec.ID); } return response; } auto ContactRecordInterface::updateQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto updateQuery = dynamic_cast(query.get()); if (updateQuery == nullptr) { LOG_ERROR("Dynamic casting db::query::ContactUpdate has failed!"); return std::unique_ptr(); } auto result = false; // Checking for duplicates along with omitting this contact in the DB in the search auto duplicates = verifyDuplicate(updateQuery->rec, updateQuery->rec.ID); if (duplicates.empty()) { result = ContactRecordInterface::Update(updateQuery->rec); } auto response = std::make_unique(result, updateQuery->rec.ID, duplicates); response->setRequestQuery(query); response->setRecordID(updateQuery->rec.ID); return response; } auto ContactRecordInterface::removeQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto removeQuery = static_cast(query.get()); auto result = ContactRecordInterface::RemoveByID(removeQuery->getID()); auto response = std::make_unique(result); response->setRequestQuery(query); response->setRecordID(removeQuery->getID()); return response; } auto ContactRecordInterface::numberGetByIdQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto numberQuery = static_cast(query.get()); auto result = ContactRecordInterface::GetNumberById(numberQuery->getID()); auto response = std::make_unique(result); response->setRequestQuery(query); return response; } auto ContactRecordInterface::mergeContactsListQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto mergeQuery = static_cast(query.get()); auto result = ContactRecordInterface::MergeContactsList(mergeQuery->getContactsList()); auto response = std::make_unique(result); response->setRequestQuery(query); return response; } auto ContactRecordInterface::checkContactsListDuplicatesQuery(const std::shared_ptr &query) -> const std::unique_ptr { auto mergeQuery = static_cast(query.get()); auto response = std::make_unique( ContactRecordInterface::CheckContactsListDuplicates(mergeQuery->getContactsList())); response->setRequestQuery(query); return response; } auto ContactRecordInterface::splitNumberIDs(const std::string &numberIDs) -> const std::vector { std::stringstream source(numberIDs); return std::vector(std::istream_iterator(source), std::istream_iterator()); } auto ContactRecordInterface::joinNumberIDs(const std::vector &numberIDs) -> std::string { if (numberIDs.empty()) { return {}; } std::ostringstream outStream; std::ostream_iterator outIterator(outStream, " "); std::copy(std::begin(numberIDs), std::end(numberIDs), outIterator); return outStream.str(); } auto ContactRecordInterface::unbindNumber(std::uint32_t contactId, std::uint32_t numberId) -> bool { auto contactRecord = contactDB->contacts.getByIdWithTemporary(contactId); if (!contactRecord.isValid()) { return false; } auto numberRecord = contactDB->number.getById(numberId); if (!numberRecord.isValid()) { return false; } // unbind contact from number numberRecord.contactID = DB_ID_NONE; if (!contactDB->number.update(numberRecord)) { return false; } // unbind number from contact auto numberIDs = splitNumberIDs(contactRecord.numbersID); numberIDs.erase(std::remove(std::begin(numberIDs), std::end(numberIDs), numberId), std::end(numberIDs)); contactRecord.numbersID = joinNumberIDs(numberIDs); return contactDB->contacts.update(contactRecord); } void ContactRecordInterface::unbindSpeedDialKeyFromOtherContacts(const UTF8 &key) { auto speedDialContacts = GetBySpeedDial(key); if (speedDialContacts->empty()) { return; } for (const auto &contact : *(speedDialContacts)) { auto oldContact = contactDB->contacts.getById(contact.ID); oldContact.speedDial.clear(); if (!contactDB->contacts.update(oldContact)) { error_db_data("Failed to remove speed dial from old contact"); } } } auto ContactRecordInterface::GetByID(uint32_t id) -> ContactRecord { auto contact = contactDB->contacts.getById(id); return getByIdCommon(contact); } auto ContactRecordInterface::GetByIdWithTemporary(uint32_t id) -> ContactRecord { debug_db_data("looking contact %" PRIu32 " with tmp", id); auto contact = contactDB->contacts.getByIdWithTemporary(id); return getByIdCommon(contact); } auto ContactRecordInterface::getByIdCommon(ContactsTableRow &contact) -> ContactRecord { ContactRecord rec = ContactRecord(); debug_db_data("%" PRIu32, contact.ID); if (!contact.isValid()) { return rec; } rec.ID = contact.ID; rec.speeddial = contact.speedDial; rec.groups = contactDB->groups.getGroupsForContact(contact.ID); auto numbers = getNumbers(contact.numbersID); if (!numbers.empty()) { rec.numbers = numbers; } else { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (ring.isValid()) { rec.assetPath = ring.assetPath; } else { debug_db_data("no ring record"); } auto address = contactDB->address.getById(contact.addressID); if (address.isValid()) { rec.address = address.address; rec.note = address.note; rec.mail = address.mail; } else { debug_db_data("no addres record"); } auto name = contactDB->name.getById(contact.nameID); if (name.isValid()) { rec.primaryName = name.namePrimary; rec.alternativeName = name.nameAlternative; } else { debug_db_data("no name record"); } return rec; } auto ContactRecordInterface::GetCount() -> uint32_t { return contactDB->contacts.count(); } auto ContactRecordInterface::GetCountFavourites() -> uint32_t { return contactDB->contacts.countByFieldId("favourites", 1); } auto ContactRecordInterface::GetLimitOffset(uint32_t offset, uint32_t limit) -> std::unique_ptr> { auto records = std::make_unique>(); auto result = contactDB->name.getLimitOffset(offset, limit); for (const auto &w : result) { auto contact = contactDB->contacts.getById(w.ID); if (!contact.isValid()) { return records; } auto name = contactDB->name.getById(contact.nameID); if (!name.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(contact.ID), .primaryName = name.namePrimary, .alternativeName = name.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(contact.ID)}); } return records; } auto ContactRecordInterface::GetLimitOffsetByField(uint32_t offset, uint32_t limit, ContactRecordField field, const char *str) -> std::unique_ptr> { auto records = std::make_unique>(); switch (field) { case ContactRecordField::PrimaryName: { auto result = contactDB->name.getLimitOffsetByField(offset, limit, ContactNameTableFields::NamePrimary, str); for (const auto &record : result) { auto contact = contactDB->contacts.getById(record.contactID); if (!contact.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(record.ID), .primaryName = record.namePrimary, .alternativeName = record.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(record.ID)}); } } break; case ContactRecordField::NumberUser: { auto ret = contactDB->number.getLimitOffsetByField(offset, limit, ContactNumberTableFields::NumberUser, str); for (const auto &record : ret) { auto contact = contactDB->contacts.getById(record.contactID); if (!contact.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto name = contactDB->name.getById(contact.nameID); if (!name.isValid()) { return records; } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(record.ID), .primaryName = name.namePrimary, .alternativeName = name.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(record.ID) }); } } break; case ContactRecordField::NumberE164: { auto result = contactDB->number.getLimitOffsetByField(offset, limit, ContactNumberTableFields::NumberE164, str); for (const auto &record : result) { auto contact = contactDB->contacts.getById(record.contactID); if (!contact.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto name = contactDB->name.getById(contact.nameID); if (!name.isValid()) { return records; } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{ Record(record.ID), .primaryName = name.namePrimary, .alternativeName = name.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(record.ID), }); } } break; case ContactRecordField::SpeedDial: { auto result = contactDB->contacts.getLimitOffsetByField(0, 1, ContactTableFields::SpeedDial, str); for (const auto &w : result) { auto contact = contactDB->contacts.getById(w.ID); if (!contact.isValid()) { return records; } auto name = contactDB->name.getById(contact.nameID); if (!name.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(contact.ID), .primaryName = name.namePrimary, .alternativeName = name.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(contact.ID)}); } } break; case ContactRecordField::Groups: { break; } } return records; } auto ContactRecordInterface::GetByName(const UTF8 &primaryName, const UTF8 &alternativeName) -> std::unique_ptr> { auto records = std::make_unique>(); auto result = contactDB->name.GetByName(primaryName.c_str(), alternativeName.c_str()); for (const auto &record : result) { auto contact = contactDB->contacts.getById(record.contactID); if (!contact.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(record.ID), .primaryName = record.namePrimary, .alternativeName = record.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(record.ID)}); } return records; } auto ContactRecordInterface::Search(const char *primaryName, const char *alternativeName, const char *number) -> std::unique_ptr> { auto records = std::make_unique>(); auto result = contactDB->contacts.Search(primaryName, alternativeName, number); for (const auto &record : result) { auto contact = contactDB->contacts.getById(record.ID); if (!contact.isValid()) { return records; } auto nrs = getNumbers(contact.numbersID); if (nrs.empty()) { debug_db_data("Contact record does not contain any numbers."); } auto ring = contactDB->ringtones.getById(contact.ringID); if (!ring.isValid()) { return records; } auto address = contactDB->address.getById(contact.addressID); if (!address.isValid()) { return records; } records->push_back(ContactRecord{Record(record.ID), .primaryName = record.namePrimary, .alternativeName = record.nameAlternative, .numbers = nrs, .address = address.address, .note = address.note, .mail = address.mail, .assetPath = ring.assetPath, .speeddial = contact.speedDial, .groups = contactDB->groups.getGroupsForContact(record.ID)}); } return records; } auto ContactRecordInterface::getContactByNumber(const UTF8 &number) -> const std::unique_ptr> { return GetLimitOffsetByField(0, 1, ContactRecordField::NumberUser, number.c_str()); } auto ContactRecordInterface::GetByNumber(const UTF8 &number, CreateTempContact createTempContact) -> std::unique_ptr> { try { return GetByNumber(utils::PhoneNumber(number.c_str(), utils::country::Id::UNKNOWN).getView(), createTempContact); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR( "Exception in utils::PhoneNumber while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return {}; } } auto ContactRecordInterface::GetByNumber(const utils::PhoneNumber::View &numberView, CreateTempContact createTempContact) -> std::unique_ptr> { const auto &number = numberView.getEntered(); auto result = getContactByNumber(number); if (!result->empty()) { return result; } // Contact not found, create temporary one if (createTempContact == CreateTempContact::True) { debug_db_data("Cannot find contact for number %s, creating temporary one", number.c_str()); const auto tmpRecord = addTemporaryContactForNumber(ContactRecord::Number(numberView)); if (!tmpRecord.has_value()) { return result; } result->push_back(GetByID(tmpRecord.value().ID)); } return result; } auto ContactRecordInterface::GetByNumberID(std::uint32_t numberId) -> std::optional { auto numberRecord = contactDB->number.getById(numberId); if (!numberRecord.isValid()) { return std::nullopt; } auto rawContactRecord = contactDB->contacts.getByIdWithTemporary(numberRecord.contactID); auto contactRecord = getByIdCommon(rawContactRecord); if (!contactRecord.isValid()) { return std::nullopt; } return contactRecord; } auto ContactRecordInterface::addTemporaryContactForNumber(const ContactRecord::Number &number) -> std::optional { ContactRecord tmp; tmp.numbers = std::vector{number}; tmp.addToGroup(contactDB->groups.temporaryId()); if (!Add(tmp)) { error_db_data("Cannot add contact record"); return std::nullopt; } return tmp; } auto ContactRecordInterface::buildNumberMatcher(unsigned int maxPageSize) -> utils::NumberHolderMatcher { return utils::NumberHolderMatcher( [this](const utils::PhoneNumber &number, auto offset, auto limit) { auto numbers = !number.get().empty() ? contactDB->number.getLimitOffset(number.get(), offset, limit) : contactDB->number.getLimitOffset(offset, limit); std::vector contactNumberHolders; contactNumberHolders.reserve(numbers.size()); std::move(numbers.begin(), numbers.end(), std::back_inserter(contactNumberHolders)); return contactNumberHolders; }, maxPageSize); } auto ContactRecordInterface::MatchByNumber(const utils::PhoneNumber::View &numberView, CreateTempContact createTempContact, utils::PhoneNumber::Match matchLevel, const std::uint32_t contactIDToIgnore) -> std::optional { utils::PhoneNumber phoneNumber; try { phoneNumber = utils::PhoneNumber(numberView); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR( "Exception in utils::PhoneNumber while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return std::nullopt; } auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); auto matchedNumber = numberMatcher.bestMatch(phoneNumber, matchLevel, contactIDToIgnore); if (!matchedNumber.has_value()) { if (createTempContact != CreateTempContact::True) { return std::nullopt; } debug_db_data("Cannot find contact for number %s, creating temporary one", numberView.getEntered().c_str()); ContactRecord newContact = {Record(DB_ID_NONE), .numbers = std::vector{ContactRecord::Number(numberView)}, .groups = {contactDB->groups.getById(ContactsDB::temporaryGroupId())}}; if (!Add(newContact)) { error_db_data("Cannot add contact record"); return std::nullopt; } auto contactID = newContact.ID; auto contactTableRow = contactDB->contacts.getByIdWithTemporary(contactID); auto numberIDs = splitNumberIDs(contactTableRow.numbersID); assert(!numberIDs.empty()); auto numberID = numberIDs[0]; return ContactRecordInterface::ContactNumberMatch(GetByIdWithTemporary(contactID), contactID, numberID); } auto contactID = matchedNumber->getContactID(); auto numberID = matchedNumber->getNumberID(); return ContactRecordInterface::ContactNumberMatch(GetByIdWithTemporary(contactID), contactID, numberID); } auto ContactRecordInterface::GetBySpeedDial(const UTF8 &speedDial) -> std::unique_ptr> { return GetLimitOffsetByField(0, 1, ContactRecordField::SpeedDial, speedDial.c_str()); } auto ContactRecordInterface::getNumbers(const std::string &numbersId) -> std::vector { std::vector nrs; for (const auto &nrStr : utils::split(numbersId, ' ')) { auto nrVal = 0L; try { nrVal = std::stol(nrStr); } catch (const std::exception &e) { error_db_data("Convertion error from %s, taking default value %ld", nrStr.c_str(), nrVal); } auto nr = contactDB->number.getById(nrVal); if (!nr.isValid()) { return nrs; } try { auto &&number = nr.numbere164.empty() ? utils::PhoneNumber(nr.numberUser, utils::country::Id::UNKNOWN) : utils::PhoneNumber(nr.numberUser, nr.numbere164); nrs.emplace_back(number.getView(), nr.type, nrVal); } catch (const utils::PhoneNumber::Error &e) { error_db_data( "Invalid contact's number pair: \"%s\" (\"%s\", \"%s\"). Using user number instead of a pair.", e.what(), nr.numberUser.c_str(), nr.numbere164.c_str()); nrs.emplace_back(utils::PhoneNumber(nr.numberUser, utils::country::Id::UNKNOWN).getView(), nr.type, nrVal); } } return nrs; } ContactRecord::Number::Number() = default; ContactRecord::Number::Number(const std::string &entered, const std::string &e164, ContactNumberType n_type, const std::uint64_t id) : number(utils::PhoneNumber(entered, e164).getView()), numberType(n_type), numberId(id) {} ContactRecord::Number::Number(const utils::PhoneNumber::View &number, ContactNumberType n_type, const std::uint64_t id) : number(number), numberType(n_type), numberId(id) {} auto ContactRecordInterface::getAllNumbers() -> const std::vector { static const std::size_t singleDumpSize = 64; std::vector v; std::size_t offset = 0; std::size_t numbersCount = contactDB->number.count(); while (offset < numbersCount) { auto singleRead = contactDB->number.getLimitOffset(offset, singleDumpSize); v.insert(std::end(v), std::begin(singleRead), std::end(singleRead)); offset += singleDumpSize; } return v; } ContactNumberHolder::ContactNumberHolder(ContactsNumberTableRow &&numberRow) : numberRow(std::move(numberRow)) {} auto ContactNumberHolder::getNumber() const noexcept -> utils::PhoneNumber { try { return utils::PhoneNumber{numberRow.numbere164.empty() ? utils::PhoneNumber(numberRow.numberUser) : utils::PhoneNumber(numberRow.numberUser, numberRow.numbere164)}; } catch (const utils::PhoneNumber::Error &) { debug_db_data( "Skipping invalid phone number pair: (%s, %s)", numberRow.numberUser.c_str(), numberRow.numbere164.c_str()); return utils::PhoneNumber{}; } } auto ContactNumberHolder::getContactID() const noexcept -> std::uint32_t { return numberRow.contactID; } auto ContactNumberHolder::getNumberID() const noexcept -> std::uint32_t { return numberRow.ID; } void ContactRecord::addToFavourites(bool add) { if (add) { groups.insert(ContactsDB::favouritesGroupId()); } else { groups.erase(ContactsDB::favouritesGroupId()); } } void ContactRecord::addToIce(bool add) { if (add) { groups.insert(ContactsDB::iceGroupId()); } else { groups.erase(ContactsDB::iceGroupId()); } } void ContactRecord::addToBlocked(bool add) { if (add) { groups.insert(ContactsDB::blockedGroupId()); speeddial.clear(); } else { groups.erase(ContactsDB::blockedGroupId()); } } void ContactRecord::addToGroup(uint32_t groupId) { groups.insert(groupId); } void ContactRecord::removeFromGroup(uint32_t groupId) { groups.erase(groupId); } auto ContactRecord::isOnFavourites() const -> bool { return isOnGroup(ContactsDB::favouritesGroupId()); } auto ContactRecord::isOnIce() const -> bool { return isOnGroup(ContactsDB::iceGroupId()); } auto ContactRecord::isOnBlocked() const -> bool { return isOnGroup(ContactsDB::blockedGroupId()); } auto ContactRecord::isOnGroup(uint32_t groupId) const -> bool { return groups.find(groupId) != groups.end(); } auto ContactRecord::isTemporary() const -> bool { return isOnGroup(ContactsDB::temporaryGroupId()); } ContactRecordInterface::ContactNumberMatch::ContactNumberMatch(ContactRecord rec, std::uint32_t contactId, std::uint32_t numberId) : contact(std::move(rec)), contactId(contactId), numberId(numberId) {} auto ContactRecordInterface::GetNumberById(std::uint32_t numberId) -> utils::PhoneNumber::View { const auto row = contactDB->number.getById(numberId); try { return utils::PhoneNumber(row.numberUser, row.numbere164).getView(); } catch (const utils::PhoneNumber::Error &e) { LOG_ERROR( "Exception in utils::PhoneNumber while processing number %s! Reason: %s", e.getNumber().c_str(), e.what()); return utils::PhoneNumber().getView(); } } auto ContactRecordInterface::GetNumbersIdsByContact(std::uint32_t contactId) -> std::vector { auto rows = contactDB->number.getByContactId(contactId); std::vector numbersIds; numbersIds.reserve(rows.size()); for (const auto &row : rows) { numbersIds.push_back(row.ID); } return numbersIds; } auto ContactRecordInterface::MergeContactsList(std::vector &contacts) -> std::vector> { std::vector> dataForNotification{}; auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); for (auto &contact : contacts) { // Important: Comparing only single number contacts if (contact.numbers.size() > 1) { LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first"); } auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE); if (!matchedNumber.has_value() or matchedNumberRefersToTemporary(matchedNumber.value())) { if (!Add(contact)) { LOG_ERROR("Contacts list merge fail when adding the contact."); } else { dataForNotification.push_back({db::Query::Type::Create, contact.ID}); } } else { // Complete override of the contact data contact.ID = matchedNumber->getContactID(); dataForNotification.push_back({db::Query::Type::Update, contact.ID}); Update(contact); // Rebuild number matcher numberMatcher = buildNumberMatcher(NumberMatcherPageSize); } } return dataForNotification; } auto ContactRecordInterface::CheckContactsListDuplicates(std::vector &contacts) -> std::pair, std::vector> { std::vector unique; std::vector duplicates; auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); for (auto &contact : contacts) { // Important: Comparing only single number contacts if (contact.numbers.size() > 1) { LOG_WARN("Contact with multiple numbers detected - ignoring all numbers except first"); } auto matchedNumber = numberMatcher.bestMatch(contact.numbers[0].number, utils::PhoneNumber::Match::POSSIBLE); if (matchedNumber.has_value() and !matchedNumberRefersToTemporary(matchedNumber.value())) { duplicates.push_back(contact); } else { unique.push_back(contact); } } return {unique, duplicates}; } auto ContactRecordInterface::verifyDuplicate(ContactRecord &record, const std::uint32_t contactIDToIgnore) -> std::vector { std::vector duplicates = {}; for (const auto &number : record.numbers) { auto matchResult = MatchByNumber( number.number, CreateTempContact::False, utils::PhoneNumber::Match::POSSIBLE, contactIDToIgnore); if (!matchResult.has_value() || matchResult.value().contact.isTemporary()) { continue; } duplicates.push_back(number.number); } return duplicates; } auto ContactRecordInterface::verifyTemporary(ContactRecord &record) -> bool { auto isTemporary = false; for (const auto &number : record.numbers) { auto matchResult = MatchByNumber(number.number); if (!matchResult.has_value() || !matchResult.value().contact.isTemporary()) { continue; } isTemporary = true; record.ID = matchResult.value().contactId; break; } return isTemporary; } auto ContactRecordInterface::matchedNumberRefersToTemporary(const ContactNumberHolder &matchedNumber) -> bool { auto contact = GetByNumberID(matchedNumber.getNumberID()); return contact.has_value() and contact->isTemporary(); } auto ContactRecordInterface::changeNumberRecordInPlaceIfCountryCodeIsOnlyDifferent( const std::vector &oldNumberIDs, std::vector &newNumbers) -> bool { if (newNumbers.empty()) { LOG_ERROR("Cannot to change number record in place if country code is only different. " "Empty new number data"); return false; } if (oldNumberIDs.empty()) { LOG_WARN("Cannot to change number record in place if country code is only different. " "Empty old number data."); return true; } for (const auto id : oldNumberIDs) { if (id == 0) LOG_WARN("Number ID == 0"); } auto numberMatcher = buildNumberMatcher(NumberMatcherPageSize); for (const auto oldNumberID : oldNumberIDs) { // pick one of the old number for this contactID (from DB) auto numberRecord = contactDB->number.getById(oldNumberID); utils::PhoneNumber oldPhoneNumber(numberRecord.numberUser, numberRecord.numbere164); for (const auto &newNumberID : newNumbers) { // pick one of the new number from newNumbers utils::PhoneNumber newPhoneNumber(newNumberID.number); // if DB have not such a new number already and if one of this have country code and other doesn't if (!numberMatcher.bestMatch(newPhoneNumber, utils::PhoneNumber::Match::EXACT).has_value() && (newPhoneNumber.match(oldPhoneNumber) == utils::PhoneNumber::Match::POSSIBLE) && ((!oldPhoneNumber.isValid() && newPhoneNumber.isValid()) || (oldPhoneNumber.isValid() && !newPhoneNumber.isValid()))) { // which means that only country code is to add or remove (change of country code is not supported here) // then change old number record in number table to the new number numberRecord.numberUser = newPhoneNumber.get(); numberRecord.numbere164 = newPhoneNumber.toE164(); if (!contactDB->number.update(numberRecord)) { LOG_ERROR("Failed to change number record in place if country code is only different. " "Number update failed."); return false; } } } } return true; } auto ContactRecordInterface::hasContactRecordSameNumbers(const ContactRecord &rec) -> bool { if (rec.numbers.size() >= 2) { if (rec.numbers.size() > 2) { LOG_WARN("Contact record has more than 2 numbers. Checking similarity for first 2 numbers only"); } utils::PhoneNumber firstPhoneNumber(rec.numbers.at(0).number); utils::PhoneNumber secondPhoneNumber(rec.numbers.at(1).number); utils::PhoneNumber::Match matchLevel = firstPhoneNumber.match(secondPhoneNumber); switch (matchLevel) { case utils::PhoneNumber::Match::EXACT: case utils::PhoneNumber::Match::POSSIBLE: return true; default: return false; } } return false; }