664 lines
29 KiB
Swift
664 lines
29 KiB
Swift
//
|
|
// Copyright 2019 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Contacts
|
|
import Foundation
|
|
import LibSignalClient
|
|
|
|
extension Notification.Name {
|
|
public static let syncManagerConfigurationSyncDidComplete = Notification.Name("OWSSyncManagerConfigurationSyncDidCompleteNotification")
|
|
public static let syncManagerKeysSyncDidComplete = Notification.Name("OWSSyncManagerKeysSyncDidCompleteNotification")
|
|
}
|
|
|
|
@objc
|
|
public class OWSSyncManager: NSObject, SyncManagerProtocolObjc {
|
|
private static var keyValueStore: SDSKeyValueStore {
|
|
SDSKeyValueStore(collection: "kTSStorageManagerOWSSyncManagerCollection")
|
|
}
|
|
private var isRequestInFlight: Bool = false
|
|
|
|
public init(default: Void) {
|
|
super.init()
|
|
SwiftSingletons.register(self)
|
|
AppReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
|
|
self.addObservers()
|
|
|
|
if TSAccountManagerObjcBridge.isRegisteredWithMaybeTransaction {
|
|
if TSAccountManagerObjcBridge.isPrimaryDeviceWithMaybeTransaction {
|
|
// syncAllContactsIfNecessary will skip if nothing has changed,
|
|
// so this won't yield redundant traffic.
|
|
self.syncAllContactsIfNecessary()
|
|
} else {
|
|
self.sendAllSyncRequestMessagesIfNecessary().catch { (_ error: Error) in
|
|
Logger.error("Error: \(error).")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func addObservers() {
|
|
NotificationCenter.default.addObserver(self, selector: #selector(signalAccountsDidChange(_:)), name: .OWSContactsManagerSignalAccountsDidChange, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(profileKeyDidChange(_:)), name: UserProfileNotifications.localProfileKeyDidChange, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(registrationStateDidChange(_:)), name: RegistrationStateChangeNotifications.registrationStateDidChange, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: .OWSApplicationWillEnterForeground, object: nil)
|
|
}
|
|
|
|
// MARK: - Notifications
|
|
|
|
@objc
|
|
private func signalAccountsDidChange(_ notification: AnyObject) {
|
|
AssertIsOnMainThread()
|
|
syncAllContactsIfNecessary()
|
|
}
|
|
|
|
@objc
|
|
private func profileKeyDidChange(_ notification: AnyObject) {
|
|
AssertIsOnMainThread()
|
|
syncAllContactsIfNecessary()
|
|
}
|
|
|
|
@objc
|
|
private func registrationStateDidChange(_ notification: AnyObject) {
|
|
AssertIsOnMainThread()
|
|
syncAllContactsIfNecessary()
|
|
}
|
|
|
|
@objc
|
|
private func willEnterForeground(_ notification: AnyObject) {
|
|
AssertIsOnMainThread()
|
|
_ = syncAllContactsIfFullSyncRequested()
|
|
}
|
|
|
|
// MARK: - SyncManagerProtocolObjc methods
|
|
|
|
public func processIncomingConfigurationSyncMessage(_ syncMessage: SSKProtoSyncMessageConfiguration, transaction: SDSAnyWriteTransaction) {
|
|
if syncMessage.hasReadReceipts {
|
|
SSKEnvironment.shared.receiptManager.setAreReadReceiptsEnabled(syncMessage.readReceipts, transaction: transaction)
|
|
}
|
|
if syncMessage.hasUnidentifiedDeliveryIndicators {
|
|
let updatedValue = syncMessage.unidentifiedDeliveryIndicators
|
|
self.preferences.setShouldShowUnidentifiedDeliveryIndicators(updatedValue, transaction: transaction)
|
|
}
|
|
if syncMessage.hasTypingIndicators {
|
|
self.typingIndicatorsImpl.setTypingIndicatorsEnabled(value: syncMessage.typingIndicators, transaction: transaction)
|
|
}
|
|
if syncMessage.hasLinkPreviews {
|
|
SSKPreferences.setAreLinkPreviewsEnabled(syncMessage.linkPreviews, transaction: transaction)
|
|
}
|
|
transaction.addAsyncCompletionOffMain {
|
|
NotificationCenter.default.postNotificationNameAsync(.syncManagerConfigurationSyncDidComplete, object: nil)
|
|
}
|
|
}
|
|
|
|
public func processIncomingContactsSyncMessage(_ syncMessage: SSKProtoSyncMessageContacts, transaction: SDSAnyWriteTransaction) {
|
|
guard let attachmentPointer = TSAttachmentPointer(fromProto: syncMessage.blob, albumMessage: nil) else {
|
|
owsFailDebug("failed to create attachment pointer from incoming contacts sync message")
|
|
return
|
|
}
|
|
attachmentPointer.anyInsert(transaction: transaction)
|
|
self.smJobQueues.incomingContactSyncJobQueue.add(attachmentId: attachmentPointer.uniqueId, isComplete: syncMessage.isComplete, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
extension OWSSyncManager: SyncManagerProtocol, SyncManagerProtocolSwift {
|
|
|
|
// MARK: - Constants
|
|
|
|
private enum Constants {
|
|
static let lastContactSyncKey = "kTSStorageManagerOWSSyncManagerLastMessageKey"
|
|
static let fullSyncRequestIdKey = "FullSyncRequestId"
|
|
static let syncRequestedAppVersionKey = "SyncRequestedAppVersion"
|
|
}
|
|
|
|
// MARK: - Sync Requests
|
|
|
|
@objc
|
|
public func sendAllSyncRequestMessagesIfNecessary() -> AnyPromise {
|
|
return AnyPromise(_sendAllSyncRequestMessages(onlyIfNecessary: true))
|
|
}
|
|
|
|
@objc
|
|
public func sendAllSyncRequestMessages(timeout: TimeInterval) -> AnyPromise {
|
|
return AnyPromise(_sendAllSyncRequestMessages(onlyIfNecessary: false)
|
|
.timeout(seconds: timeout, substituteValue: ()))
|
|
}
|
|
|
|
private func _sendAllSyncRequestMessages(onlyIfNecessary: Bool) -> Promise<Void> {
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
return Promise(error: OWSAssertionError("Unexpectedly tried to send sync request before registration."))
|
|
}
|
|
|
|
return databaseStorage.write(.promise) { (transaction) -> Promise<Void> in
|
|
let currentAppVersion = AppVersionImpl.shared.currentAppVersion
|
|
let syncRequestedAppVersion = {
|
|
Self.keyValueStore.getString(
|
|
Constants.syncRequestedAppVersionKey,
|
|
transaction: transaction
|
|
)
|
|
}
|
|
|
|
// If we don't need to send sync messages, don't send them.
|
|
if onlyIfNecessary, currentAppVersion == syncRequestedAppVersion() {
|
|
return .value(())
|
|
}
|
|
|
|
// Otherwise, send them & mark that we sent them for this app version.
|
|
self.sendSyncRequestMessage(.blocked, transaction: transaction)
|
|
self.sendSyncRequestMessage(.configuration, transaction: transaction)
|
|
self.sendSyncRequestMessage(.contacts, transaction: transaction)
|
|
self.sendSyncRequestMessage(.keys, transaction: transaction)
|
|
|
|
Self.keyValueStore.setString(
|
|
currentAppVersion,
|
|
key: Constants.syncRequestedAppVersionKey,
|
|
transaction: transaction
|
|
)
|
|
|
|
return Promise.when(fulfilled: [
|
|
NotificationCenter.default.observe(once: .incomingContactSyncDidComplete).asVoid(),
|
|
NotificationCenter.default.observe(once: .syncManagerConfigurationSyncDidComplete).asVoid(),
|
|
NotificationCenter.default.observe(once: BlockingManager.blockedSyncDidComplete).asVoid(),
|
|
NotificationCenter.default.observe(once: .syncManagerKeysSyncDidComplete).asVoid()
|
|
])
|
|
}.then(on: DependenciesBridge.shared.schedulers.sync) { $0 }
|
|
}
|
|
|
|
public func sendKeysSyncMessage() {
|
|
Logger.info("")
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
return owsFailDebug("Unexpectedly tried to send sync request before registration.")
|
|
}
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isPrimaryDevice ?? false else {
|
|
return owsFailDebug("Keys sync should only be initiated from the primary device")
|
|
}
|
|
|
|
databaseStorage.asyncWrite { [weak self] transaction in
|
|
self?.sendKeysSyncMessage(tx: transaction)
|
|
}
|
|
}
|
|
|
|
public func sendKeysSyncMessage(tx: SDSAnyWriteTransaction) {
|
|
Logger.info("")
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationState(tx: tx.asV2Write).isRegisteredPrimaryDevice else {
|
|
return owsFailDebug("Keys sync should only be initiated from the registered primary device")
|
|
}
|
|
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: tx) else {
|
|
return owsFailDebug("Missing thread")
|
|
}
|
|
|
|
let storageServiceKey = DependenciesBridge.shared.svr.data(for: .storageService, transaction: tx.asV2Read)
|
|
let masterKey = DependenciesBridge.shared.svr.masterKeyDataForKeysSyncMessage(tx: tx.asV2Read)
|
|
let syncKeysMessage = OWSSyncKeysMessage(
|
|
thread: thread,
|
|
storageServiceKey: storageServiceKey?.rawData,
|
|
masterKey: masterKey,
|
|
transaction: tx
|
|
)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: syncKeysMessage
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
|
}
|
|
|
|
@objc
|
|
public func processIncomingKeysSyncMessage(_ syncMessage: SSKProtoSyncMessageKeys, transaction: SDSAnyWriteTransaction) {
|
|
guard !DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isRegisteredPrimaryDevice else {
|
|
return owsFailDebug("Key sync messages should only be processed on linked devices")
|
|
}
|
|
|
|
if let masterKey = syncMessage.master {
|
|
DependenciesBridge.shared.svr.storeSyncedMasterKey(
|
|
data: masterKey,
|
|
authedDevice: .implicit,
|
|
updateStorageService: true,
|
|
transaction: transaction.asV2Write
|
|
)
|
|
} else {
|
|
DependenciesBridge.shared.svr.storeSyncedStorageServiceKey(
|
|
data: syncMessage.storageService,
|
|
authedAccount: .implicit(),
|
|
transaction: transaction.asV2Write
|
|
)
|
|
}
|
|
|
|
transaction.addAsyncCompletionOffMain {
|
|
NotificationCenter.default.postNotificationNameAsync(.syncManagerKeysSyncDidComplete, object: nil)
|
|
}
|
|
}
|
|
|
|
public func sendKeysSyncRequestMessage(transaction: SDSAnyWriteTransaction) {
|
|
sendSyncRequestMessage(.keys, transaction: transaction)
|
|
}
|
|
|
|
public func processIncomingFetchLatestSyncMessage(
|
|
_ syncMessage: SSKProtoSyncMessageFetchLatest,
|
|
transaction: SDSAnyWriteTransaction
|
|
) {
|
|
switch syncMessage.unwrappedType {
|
|
case .unknown:
|
|
owsFailDebug("Unknown fetch latest type")
|
|
case .localProfile:
|
|
_ = profileManager.fetchLocalUsersProfile(mainAppOnly: true, authedAccount: .implicit())
|
|
case .storageManifest:
|
|
storageServiceManager.restoreOrCreateManifestIfNecessary(authedDevice: .implicit)
|
|
case .subscriptionStatus:
|
|
SubscriptionManagerImpl.performDeviceSubscriptionExpiryUpdate()
|
|
}
|
|
}
|
|
|
|
@objc
|
|
public func processIncomingMessageRequestResponseSyncMessage(
|
|
_ syncMessage: SSKProtoSyncMessageMessageRequestResponse,
|
|
transaction: SDSAnyWriteTransaction
|
|
) {
|
|
guard let thread: TSThread = {
|
|
if let groupId = syncMessage.groupID {
|
|
TSGroupThread.ensureGroupIdMapping(forGroupId: groupId, transaction: transaction)
|
|
return TSGroupThread.fetch(groupId: groupId, transaction: transaction)
|
|
}
|
|
if let threadAci = Aci.parseFrom(aciString: syncMessage.threadAci) {
|
|
return TSContactThread.getWithContactAddress(SignalServiceAddress(threadAci), transaction: transaction)
|
|
}
|
|
return nil
|
|
}() else {
|
|
return owsFailDebug("message request response couldn't find thread")
|
|
}
|
|
|
|
switch syncMessage.type {
|
|
case .accept:
|
|
blockingManager.removeBlockedThread(thread, wasLocallyInitiated: false, transaction: transaction)
|
|
if let thread = thread as? TSContactThread {
|
|
/// When we accept a message request on a linked device,
|
|
/// we unhide the message sender. We will eventually also
|
|
/// learn about the unhide via a StorageService contact sync,
|
|
/// since the linked device should mark unhidden in
|
|
/// StorageService. But it doesn't hurt to get ahead of the
|
|
/// game and unhide here.
|
|
DependenciesBridge.shared.recipientHidingManager.removeHiddenRecipient(
|
|
thread.contactAddress,
|
|
wasLocallyInitiated: false,
|
|
tx: transaction.asV2Write
|
|
)
|
|
}
|
|
profileManager.addThread(toProfileWhitelist: thread, transaction: transaction)
|
|
case .delete:
|
|
thread.softDelete(with: transaction)
|
|
case .block:
|
|
blockingManager.addBlockedThread(thread, blockMode: .remote, transaction: transaction)
|
|
case .blockAndDelete:
|
|
thread.softDelete(with: transaction)
|
|
blockingManager.addBlockedThread(thread, blockMode: .remote, transaction: transaction)
|
|
case .spam:
|
|
TSInfoMessage(thread: thread, messageType: .reportedSpam).anyInsert(transaction: transaction)
|
|
case .blockAndSpam:
|
|
blockingManager.addBlockedThread(thread, blockMode: .remote, transaction: transaction)
|
|
TSInfoMessage(thread: thread, messageType: .reportedSpam).anyInsert(transaction: transaction)
|
|
case .unknown, .none:
|
|
owsFailDebug("unexpected message request response type")
|
|
}
|
|
}
|
|
|
|
public func sendMessageRequestResponseSyncMessage(thread: TSThread, responseType: OWSSyncMessageRequestResponseType) {
|
|
Logger.info("")
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
return owsFailDebug("Unexpectedly tried to send sync message before registration.")
|
|
}
|
|
|
|
databaseStorage.asyncWrite { [weak self] transaction in
|
|
self?.sendMessageRequestResponseSyncMessage(thread: thread, responseType: responseType, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
public func sendMessageRequestResponseSyncMessage(
|
|
thread: TSThread,
|
|
responseType: OWSSyncMessageRequestResponseType,
|
|
transaction: SDSAnyWriteTransaction
|
|
) {
|
|
Logger.info("")
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationState(tx: transaction.asV2Read).isRegistered else {
|
|
return owsFailDebug("Unexpectedly tried to send sync message before registration.")
|
|
}
|
|
|
|
let syncMessageRequestResponse = OWSSyncMessageRequestResponseMessage(thread: thread, responseType: responseType, transaction: transaction)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: syncMessageRequestResponse
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
|
|
}
|
|
|
|
// MARK: - Configuration Sync
|
|
|
|
public func sendConfigurationSyncMessage() {
|
|
AppReadiness.runNowOrWhenAppDidBecomeReadyAsync {
|
|
Task { await self.databaseStorage.awaitableWrite(block: self._sendConfigurationSyncMessage(tx:)) }
|
|
}
|
|
}
|
|
|
|
private func _sendConfigurationSyncMessage(tx: SDSAnyWriteTransaction) {
|
|
Logger.info("")
|
|
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationState(tx: tx.asV2Read).isRegistered else {
|
|
return
|
|
}
|
|
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: tx) else {
|
|
owsFailDebug("Missing thread.")
|
|
return
|
|
}
|
|
|
|
let linkPreviews = SSKPreferences.areLinkPreviewsEnabled(transaction: tx)
|
|
let readReceipts = receiptManager.areReadReceiptsEnabled(transaction: tx)
|
|
let sealedSenderIndicators = preferences.shouldShowUnidentifiedDeliveryIndicators(transaction: tx)
|
|
let typingIndicators = typingIndicatorsImpl.areTypingIndicatorsEnabled()
|
|
|
|
let configurationSyncMessage = OWSSyncConfigurationMessage(
|
|
thread: thread,
|
|
readReceiptsEnabled: readReceipts,
|
|
showUnidentifiedDeliveryIndicators: sealedSenderIndicators,
|
|
showTypingIndicators: typingIndicators,
|
|
sendLinkPreviews: linkPreviews,
|
|
transaction: tx
|
|
)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: configurationSyncMessage
|
|
)
|
|
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
|
}
|
|
|
|
// MARK: - Contact Sync
|
|
|
|
public func syncAllContacts() -> AnyPromise {
|
|
owsAssertDebug(canSendContactSyncMessage())
|
|
return AnyPromise(syncContacts(mode: .allSignalAccounts))
|
|
}
|
|
|
|
@objc
|
|
func syncAllContactsIfNecessary() {
|
|
owsAssertDebug(CurrentAppContext().isMainApp)
|
|
_ = syncContacts(mode: .allSignalAccountsIfChanged)
|
|
}
|
|
|
|
public func syncAllContactsIfFullSyncRequested() -> AnyPromise {
|
|
owsAssertDebug(CurrentAppContext().isMainApp)
|
|
return AnyPromise(syncContacts(mode: .allSignalAccountsIfFullSyncRequested))
|
|
}
|
|
|
|
private enum ContactSyncMode {
|
|
case allSignalAccounts
|
|
case allSignalAccountsIfChanged
|
|
case allSignalAccountsIfFullSyncRequested
|
|
}
|
|
|
|
private func canSendContactSyncMessage() -> Bool {
|
|
guard AppReadiness.isAppReady else {
|
|
return false
|
|
}
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegisteredPrimaryDevice else {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static let contactSyncQueue = DispatchQueue(label: "org.signal.contact-sync", autoreleaseFrequency: .workItem)
|
|
|
|
private func syncContacts(mode: ContactSyncMode) -> Promise<Void> {
|
|
if DebugFlags.dontSendContactOrGroupSyncMessages.get() {
|
|
Logger.info("Skipping contact sync message.")
|
|
return .value(())
|
|
}
|
|
|
|
guard canSendContactSyncMessage() else {
|
|
return Promise(error: OWSGenericError("Not ready to sync contacts."))
|
|
}
|
|
|
|
return Promise { future in
|
|
Self.contactSyncQueue.async {
|
|
do {
|
|
future.resolve(on: SyncScheduler(), with: try self._syncContacts(mode: mode))
|
|
} catch {
|
|
future.reject(error)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private func _syncContacts(mode: ContactSyncMode) throws -> Promise<Void> {
|
|
// Don't bother sending sync messages with the same data as the last
|
|
// successfully sent contact sync message.
|
|
let opportunistic = mode == .allSignalAccountsIfChanged
|
|
// Only have one sync message in flight at a time.
|
|
let debounce = mode == .allSignalAccountsIfChanged
|
|
|
|
if debounce, self.isRequestInFlight {
|
|
// De-bounce. It's okay if we ignore some new changes;
|
|
// `syncAllContactsIfNecessary` is called fairly often so we'll sync soon.
|
|
return .value(())
|
|
}
|
|
|
|
if CurrentAppContext().isNSE {
|
|
// If a full sync is specifically requested in the NSE, mark it so that the
|
|
// main app can send that request the next time in runs.
|
|
if mode == .allSignalAccounts {
|
|
databaseStorage.write { tx in
|
|
Self.keyValueStore.setString(UUID().uuidString, key: Constants.fullSyncRequestIdKey, transaction: tx)
|
|
}
|
|
}
|
|
// If a full sync sync is requested in NSE, ignore it. Opportunistic syncs
|
|
// shouldn't be requested, but this guards against cases where they are.
|
|
return .value(())
|
|
}
|
|
|
|
guard let thread = TSContactThread.getOrCreateLocalThreadWithSneakyTransaction() else {
|
|
owsFailDebug("Missing thread.")
|
|
throw OWSError(error: .contactSyncFailed, description: "Could not sync contacts.", isRetryable: false)
|
|
}
|
|
|
|
let result = try databaseStorage.read { tx in try buildContactSyncMessage(in: thread, mode: mode, tx: tx) }
|
|
guard let result else {
|
|
return .value(())
|
|
}
|
|
|
|
let messageHash: Data
|
|
do {
|
|
messageHash = try Cryptography.computeSHA256DigestOfFile(at: result.syncFileUrl)
|
|
} catch {
|
|
owsFailDebug("Error: \(error).")
|
|
throw OWSError(error: .contactSyncFailed, description: "Could not sync contacts.", isRetryable: false)
|
|
}
|
|
|
|
// If the NSE requested a sync and the main app does an opportunistic sync,
|
|
// we should send that request since we've been given a strong signal that
|
|
// someone is waiting to receive this message.
|
|
if opportunistic, result.fullSyncRequestId == nil, messageHash == result.previousMessageHash {
|
|
// Ignore redundant contacts sync message.
|
|
return .value(())
|
|
}
|
|
|
|
let dataSource = try DataSourcePath.dataSource(with: result.syncFileUrl, shouldDeleteOnDeallocation: true)
|
|
|
|
if debounce {
|
|
self.isRequestInFlight = true
|
|
}
|
|
return Promise.wrapAsync {
|
|
defer {
|
|
if debounce {
|
|
Self.contactSyncQueue.async {
|
|
self.isRequestInFlight = false
|
|
}
|
|
}
|
|
}
|
|
try await self.messageSender.sendTransientContactSyncAttachment(dataSource: dataSource, thread: thread)
|
|
await self.databaseStorage.awaitableWrite { tx in
|
|
Self.keyValueStore.setData(messageHash, key: Constants.lastContactSyncKey, transaction: tx)
|
|
self.clearFullSyncRequestId(ifMatches: result.fullSyncRequestId, tx: tx)
|
|
}
|
|
}
|
|
}
|
|
|
|
private struct BuildContactSyncMessageResult {
|
|
var syncFileUrl: URL
|
|
var fullSyncRequestId: String?
|
|
var previousMessageHash: Data?
|
|
}
|
|
|
|
private func buildContactSyncMessage(
|
|
in thread: TSThread,
|
|
mode: ContactSyncMode,
|
|
tx: SDSAnyReadTransaction
|
|
) throws -> BuildContactSyncMessageResult? {
|
|
// Check if there's a pending request from the NSE. Any full sync in the
|
|
// main app can clear this flag, even if it's not started in response to
|
|
// calling syncAllContactsIfFullSyncRequested.
|
|
let fullSyncRequestId = Self.keyValueStore.getString(Constants.fullSyncRequestIdKey, transaction: tx)
|
|
|
|
// However, only syncAllContactsIfFullSyncRequested-initiated requests
|
|
// should be skipped if there's no request.
|
|
if mode == .allSignalAccountsIfFullSyncRequested, fullSyncRequestId == nil {
|
|
return nil
|
|
}
|
|
|
|
guard let syncFileUrl = ContactSyncAttachmentBuilder.buildAttachmentFile(
|
|
contactsManager: Self.contactsManagerImpl,
|
|
tx: tx
|
|
) else {
|
|
owsFailDebug("Failed to serialize contacts sync message.")
|
|
throw OWSError(error: .contactSyncFailed, description: "Could not sync contacts.", isRetryable: false)
|
|
}
|
|
return BuildContactSyncMessageResult(
|
|
syncFileUrl: syncFileUrl,
|
|
fullSyncRequestId: fullSyncRequestId,
|
|
previousMessageHash: Self.keyValueStore.getData(Constants.lastContactSyncKey, transaction: tx)
|
|
)
|
|
}
|
|
|
|
private func clearFullSyncRequestId(ifMatches requestId: String?, tx: SDSAnyWriteTransaction) {
|
|
guard let requestId else {
|
|
return
|
|
}
|
|
let storedRequestId = Self.keyValueStore.getString(Constants.fullSyncRequestIdKey, transaction: tx)
|
|
// If the requestId we just finished matches the one in the database, we've
|
|
// fulfilled the contract with the NSE. If the NSE triggers *another* sync
|
|
// while this is outstanding, the match will fail, and we'll kick off
|
|
// another sync at the next opportunity.
|
|
if storedRequestId == requestId {
|
|
Self.keyValueStore.removeValue(forKey: Constants.fullSyncRequestIdKey, transaction: tx)
|
|
}
|
|
}
|
|
|
|
// MARK: - Fetch Latest
|
|
|
|
public func sendFetchLatestProfileSyncMessage(tx: SDSAnyWriteTransaction) {
|
|
_sendFetchLatestSyncMessage(type: .localProfile, tx: tx)
|
|
}
|
|
|
|
public func sendFetchLatestStorageManifestSyncMessage() { sendFetchLatestSyncMessage(type: .storageManifest) }
|
|
|
|
public func sendFetchLatestSubscriptionStatusSyncMessage() { sendFetchLatestSyncMessage(type: .subscriptionStatus) }
|
|
|
|
private func sendFetchLatestSyncMessage(type: OWSSyncFetchType) {
|
|
Task { await self.databaseStorage.awaitableWrite { tx in self._sendFetchLatestSyncMessage(type: type, tx: tx) } }
|
|
}
|
|
|
|
private func _sendFetchLatestSyncMessage(type: OWSSyncFetchType, tx: SDSAnyWriteTransaction) {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationState(tx: tx.asV2Read).isRegistered else {
|
|
owsFailDebug("Tried to send sync message before registration.")
|
|
return
|
|
}
|
|
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: tx) else {
|
|
owsFailDebug("Missing thread.")
|
|
return
|
|
}
|
|
|
|
let fetchLatestSyncMessage = OWSSyncFetchLatestMessage(thread: thread, fetchType: type, transaction: tx)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: fetchLatestSyncMessage
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: tx)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public extension OWSSyncManager {
|
|
|
|
func sendInitialSyncRequestsAwaitingCreatedThreadOrdering(timeoutSeconds: TimeInterval) -> Promise<[String]> {
|
|
Logger.info("")
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
return Promise(error: OWSAssertionError("Unexpectedly tried to send sync request before registration."))
|
|
}
|
|
|
|
databaseStorage.asyncWrite { transaction in
|
|
self.sendSyncRequestMessage(.blocked, transaction: transaction)
|
|
self.sendSyncRequestMessage(.configuration, transaction: transaction)
|
|
self.sendSyncRequestMessage(.contacts, transaction: transaction)
|
|
}
|
|
|
|
let notificationsPromise: Promise<([(threadUniqueId: String, sortOrder: UInt32)], Void, Void)> = Promise.when(fulfilled:
|
|
NotificationCenter.default.observe(once: .incomingContactSyncDidComplete).map { $0.insertedThreads }.timeout(seconds: timeoutSeconds, substituteValue: []),
|
|
NotificationCenter.default.observe(once: .syncManagerConfigurationSyncDidComplete).asVoid().timeout(seconds: timeoutSeconds),
|
|
NotificationCenter.default.observe(once: BlockingManager.blockedSyncDidComplete).asVoid().timeout(seconds: timeoutSeconds)
|
|
)
|
|
|
|
return notificationsPromise.map { (insertedThreads, _, _) -> [String] in
|
|
return insertedThreads.sorted(by: { $0.sortOrder < $1.sortOrder }).map({ $0.threadUniqueId })
|
|
}
|
|
}
|
|
|
|
@objc
|
|
fileprivate func sendSyncRequestMessage(_ requestType: SSKProtoSyncMessageRequestType,
|
|
transaction: SDSAnyWriteTransaction) {
|
|
switch requestType {
|
|
case .unknown:
|
|
owsFailDebug("should not request unknown")
|
|
case .contacts:
|
|
Logger.info("contacts")
|
|
case .blocked:
|
|
Logger.info("blocked")
|
|
case .configuration:
|
|
Logger.info("configuration")
|
|
case .keys:
|
|
Logger.info("keys")
|
|
}
|
|
|
|
guard DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
return owsFailDebug("Unexpectedly tried to send sync request before registration.")
|
|
}
|
|
|
|
guard !DependenciesBridge.shared.tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegisteredPrimaryDevice else {
|
|
return owsFailDebug("Sync request should only be sent from a linked device")
|
|
}
|
|
|
|
guard let thread = TSContactThread.getOrCreateLocalThread(transaction: transaction) else {
|
|
return owsFailDebug("Missing thread")
|
|
}
|
|
|
|
let syncRequestMessage = OWSSyncRequestMessage(thread: thread, requestType: requestType.rawValue, transaction: transaction)
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: syncRequestMessage
|
|
)
|
|
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
private extension Notification {
|
|
var insertedThreads: [(threadUniqueId: String, sortOrder: UInt32)] {
|
|
return userInfo?[IncomingContactSyncJobQueue.Constants.insertedThreads] as! [(threadUniqueId: String, sortOrder: UInt32)]
|
|
}
|
|
}
|