574 lines
22 KiB
Swift
574 lines
22 KiB
Swift
//
|
|
// Copyright 2021 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
public import LibSignalClient
|
|
|
|
public enum BlockMode {
|
|
case remote
|
|
case restoreFromBackup
|
|
case localShouldLeaveGroups
|
|
case localShouldNotLeaveGroups
|
|
|
|
var locallyInitiated: Bool {
|
|
switch self {
|
|
case .remote, .restoreFromBackup:
|
|
return false
|
|
case .localShouldLeaveGroups, .localShouldNotLeaveGroups:
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
public class BlockingManager {
|
|
private let appReadiness: AppReadiness
|
|
private let blockedGroupStore: BlockedGroupStore
|
|
private let blockedRecipientStore: BlockedRecipientStore
|
|
|
|
private let syncQueue = SerialTaskQueue()
|
|
|
|
#if TESTABLE_BUILD
|
|
func flushSyncQueueTask() -> Task<Void, any Error> {
|
|
return self.syncQueue.enqueue {}
|
|
}
|
|
#endif
|
|
|
|
init(
|
|
appReadiness: AppReadiness,
|
|
blockedGroupStore: BlockedGroupStore,
|
|
blockedRecipientStore: BlockedRecipientStore
|
|
) {
|
|
self.appReadiness = appReadiness
|
|
self.blockedGroupStore = blockedGroupStore
|
|
self.blockedRecipientStore = blockedRecipientStore
|
|
|
|
SwiftSingletons.register(self)
|
|
appReadiness.runNowOrWhenAppWillBecomeReady {
|
|
self.observeNotifications()
|
|
}
|
|
// Once we're ready to send a message, check to see if we need to sync.
|
|
syncIfNeeded()
|
|
}
|
|
|
|
private func syncIfNeeded() {
|
|
appReadiness.runNowOrWhenMainAppDidBecomeReadyAsync {
|
|
self.syncQueue.enqueue {
|
|
await self.sendBlockListSyncMessage(force: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func didUpdate(wasLocallyInitiated: Bool, tx: DBWriteTransaction) {
|
|
if wasLocallyInitiated {
|
|
setNeedsSync(tx: tx)
|
|
} else {
|
|
clearNeedsSync(tx: tx)
|
|
}
|
|
tx.addSyncCompletion {
|
|
NotificationCenter.default.postOnMainThread(name: Self.blockListDidChange, object: nil)
|
|
}
|
|
}
|
|
|
|
private func setNeedsSync(tx: DBWriteTransaction) {
|
|
setChangeToken(fetchChangeToken(tx: tx) + 1, tx: tx)
|
|
tx.addSyncCompletion {
|
|
self.syncQueue.enqueue {
|
|
await self.sendBlockListSyncMessage(force: false)
|
|
}
|
|
}
|
|
}
|
|
|
|
private func clearNeedsSync(tx: DBWriteTransaction) {
|
|
setLastSyncedChangeToken(fetchChangeToken(tx: tx), transaction: tx)
|
|
}
|
|
|
|
public func isAddressBlocked(_ address: SignalServiceAddress, transaction: DBReadTransaction) -> Bool {
|
|
guard !address.isLocalAddress else {
|
|
return false
|
|
}
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
guard let recipientId = recipientDatabaseTable.fetchRecipient(address: address, tx: transaction)?.id else {
|
|
return false
|
|
}
|
|
return (try? blockedRecipientStore.isBlocked(recipientId: recipientId, tx: transaction)) ?? false
|
|
}
|
|
|
|
public func isGroupIdBlocked(_ groupId: Data, transaction: DBReadTransaction) -> Bool {
|
|
return (try? blockedGroupStore.isBlocked(groupId: groupId, tx: transaction)) ?? false
|
|
}
|
|
|
|
public func blockedRecipientIds(tx: DBReadTransaction) throws -> Set<SignalRecipient.RowId> {
|
|
return Set(try blockedRecipientStore.blockedRecipientIds(tx: tx))
|
|
}
|
|
|
|
public func blockedAddresses(transaction: DBReadTransaction) -> Set<SignalServiceAddress> {
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
|
|
let blockedRecipientIds = (try? self.blockedRecipientIds(tx: transaction)) ?? []
|
|
return Set(blockedRecipientIds.compactMap {
|
|
return recipientDatabaseTable.fetchRecipient(rowId: $0, tx: transaction)?.address
|
|
})
|
|
}
|
|
|
|
public func blockedGroupIds(transaction: DBReadTransaction) throws -> [Data] {
|
|
return try blockedGroupStore.blockedGroupIds(tx: transaction)
|
|
}
|
|
|
|
public func addBlockedAci(_ aci: Aci, blockMode: BlockMode, tx: DBWriteTransaction) {
|
|
self.addBlockedAddress(SignalServiceAddress(aci), blockMode: blockMode, transaction: SDSDB.shimOnlyBridge(tx))
|
|
}
|
|
|
|
public func addBlockedAddress(
|
|
_ address: SignalServiceAddress,
|
|
blockMode: BlockMode,
|
|
transaction tx: DBWriteTransaction
|
|
) {
|
|
guard !address.isLocalAddress else {
|
|
owsFailDebug("Cannot block the local address")
|
|
return
|
|
}
|
|
|
|
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
|
|
let recipient: SignalRecipient
|
|
if let serviceId = address.serviceId {
|
|
recipient = recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx)
|
|
} else if let phoneNumber = E164(address.phoneNumber) {
|
|
recipient = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx)
|
|
} else {
|
|
owsFailDebug("Invalid address: \(address).")
|
|
return
|
|
}
|
|
|
|
let isBlocked = failIfThrows { try blockedRecipientStore.isBlocked(recipientId: recipient.id!, tx: tx) }
|
|
guard !isBlocked else {
|
|
return
|
|
}
|
|
failIfThrows {
|
|
try blockedRecipientStore.setBlocked(true, recipientId: recipient.id!, tx: tx)
|
|
}
|
|
|
|
Logger.info("Added blocked address: \(address)")
|
|
|
|
if blockMode.locallyInitiated {
|
|
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedAddresses: [address])
|
|
}
|
|
|
|
// We will start dropping new stories from the blocked address;
|
|
// delete any existing ones we already have.
|
|
if let aci = address.aci {
|
|
StoryManager.deleteAllStories(forSender: aci, tx: tx)
|
|
}
|
|
let storyRecipientManager = DependenciesBridge.shared.storyRecipientManager
|
|
storyRecipientManager.removeRecipientIdFromAllPrivateStoryThreads(
|
|
recipient.id!,
|
|
shouldUpdateStorageService: true,
|
|
tx: tx
|
|
)
|
|
|
|
switch blockMode {
|
|
case .restoreFromBackup:
|
|
// If we're restoring from a Backup, avoid the side effect of
|
|
// inserting a message. One either existed in the backup or not.
|
|
break
|
|
case .remote, .localShouldLeaveGroups, .localShouldNotLeaveGroups:
|
|
// Insert an info message that we blocked this user.
|
|
let threadStore = DependenciesBridge.shared.threadStore
|
|
let interactionStore = DependenciesBridge.shared.interactionStore
|
|
if let contactThread = threadStore.fetchContactThread(recipient: recipient, tx: tx) {
|
|
interactionStore.insertInteraction(
|
|
TSInfoMessage(thread: contactThread, messageType: .blockedOtherUser),
|
|
tx: tx
|
|
)
|
|
}
|
|
}
|
|
|
|
didUpdate(wasLocallyInitiated: blockMode.locallyInitiated, tx: tx)
|
|
}
|
|
|
|
public func removeBlockedAddress(
|
|
_ address: SignalServiceAddress,
|
|
wasLocallyInitiated: Bool,
|
|
transaction tx: DBWriteTransaction
|
|
) {
|
|
guard address.isValid else {
|
|
owsFailDebug("Invalid address: \(address).")
|
|
return
|
|
}
|
|
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
guard let recipient = recipientDatabaseTable.fetchRecipient(address: address, tx: tx) else {
|
|
// No need to unblock non-existent recipients. They can't possibly be blocked.
|
|
return
|
|
}
|
|
|
|
let isBlocked = failIfThrows { try blockedRecipientStore.isBlocked(recipientId: recipient.id!, tx: tx) }
|
|
guard isBlocked else {
|
|
return
|
|
}
|
|
failIfThrows {
|
|
try blockedRecipientStore.setBlocked(false, recipientId: recipient.id!, tx: tx)
|
|
}
|
|
|
|
Logger.info("Removed blocked address: \(address)")
|
|
|
|
if wasLocallyInitiated {
|
|
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedAddresses: [address])
|
|
}
|
|
|
|
// Insert an info message that we unblocked this user.
|
|
let threadStore = DependenciesBridge.shared.threadStore
|
|
let interactionStore = DependenciesBridge.shared.interactionStore
|
|
if let contactThread = threadStore.fetchContactThread(recipient: recipient, tx: tx) {
|
|
interactionStore.insertInteraction(
|
|
TSInfoMessage(thread: contactThread, messageType: .unblockedOtherUser),
|
|
tx: tx
|
|
)
|
|
}
|
|
|
|
didUpdate(wasLocallyInitiated: wasLocallyInitiated, tx: tx)
|
|
}
|
|
|
|
public func addBlockedGroupId(_ groupId: Data, blockMode: BlockMode, transaction: DBWriteTransaction) {
|
|
guard GroupManager.isValidGroupIdOfAnyKind(groupId) else {
|
|
owsFailDebug("Can't block invalid groupId: \(groupId.toHex())")
|
|
return
|
|
}
|
|
|
|
let isBlocked = failIfThrows { try blockedGroupStore.isBlocked(groupId: groupId, tx: transaction) }
|
|
guard !isBlocked else {
|
|
return
|
|
}
|
|
failIfThrows {
|
|
try blockedGroupStore.setBlocked(true, groupId: groupId, tx: transaction)
|
|
}
|
|
|
|
Logger.info("Added blocked groupId: \(groupId.toHex())")
|
|
|
|
let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction)
|
|
owsAssertDebug(groupThread != nil, "Must have TSGroupThread in order to block it.")
|
|
|
|
if blockMode.locallyInitiated, let masterKey = try? (groupThread?.groupModel as? TSGroupModelV2)?.masterKey() {
|
|
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedGroupV2MasterKeys: [masterKey.serialize().asData])
|
|
}
|
|
|
|
if let groupThread {
|
|
// Quit the group if we're a member.
|
|
if
|
|
blockMode == .localShouldLeaveGroups,
|
|
groupThread.groupModel.groupMembership.isLocalUserMemberOfAnyKind,
|
|
groupThread.isLocalUserMemberOfAnyKind
|
|
{
|
|
GroupManager.leaveGroupOrDeclineInviteAsyncWithoutUI(
|
|
groupThread: groupThread,
|
|
tx: transaction
|
|
)
|
|
}
|
|
|
|
switch blockMode {
|
|
case .restoreFromBackup:
|
|
// If we're restoring from a Backup, avoid the side effect of
|
|
// inserting a message. One either existed in the backup or not.
|
|
break
|
|
case .remote, .localShouldLeaveGroups, .localShouldNotLeaveGroups:
|
|
// Insert an info message that we blocked this group.
|
|
DependenciesBridge.shared.interactionStore.insertInteraction(
|
|
TSInfoMessage(thread: groupThread, messageType: .blockedGroup),
|
|
tx: transaction
|
|
)
|
|
}
|
|
}
|
|
|
|
didUpdate(wasLocallyInitiated: blockMode.locallyInitiated, tx: transaction)
|
|
}
|
|
|
|
public func removeBlockedGroup(groupId: Data, wasLocallyInitiated: Bool, transaction: DBWriteTransaction) {
|
|
let isBlocked = failIfThrows { try blockedGroupStore.isBlocked(groupId: groupId, tx: transaction) }
|
|
guard isBlocked else {
|
|
return
|
|
}
|
|
failIfThrows {
|
|
try blockedGroupStore.setBlocked(false, groupId: groupId, tx: transaction)
|
|
}
|
|
|
|
Logger.info("Removed blocked groupId: \(groupId.toHex())")
|
|
|
|
let groupThread = TSGroupThread.fetch(groupId: groupId, transaction: transaction)
|
|
|
|
if wasLocallyInitiated {
|
|
let masterKey = { () -> GroupMasterKey? in
|
|
if let groupThread {
|
|
return try? (groupThread.groupModel as? TSGroupModelV2)?.masterKey()
|
|
}
|
|
if GroupManager.isV2GroupId(groupId) {
|
|
// TODO: Check groups we're still trying to restore from Storage Service.
|
|
}
|
|
return nil
|
|
}()
|
|
if let masterKey {
|
|
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedGroupV2MasterKeys: [masterKey.serialize().asData])
|
|
}
|
|
}
|
|
|
|
if let groupThread {
|
|
// Insert an info message that we unblocked.
|
|
DependenciesBridge.shared.interactionStore.insertInteraction(
|
|
TSInfoMessage(thread: groupThread, messageType: .unblockedGroup),
|
|
tx: transaction
|
|
)
|
|
|
|
// Refresh unblocked group.
|
|
transaction.addSyncCompletion {
|
|
SSKEnvironment.shared.groupV2UpdatesRef.refreshGroupUpThroughCurrentRevision(groupThread: groupThread, throttle: false)
|
|
}
|
|
}
|
|
|
|
didUpdate(wasLocallyInitiated: wasLocallyInitiated, tx: transaction)
|
|
}
|
|
|
|
// MARK: Other convenience access
|
|
|
|
public func isThreadBlocked(_ thread: TSThread, transaction: DBReadTransaction) -> Bool {
|
|
if let contactThread = thread as? TSContactThread {
|
|
return isAddressBlocked(contactThread.contactAddress, transaction: transaction)
|
|
} else if let groupThread = thread as? TSGroupThread {
|
|
return isGroupIdBlocked(groupThread.groupModel.groupId, transaction: transaction)
|
|
} else if thread is TSPrivateStoryThread {
|
|
return false
|
|
} else {
|
|
owsFailDebug("Invalid thread: \(type(of: thread))")
|
|
return false
|
|
}
|
|
}
|
|
|
|
public func addBlockedThread(_ thread: TSThread, blockMode: BlockMode, transaction: DBWriteTransaction) {
|
|
if let contactThread = thread as? TSContactThread {
|
|
addBlockedAddress(contactThread.contactAddress, blockMode: blockMode, transaction: transaction)
|
|
} else if let groupThread = thread as? TSGroupThread {
|
|
addBlockedGroupId(groupThread.groupId, blockMode: blockMode, transaction: transaction)
|
|
} else {
|
|
owsFailDebug("Invalid thread: \(type(of: thread))")
|
|
}
|
|
}
|
|
|
|
public func removeBlockedThread(_ thread: TSThread, wasLocallyInitiated: Bool, transaction: DBWriteTransaction) {
|
|
if let contactThread = thread as? TSContactThread {
|
|
removeBlockedAddress(contactThread.contactAddress, wasLocallyInitiated: wasLocallyInitiated, transaction: transaction)
|
|
} else if let groupThread = thread as? TSGroupThread {
|
|
removeBlockedGroup(groupId: groupThread.groupId, wasLocallyInitiated: wasLocallyInitiated, transaction: transaction)
|
|
} else {
|
|
owsFailDebug("Invalid thread: \(type(of: thread))")
|
|
}
|
|
}
|
|
|
|
// MARK: - Syncing
|
|
|
|
public func processIncomingSync(
|
|
blockedPhoneNumbers: Set<String>,
|
|
blockedAcis: Set<Aci>,
|
|
blockedGroupIds: Set<Data>,
|
|
tx transaction: DBWriteTransaction
|
|
) {
|
|
Logger.info("")
|
|
transaction.addSyncCompletion {
|
|
NotificationCenter.default.postOnMainThread(name: Self.blockedSyncDidComplete, object: nil)
|
|
}
|
|
|
|
var didChange = false
|
|
|
|
failIfThrows {
|
|
let oldBlockedGroupIds = Set(try blockedGroupStore.blockedGroupIds(tx: transaction))
|
|
let newBlockedGroupIds = blockedGroupIds
|
|
try oldBlockedGroupIds.subtracting(newBlockedGroupIds).forEach {
|
|
didChange = true
|
|
try blockedGroupStore.setBlocked(false, groupId: $0, tx: transaction)
|
|
}
|
|
try newBlockedGroupIds.subtracting(oldBlockedGroupIds).forEach {
|
|
didChange = true
|
|
try blockedGroupStore.setBlocked(true, groupId: $0, tx: transaction)
|
|
}
|
|
}
|
|
|
|
var blockedRecipientIds = Set<SignalRecipient.RowId>()
|
|
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
|
|
for blockedAci in blockedAcis {
|
|
blockedRecipientIds.insert(recipientFetcher.fetchOrCreate(serviceId: blockedAci, tx: transaction).id!)
|
|
}
|
|
for blockedPhoneNumber in blockedPhoneNumbers.compactMap(E164.init) {
|
|
blockedRecipientIds.insert(recipientFetcher.fetchOrCreate(phoneNumber: blockedPhoneNumber, tx: transaction).id!)
|
|
}
|
|
|
|
failIfThrows {
|
|
let oldBlockedRecipientIds = Set(try blockedRecipientStore.blockedRecipientIds(tx: transaction))
|
|
let newBlockedRecipientIds = blockedRecipientIds
|
|
try oldBlockedRecipientIds.subtracting(newBlockedRecipientIds).forEach {
|
|
didChange = true
|
|
try blockedRecipientStore.setBlocked(false, recipientId: $0, tx: transaction)
|
|
}
|
|
try newBlockedRecipientIds.subtracting(oldBlockedRecipientIds).forEach {
|
|
didChange = true
|
|
try blockedRecipientStore.setBlocked(true, recipientId: $0, tx: transaction)
|
|
}
|
|
}
|
|
|
|
if didChange {
|
|
didUpdate(wasLocallyInitiated: false, tx: transaction)
|
|
}
|
|
}
|
|
|
|
public func syncBlockList() -> Task<Void, any Error> {
|
|
return self.syncQueue.enqueue {
|
|
await self.sendBlockListSyncMessage(force: true)
|
|
}
|
|
}
|
|
|
|
private func sendBlockListSyncMessage(force: Bool) async {
|
|
do {
|
|
try await _sendBlockListSyncMessage(force: force)
|
|
} catch {
|
|
Logger.warn("\(error)")
|
|
}
|
|
}
|
|
|
|
private func _sendBlockListSyncMessage(force: Bool) async throws {
|
|
let tsAccountManager = DependenciesBridge.shared.tsAccountManager
|
|
guard tsAccountManager.registrationStateWithMaybeSneakyTransaction.isRegistered else {
|
|
throw OWSGenericError("Not registered.")
|
|
}
|
|
|
|
let sendResult = try await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { (tx) -> (sendPromise: Promise<Void>, changeToken: UInt64)? in
|
|
// If we're not forcing a sync, then we only sync if our last synced token is stale
|
|
// and we're not in the NSE. We'll leaving syncing to the main app.
|
|
let changeToken = fetchChangeToken(tx: tx)
|
|
if !force {
|
|
guard shouldSync(changeToken: changeToken, tx: tx) else {
|
|
return nil
|
|
}
|
|
guard !CurrentAppContext().isNSE else {
|
|
throw OWSGenericError("Can't send in the NSE.")
|
|
}
|
|
}
|
|
|
|
guard let localThread = TSContactThread.getOrCreateLocalThread(transaction: tx) else {
|
|
throw OWSAssertionError("Missing thread.")
|
|
}
|
|
|
|
let recipientDatabaseTable = DependenciesBridge.shared.recipientDatabaseTable
|
|
let blockedRecipients = try blockedRecipientStore.blockedRecipientIds(tx: tx).compactMap {
|
|
return recipientDatabaseTable.fetchRecipient(rowId: $0, tx: tx)
|
|
}
|
|
|
|
let blockedGroupIds = try blockedGroupStore.blockedGroupIds(tx: tx)
|
|
|
|
let message = OWSBlockedPhoneNumbersMessage(
|
|
localThread: localThread,
|
|
phoneNumbers: blockedRecipients.compactMap { $0.phoneNumber?.stringValue },
|
|
aciStrings: blockedRecipients.compactMap { $0.aci?.serviceIdString },
|
|
groupIds: Array(blockedGroupIds),
|
|
transaction: tx
|
|
)
|
|
|
|
let preparedMessage = PreparedOutgoingMessage.preprepared(
|
|
transientMessageWithoutAttachments: message
|
|
)
|
|
|
|
let sendPromise = SSKEnvironment.shared.messageSenderJobQueueRef.add(
|
|
.promise,
|
|
message: preparedMessage,
|
|
transaction: tx
|
|
)
|
|
|
|
return (sendPromise, changeToken)
|
|
}
|
|
|
|
guard let sendResult else {
|
|
return
|
|
}
|
|
|
|
try await sendResult.sendPromise.awaitable()
|
|
|
|
// Record the last block list which we successfully synced..
|
|
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
|
|
setLastSyncedChangeToken(sendResult.changeToken, transaction: transaction)
|
|
}
|
|
}
|
|
|
|
private func shouldSync(changeToken: UInt64, tx: DBReadTransaction) -> Bool {
|
|
// If we've ever sync'd with this mechanism, we need only sync again if the
|
|
// token has changed.
|
|
if let lastSyncedChangeToken = fetchLastSyncedChangeToken(tx: tx) {
|
|
return changeToken != lastSyncedChangeToken
|
|
}
|
|
// Otherwise, if we've made any change, we must sync.
|
|
if changeToken > Constants.initialChangeToken {
|
|
return true
|
|
}
|
|
// If we don't have a last synced change token, we can use the existence of
|
|
// one of our old KVS keys as a hint that we may need to sync. If they
|
|
// don't exist this is probably a fresh install and we don't need to sync.
|
|
return PersistenceKey.Legacy.allCases.contains { key in
|
|
return keyValueStore.hasValue(key.rawValue, transaction: tx)
|
|
}
|
|
}
|
|
|
|
// MARK: - Notifications
|
|
|
|
public static let blockListDidChange = Notification.Name("blockListDidChange")
|
|
public static let blockedSyncDidComplete = Notification.Name("blockedSyncDidComplete")
|
|
|
|
fileprivate func observeNotifications() {
|
|
AssertIsOnMainThread()
|
|
|
|
NotificationCenter.default.addObserver(
|
|
self,
|
|
selector: #selector(applicationDidBecomeActive),
|
|
name: .OWSApplicationDidBecomeActive,
|
|
object: nil
|
|
)
|
|
}
|
|
|
|
@objc
|
|
private func applicationDidBecomeActive() {
|
|
syncIfNeeded()
|
|
}
|
|
|
|
// MARK: - Persistence
|
|
|
|
private enum Constants {
|
|
static let initialChangeToken: UInt64 = 1
|
|
}
|
|
|
|
private let keyValueStore = KeyValueStore(collection: "kOWSBlockingManager_BlockedPhoneNumbersCollection")
|
|
|
|
enum PersistenceKey: String {
|
|
case changeTokenKey = "kOWSBlockingManager_ChangeTokenKey"
|
|
case lastSyncedChangeTokenKey = "kOWSBlockingManager_LastSyncedChangeTokenKey"
|
|
|
|
// No longer in use
|
|
enum Legacy: String, CaseIterable {
|
|
case syncedBlockedPhoneNumbersKey = "kOWSBlockingManager_SyncedBlockedPhoneNumbersKey"
|
|
case syncedBlockedUUIDsKey = "kOWSBlockingManager_SyncedBlockedUUIDsKey"
|
|
case syncedBlockedGroupIdsKey = "kOWSBlockingManager_SyncedBlockedGroupIdsKey"
|
|
}
|
|
}
|
|
|
|
func fetchChangeToken(tx: DBReadTransaction) -> UInt64 {
|
|
keyValueStore.getUInt64(PersistenceKey.changeTokenKey.rawValue, defaultValue: Constants.initialChangeToken, transaction: tx)
|
|
}
|
|
|
|
func setChangeToken(_ newValue: UInt64, tx: DBWriteTransaction) {
|
|
keyValueStore.setUInt64(newValue, key: PersistenceKey.changeTokenKey.rawValue, transaction: tx)
|
|
}
|
|
|
|
func fetchLastSyncedChangeToken(tx: DBReadTransaction) -> UInt64? {
|
|
keyValueStore.getUInt64(PersistenceKey.lastSyncedChangeTokenKey.rawValue, transaction: tx)
|
|
}
|
|
|
|
func setLastSyncedChangeToken(_ newValue: UInt64, transaction writeTx: DBWriteTransaction) {
|
|
keyValueStore.setUInt64(newValue, key: PersistenceKey.lastSyncedChangeTokenKey.rawValue, transaction: writeTx)
|
|
}
|
|
}
|