Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
signalapp
GitHub Repository: signalapp/Signal-iOS
Path: blob/main/SignalServiceKit/Backups/Archiving/Archivers/ChatItem/BackupArchiveContactAttachmentArchiver.swift
1 views
//
// Copyright 2024 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//

import Foundation

class BackupArchiveContactAttachmentArchiver: BackupArchiveProtoStreamWriter {
    private typealias ArchiveFrameError = BackupArchive.ArchiveFrameError<BackupArchive.InteractionUniqueId>
    typealias RestoreInteractionResult = BackupArchive.RestoreInteractionResult
    private typealias RestoreFrameError = BackupArchive.RestoreFrameError<BackupArchive.ChatItemId>

    private let attachmentsArchiver: BackupArchiveMessageAttachmentArchiver

    init(
        attachmentsArchiver: BackupArchiveMessageAttachmentArchiver,
    ) {
        self.attachmentsArchiver = attachmentsArchiver
    }

    func archiveContact(
        _ contact: OWSContact,
        contactAvatarReferencedAttachment: ReferencedAttachment?,
        uniqueInteractionId: BackupArchive.InteractionUniqueId,
        context: BackupArchive.ArchivingContext,
    ) -> BackupArchive.ArchiveInteractionResult<BackupProto_ContactAttachment> {
        let resultType = BackupProto_ContactAttachment.self
        var partialErrors = [ArchiveFrameError]()

        var contactProto = BackupProto_ContactAttachment()

        switch archiveContactName(contact.name).bubbleUp(resultType, partialErrors: &partialErrors) {
        case .continue(let nameProto):
            nameProto.map { contactProto.name = $0 }
        case .bubbleUpError(let errorResult):
            return errorResult
        }

        var phoneNumberProtos = [BackupProto_ContactAttachment.Phone]()
        for phoneNumber in contact.phoneNumbers {
            switch archiveContactPhoneNumber(phoneNumber).bubbleUp(resultType, partialErrors: &partialErrors) {
            case .continue(let phoneNumberProto):
                phoneNumberProto.map { phoneNumberProtos.append($0) }
            case .bubbleUpError(let errorResult):
                return errorResult
            }
        }
        contactProto.number = phoneNumberProtos

        var emailProtos = [BackupProto_ContactAttachment.Email]()
        for email in contact.emails {
            switch archiveContactEmail(email).bubbleUp(resultType, partialErrors: &partialErrors) {
            case .continue(let emailProto):
                emailProto.map { emailProtos.append($0) }
            case .bubbleUpError(let errorResult):
                return errorResult
            }
        }
        contactProto.email = emailProtos

        var addressProtos = [BackupProto_ContactAttachment.PostalAddress]()
        for address in contact.addresses {
            switch archiveContactAddress(address).bubbleUp(resultType, partialErrors: &partialErrors) {
            case .continue(let addressProto):
                addressProto.map { addressProtos.append($0) }
            case .bubbleUpError(let errorResult):
                return errorResult
            }
        }
        contactProto.address = addressProtos

        if let organization = contact.name.organizationName {
            contactProto.organization = organization
        }

        if let contactAvatarReferencedAttachment {
            contactProto.avatar = attachmentsArchiver.archiveContactAvatar(
                referencedAttachment: contactAvatarReferencedAttachment,
                context: context,
            )
        }

        if partialErrors.isEmpty {
            return .success(contactProto)
        } else {
            return .partialFailure(contactProto, partialErrors)
        }
    }

    private func archiveContactName(
        _ contactName: OWSContactName,
    ) -> BackupArchive.ArchiveInteractionResult<BackupProto_ContactAttachment.Name?> {
        var nameProto = BackupProto_ContactAttachment.Name()
        var setSomeName = false

        if let givenName = contactName.givenName?.strippedOrNil {
            nameProto.givenName = givenName
            setSomeName = true
        }
        if let familyName = contactName.familyName?.strippedOrNil {
            nameProto.familyName = familyName
            setSomeName = true
        }
        if let namePrefix = contactName.namePrefix?.strippedOrNil {
            nameProto.prefix = namePrefix
            setSomeName = true
        }
        if let nameSuffix = contactName.nameSuffix?.strippedOrNil {
            nameProto.suffix = nameSuffix
            setSomeName = true
        }
        if let middleName = contactName.middleName?.strippedOrNil {
            nameProto.middleName = middleName
            setSomeName = true
        }
        if let nickname = contactName.nickname?.strippedOrNil {
            nameProto.nickname = nickname
            setSomeName = true
        }

        if setSomeName {
            return .success(nameProto)
        } else {
            return .success(nil)
        }
    }

    private func archiveContactPhoneNumber(
        _ contactPhoneNumber: OWSContactPhoneNumber,
    ) -> BackupArchive.ArchiveInteractionResult<BackupProto_ContactAttachment.Phone?> {
        var phoneProto = BackupProto_ContactAttachment.Phone()
        guard let phoneNumber = contactPhoneNumber.phoneNumber.nilIfEmpty else {
            return .success(nil)
        }
        phoneProto.value = phoneNumber
        if let label = contactPhoneNumber.label {
            phoneProto.label = label
        }
        phoneProto.type = switch contactPhoneNumber.type {
        case .home:
            .home
        case .mobile:
            .mobile
        case .work:
            .work
        case .custom:
            .custom
        }
        return .success(phoneProto)
    }

    private func archiveContactEmail(
        _ contactEmail: OWSContactEmail,
    ) -> BackupArchive.ArchiveInteractionResult<BackupProto_ContactAttachment.Email?> {
        var emailProto = BackupProto_ContactAttachment.Email()
        guard let email = contactEmail.email.nilIfEmpty else {
            return .success(nil)
        }
        emailProto.value = email
        if let label = contactEmail.label {
            emailProto.label = label
        }
        emailProto.type = switch contactEmail.type {
        case .home:
            .home
        case .mobile:
            .mobile
        case .work:
            .work
        case .custom:
            .custom
        }
        return .success(emailProto)
    }

    private func archiveContactAddress(
        _ contactAddress: OWSContactAddress,
    ) -> BackupArchive.ArchiveInteractionResult<BackupProto_ContactAttachment.PostalAddress?> {
        var addressProto = BackupProto_ContactAttachment.PostalAddress()
        var isValid = false
        if let label = contactAddress.label?.nilIfEmpty {
            isValid = true
            addressProto.label = label
        }
        if let street = contactAddress.street?.nilIfEmpty {
            isValid = true
            addressProto.street = street
        }
        if let pobox = contactAddress.pobox?.nilIfEmpty {
            isValid = true
            addressProto.pobox = pobox
        }
        if let neighborhood = contactAddress.neighborhood?.nilIfEmpty {
            isValid = true
            addressProto.neighborhood = neighborhood
        }
        if let city = contactAddress.city?.nilIfEmpty {
            isValid = true
            addressProto.city = city
        }
        if let region = contactAddress.region?.nilIfEmpty {
            isValid = true
            addressProto.region = region
        }
        if let postcode = contactAddress.postcode?.nilIfEmpty {
            isValid = true
            addressProto.postcode = postcode
        }
        if let country = contactAddress.country?.nilIfEmpty {
            isValid = true
            addressProto.country = country
        }
        guard isValid else {
            return .success(nil)
        }
        addressProto.type = switch contactAddress.type {
        case .home:
            .home
        case .work:
            .work
        case .custom:
            .custom
        }
        return .success(addressProto)
    }

    // MARK: - Restore

    func restoreContact(
        _ contactProto: BackupProto_ContactAttachment,
        chatItemId: BackupArchive.ChatItemId,
    ) -> RestoreInteractionResult<OWSContact> {
        var partialErrors = [RestoreFrameError]()

        var givenName: String?
        var familyName: String?
        var namePrefix: String?
        var nameSuffix: String?
        var middleName: String?
        var nickname: String?
        if contactProto.hasName {
            givenName = contactProto.name.givenName.strippedOrNil
            familyName = contactProto.name.familyName.strippedOrNil
            namePrefix = contactProto.name.prefix.strippedOrNil
            nameSuffix = contactProto.name.suffix.strippedOrNil
            middleName = contactProto.name.middleName.strippedOrNil
            nickname = contactProto.name.nickname.strippedOrNil
        }

        let organizationName = contactProto.organization.strippedOrNil

        let contactName = OWSContactName(
            givenName: givenName,
            familyName: familyName,
            namePrefix: namePrefix,
            nameSuffix: nameSuffix,
            middleName: middleName,
            nickname: nickname,
            organizationName: organizationName,
        )

        contactName.ensureDisplayName()

        let contact = OWSContact(name: contactName)

        for phoneNumberProto in contactProto.number {
            switch self
                .restoreContactPhoneNumber(
                    proto: phoneNumberProto,
                    chatItemId: chatItemId,
                )
                .bubbleUp(OWSContact.self, partialErrors: &partialErrors)
            {
            case .continue(let phoneNumber):
                if let phoneNumber {
                    contact.phoneNumbers.append(phoneNumber)
                }
            case .bubbleUpError(let error):
                return error
            }
        }
        for emailProto in contactProto.email {
            switch self
                .restoreContactEmail(
                    proto: emailProto,
                    chatItemId: chatItemId,
                )
                .bubbleUp(OWSContact.self, partialErrors: &partialErrors)
            {
            case .continue(let email):
                if let email {
                    contact.emails.append(email)
                }
            case .bubbleUpError(let error):
                return error
            }
        }
        for addressProto in contactProto.address {
            switch self
                .restoreContactAddress(
                    proto: addressProto,
                    chatItemId: chatItemId,
                )
                .bubbleUp(OWSContact.self, partialErrors: &partialErrors)
            {
            case .continue(let address):
                if let address {
                    contact.addresses.append(address)
                }
            case .bubbleUpError(let error):
                return error
            }
        }

        // Note: the contact attachment's avatar is restored later (if any is set).

        if partialErrors.isEmpty {
            return .success(contact)
        } else {
            return .partialRestore(contact, partialErrors)
        }
    }

    private func restoreContactPhoneNumber(
        proto: BackupProto_ContactAttachment.Phone,
        chatItemId: BackupArchive.ChatItemId,
    ) -> RestoreInteractionResult<OWSContactPhoneNumber?> {
        guard let phoneNumber = proto.value.strippedOrNil else {
            return .partialRestore(nil, [.restoreFrameError(
                .invalidProtoData(.contactAttachmentPhoneNumberMissingValue),
                chatItemId,
            )])
        }

        let type: OWSContactPhoneNumber.`Type`
        switch proto.type {
        case .home:
            type = .home
        case .mobile:
            type = .mobile
        case .work:
            type = .work
        case .custom:
            type = .custom
        case .unknown, .UNRECOGNIZED:
            type = .home
        }

        return .success(OWSContactPhoneNumber(
            type: type,
            label: proto.label.strippedOrNil,
            phoneNumber: phoneNumber,
        ))
    }

    private func restoreContactEmail(
        proto: BackupProto_ContactAttachment.Email,
        chatItemId: BackupArchive.ChatItemId,
    ) -> RestoreInteractionResult<OWSContactEmail?> {
        guard let email = proto.value.strippedOrNil else {
            return .partialRestore(nil, [.restoreFrameError(
                .invalidProtoData(.contactAttachmentEmailMissingValue),
                chatItemId,
            )])
        }

        let type: OWSContactEmail.`Type`
        switch proto.type {
        case .home:
            type = .home
        case .mobile:
            type = .mobile
        case .work:
            type = .work
        case .custom:
            type = .custom
        case .unknown, .UNRECOGNIZED:
            type = .home
        }

        return .success(OWSContactEmail(
            type: type,
            label: proto.label.strippedOrNil,
            email: email,
        ))
    }

    private func restoreContactAddress(
        proto: BackupProto_ContactAttachment.PostalAddress,
        chatItemId: BackupArchive.ChatItemId,
    ) -> RestoreInteractionResult<OWSContactAddress?> {
        let type: OWSContactAddress.`Type`
        switch proto.type {
        case .home:
            type = .home
        case .work:
            type = .work
        case .custom:
            type = .custom
        case .unknown, .UNRECOGNIZED:
            type = .home
        }

        let address = OWSContactAddress(
            type: type,
            label: proto.label.strippedOrNil,
            street: proto.street.strippedOrNil,
            pobox: proto.pobox.strippedOrNil,
            neighborhood: proto.neighborhood.strippedOrNil,
            city: proto.city.strippedOrNil,
            region: proto.region.strippedOrNil,
            postcode: proto.postcode.strippedOrNil,
            country: proto.country.strippedOrNil,
        )

        guard
            address.street?.isEmpty == false
            || address.pobox?.isEmpty == false
            || address.neighborhood?.isEmpty == false
            || address.city?.isEmpty == false
            || address.region?.isEmpty == false
            || address.postcode?.isEmpty == false
            || address.country?.isEmpty == false
        else {
            return .partialRestore(nil, [.restoreFrameError(
                .invalidProtoData(.contactAttachmentEmptyAddress),
                chatItemId,
            )])
        }

        return .success(address)
    }
}