154 lines
5.7 KiB
Swift
154 lines
5.7 KiB
Swift
//
|
|
// Copyright 2023 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Contacts
|
|
import Foundation
|
|
import LibSignalClient
|
|
|
|
enum ContactSyncAttachmentBuilder {
|
|
static func buildAttachmentFile(
|
|
contactsManager: OWSContactsManager,
|
|
tx: SDSAnyReadTransaction
|
|
) -> URL? {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard let localAddress = tsAccountManager.localIdentifiers(tx: tx.asV2Read)?.aciAddress else {
|
|
owsFailDebug("Missing localAddress.")
|
|
return nil
|
|
}
|
|
|
|
let fileUrl = OWSFileSystem.temporaryFileUrl(isAvailableWhileDeviceLocked: true)
|
|
guard let outputStream = OutputStream(url: fileUrl, append: false) else {
|
|
owsFailDebug("Could not open outputStream.")
|
|
return nil
|
|
}
|
|
let outputStreamDelegate = OWSStreamDelegate()
|
|
outputStream.delegate = outputStreamDelegate
|
|
outputStream.schedule(in: .current, forMode: .default)
|
|
outputStream.open()
|
|
guard outputStream.streamStatus == .open else {
|
|
owsFailDebug("Could not open outputStream.")
|
|
return nil
|
|
}
|
|
|
|
do {
|
|
defer {
|
|
outputStream.remove(from: .current, forMode: .default)
|
|
outputStream.close()
|
|
}
|
|
try fetchAndWriteContacts(
|
|
to: ContactOutputStream(outputStream: outputStream),
|
|
localAddress: localAddress,
|
|
contactsManager: contactsManager,
|
|
tx: tx
|
|
)
|
|
} catch {
|
|
owsFailDebug("Could not write contacts sync stream: \(error)")
|
|
return nil
|
|
}
|
|
|
|
guard outputStream.streamStatus == .closed, !outputStreamDelegate.hadError else {
|
|
owsFailDebug("Could not close stream.")
|
|
return nil
|
|
}
|
|
|
|
return fileUrl
|
|
}
|
|
|
|
private static func fetchAndWriteContacts(
|
|
to contactOutputStream: ContactOutputStream,
|
|
localAddress: SignalServiceAddress,
|
|
contactsManager: OWSContactsManager,
|
|
tx: SDSAnyReadTransaction
|
|
) throws {
|
|
let threadFinder = ThreadFinder()
|
|
var threadPositions = [Int64: Int]()
|
|
for (inboxPosition, rowId) in try threadFinder.fetchContactSyncThreadRowIds(tx: tx).enumerated() {
|
|
threadPositions[rowId] = inboxPosition + 1 // Row numbers start from 1.
|
|
}
|
|
|
|
let localAccount = localAccountToSync(localAddress: localAddress)
|
|
let otherAccounts = SignalAccount.anyFetchAll(transaction: tx)
|
|
let signalAccounts = [localAccount] + otherAccounts.sorted(
|
|
by: { ($0.recipientPhoneNumber ?? "") < ($1.recipientPhoneNumber ?? "") }
|
|
)
|
|
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
|
|
for signalAccount in signalAccounts {
|
|
try autoreleasepool {
|
|
guard let phoneNumber = signalAccount.recipientPhoneNumber else {
|
|
return
|
|
}
|
|
let signalRecipient = recipientDatabaseTable.fetchRecipient(phoneNumber: phoneNumber, transaction: tx.asV2Read)
|
|
guard let signalRecipient else {
|
|
return
|
|
}
|
|
let contactThread = TSContactThread.getWithContactAddress(signalRecipient.address, transaction: tx)
|
|
let inboxPosition = contactThread?.sqliteRowId.flatMap { threadPositions.removeValue(forKey: $0) }
|
|
try writeContact(
|
|
to: contactOutputStream,
|
|
address: signalRecipient.address,
|
|
contactThread: contactThread,
|
|
signalAccount: signalAccount,
|
|
inboxPosition: inboxPosition,
|
|
tx: tx
|
|
)
|
|
}
|
|
}
|
|
|
|
for (rowId, inboxPosition) in threadPositions.sorted(by: { $0.key < $1.key }) {
|
|
try autoreleasepool {
|
|
guard let contactThread = threadFinder.fetch(rowId: rowId, tx: tx) as? TSContactThread else {
|
|
return
|
|
}
|
|
try writeContact(
|
|
to: contactOutputStream,
|
|
address: contactThread.contactAddress,
|
|
contactThread: contactThread,
|
|
signalAccount: nil,
|
|
inboxPosition: inboxPosition,
|
|
tx: tx
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
private static func writeContact(
|
|
to contactOutputStream: ContactOutputStream,
|
|
address: SignalServiceAddress,
|
|
contactThread: TSContactThread?,
|
|
signalAccount: SignalAccount?,
|
|
inboxPosition: Int?,
|
|
tx: SDSAnyReadTransaction
|
|
) throws {
|
|
let dmStore = DependenciesBridge.shared.disappearingMessagesConfigurationStore
|
|
let dmConfiguration = contactThread.map { dmStore.fetchOrBuildDefault(for: .thread($0), tx: tx.asV2Read) }
|
|
|
|
try contactOutputStream.writeContact(
|
|
aci: address.serviceId as? Aci,
|
|
phoneNumber: address.e164,
|
|
signalAccount: signalAccount,
|
|
disappearingMessagesConfiguration: dmConfiguration,
|
|
inboxPosition: inboxPosition
|
|
)
|
|
}
|
|
|
|
private static func localAccountToSync(localAddress: SignalServiceAddress) -> SignalAccount {
|
|
// OWSContactsOutputStream requires all signalAccount to have a contact.
|
|
return SignalAccount(address: localAddress)
|
|
}
|
|
}
|
|
|
|
private class OWSStreamDelegate: NSObject, StreamDelegate {
|
|
private let _hadError = AtomicBool(false, lock: .sharedGlobal)
|
|
public var hadError: Bool { _hadError.get() }
|
|
|
|
@objc
|
|
public func stream(_ stream: Stream, handle eventCode: Stream.Event) {
|
|
if eventCode == .errorOccurred {
|
|
_hadError.set(true)
|
|
}
|
|
}
|
|
}
|