Anforderungen  |   Konzepte  |   Entwurf  |   Entwicklung  |   Qualitätssicherung  |   Lebenszyklus  |   Steuerung
 
 
 
 


Quelle  ContactsService.swift   Sprache: unbekannt

 
Spracherkennung für: .swift vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]

import Contacts
import Foundation
import OpenClawKit

final class ContactsService: ContactsServicing {
    private static var payloadKeys: [CNKeyDescriptor] {
        [
            CNContactIdentifierKey as CNKeyDescriptor,
            CNContactGivenNameKey as CNKeyDescriptor,
            CNContactFamilyNameKey as CNKeyDescriptor,
            CNContactOrganizationNameKey as CNKeyDescriptor,
            CNContactPhoneNumbersKey as CNKeyDescriptor,
            CNContactEmailAddressesKey as CNKeyDescriptor,
        ]
    }

    func search(params: OpenClawContactsSearchParams) async throws -> OpenClawContactsSearchPayload {
        let store = try await Self.authorizedStore()

        let limit = max(1, min(params.limit ?? 25, 200))

        var contacts: [CNContact] = []
        if let query = params.query?.trimmingCharacters(in: .whitespacesAndNewlines), !query.isEmpty {
            let predicate = CNContact.predicateForContacts(matchingName: query)
            contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
        } else {
            let request = CNContactFetchRequest(keysToFetch: Self.payloadKeys)
            try store.enumerateContacts(with: request) { contact, stop in
                contacts.append(contact)
                if contacts.count >= limit {
                    stop.pointee = true
                }
            }
        }

        let sliced = Array(contacts.prefix(limit))
        let payload = sliced.map { Self.payload(from: $0) }

        return OpenClawContactsSearchPayload(contacts: payload)
    }

    func add(params: OpenClawContactsAddParams) async throws -> OpenClawContactsAddPayload {
        let store = try await Self.authorizedStore()

        let givenName = params.givenName?.trimmingCharacters(in: .whitespacesAndNewlines)
        let familyName = params.familyName?.trimmingCharacters(in: .whitespacesAndNewlines)
        let organizationName = params.organizationName?.trimmingCharacters(in: .whitespacesAndNewlines)
        let displayName = params.displayName?.trimmingCharacters(in: .whitespacesAndNewlines)
        let phoneNumbers = Self.normalizeStrings(params.phoneNumbers)
        let emails = Self.normalizeStrings(params.emails, lowercased: true)

        let hasName = !(givenName ?? "").isEmpty || !(familyName ?? "").isEmpty || !(displayName ?? "").isEmpty
        let hasOrg = !(organizationName ?? "").isEmpty
        let hasDetails = !phoneNumbers.isEmpty || !emails.isEmpty
        guard hasName || hasOrg || hasDetails else {
            throw NSError(domain: "Contacts", code: 2, userInfo: [
                NSLocalizedDescriptionKey: "CONTACTS_INVALID: include a name, organization, phone, or email",
            ])
        }

        if !phoneNumbers.isEmpty || !emails.isEmpty {
            if let existing = try Self.findExistingContact(
                store: store,
                phoneNumbers: phoneNumbers,
                emails: emails)
            {
                return OpenClawContactsAddPayload(contact: Self.payload(from: existing))
            }
        }

        let contact = CNMutableContact()
        contact.givenName = givenName ?? ""
        contact.familyName = familyName ?? ""
        contact.organizationName = organizationName ?? ""
        if contact.givenName.isEmpty && contact.familyName.isEmpty, let displayName {
            contact.givenName = displayName
        }
        contact.phoneNumbers = phoneNumbers.map {
            CNLabeledValue(label: CNLabelPhoneNumberMobile, value: CNPhoneNumber(stringValue: $0))
        }
        contact.emailAddresses = emails.map {
            CNLabeledValue(label: CNLabelHome, value: $0 as NSString)
        }

        let save = CNSaveRequest()
        save.add(contact, toContainerWithIdentifier: nil)
        try store.execute(save)

        let persisted: CNContact
        if !contact.identifier.isEmpty {
            persisted = try store.unifiedContact(
                withIdentifier: contact.identifier,
                keysToFetch: Self.payloadKeys)
        } else {
            persisted = contact
        }

        return OpenClawContactsAddPayload(contact: Self.payload(from: persisted))
    }

    private static func ensureAuthorization(store: CNContactStore, status: CNAuthorizationStatus) async -> Bool {
        switch status {
        case .authorized, .limited:
            return true
        case .notDetermined:
            // Don’t prompt during node.invoke; the caller should instruct the user to grant permission.
            // Prompts block the invoke and lead to timeouts in headless flows.
            return false
        case .restricted, .denied:
            return false
        @unknown default:
            return false
        }
    }

    private static func authorizedStore() async throws -> CNContactStore {
        let store = CNContactStore()
        let status = CNContactStore.authorizationStatus(for: .contacts)
        let authorized = await Self.ensureAuthorization(store: store, status: status)
        guard authorized else {
            throw NSError(domain: "Contacts", code: 1, userInfo: [
                NSLocalizedDescriptionKey: "CONTACTS_PERMISSION_REQUIRED: grant Contacts permission",
            ])
        }
        return store
    }

    private static func normalizeStrings(_ values: [String]?, lowercased: Bool = false) -> [String] {
        (values ?? [])
            .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
            .filter { !$0.isEmpty }
            .map { lowercased ? $0.lowercased() : $0 }
    }

    private static func findExistingContact(
        store: CNContactStore,
        phoneNumbers: [String],
        emails: [String]) throws -> CNContact?
    {
        if phoneNumbers.isEmpty && emails.isEmpty {
            return nil
        }

        var matches: [CNContact] = []

        for phone in phoneNumbers {
            let predicate = CNContact.predicateForContacts(matching: CNPhoneNumber(stringValue: phone))
            let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
            matches.append(contentsOf: contacts)
        }

        for email in emails {
            let predicate = CNContact.predicateForContacts(matchingEmailAddress: email)
            let contacts = try store.unifiedContacts(matching: predicate, keysToFetch: Self.payloadKeys)
            matches.append(contentsOf: contacts)
        }

        return Self.matchContacts(contacts: matches, phoneNumbers: phoneNumbers, emails: emails)
    }

    private static func matchContacts(
        contacts: [CNContact],
        phoneNumbers: [String],
        emails: [String]) -> CNContact?
    {
        let normalizedPhones = Set(phoneNumbers.map { normalizePhone($0) }.filter { !$0.isEmpty })
        let normalizedEmails = Set(emails.map { $0.lowercased() }.filter { !$0.isEmpty })
        var seen = Set<String>()

        for contact in contacts {
            guard seen.insert(contact.identifier).inserted else { continue }
            let contactPhones = Set(contact.phoneNumbers.map { normalizePhone($0.value.stringValue) })
            let contactEmails = Set(contact.emailAddresses.map { String($0.value).lowercased() })

            if !normalizedPhones.isEmpty, !contactPhones.isDisjoint(with: normalizedPhones) {
                return contact
            }
            if !normalizedEmails.isEmpty, !contactEmails.isDisjoint(with: normalizedEmails) {
                return contact
            }
        }

        return nil
    }

    private static func normalizePhone(_ phone: String) -> String {
        let trimmed = phone.trimmingCharacters(in: .whitespacesAndNewlines)
        let digits = trimmed.unicodeScalars.filter { CharacterSet.decimalDigits.contains($0) }
        let normalized = String(String.UnicodeScalarView(digits))
        return normalized.isEmpty ? trimmed : normalized
    }

    private static func payload(from contact: CNContact) -> OpenClawContactPayload {
        OpenClawContactPayload(
            identifier: contact.identifier,
            displayName: CNContactFormatter.string(from: contact, style: .fullName)
                ?? "\(contact.givenName) \(contact.familyName)".trimmingCharacters(in: .whitespacesAndNewlines),
            givenName: contact.givenName,
            familyName: contact.familyName,
            organizationName: contact.organizationName,
            phoneNumbers: contact.phoneNumbers.map { $0.value.stringValue },
            emails: contact.emailAddresses.map { String($0.value) })
    }

#if DEBUG
    static func _test_matches(contact: CNContact, phoneNumbers: [String], emails: [String]) -> Bool {
        matchContacts(contacts: [contact], phoneNumbers: phoneNumbers, emails: emails) != nil
    }
#endif
}

[Dauer der Verarbeitung: 0.21 Sekunden, vorverarbeitet 2026-04-27]

                                                                                                                                                                                                                                                                                                                                                                                                     


Neuigkeiten

     Aktuelles
     Motto des Tages

Software

     Produkte
     Quellcodebibliothek

Aktivitäten

     Artikel über Sicherheit
     Anleitung zur Aktivierung von SSL

Muße

     Gedichte
     Musik
     Bilder

Jenseits des Üblichen ....

Besucherstatistik

Besucherstatistik

Monitoring

Montastic status badge