Migrate whitelist to SignalRecipient.status

This commit is contained in:
Max Radermacher 2026-01-06 13:34:35 -06:00 committed by GitHub
parent b9dee66b57
commit a198b817be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
32 changed files with 545 additions and 598 deletions

View File

@ -778,7 +778,6 @@
50F54C2D2CE3FF7C005765EA /* GroupSendEndorsementStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F54C2C2CE3FF7C005765EA /* GroupSendEndorsementStore.swift */; };
50F54C2F2CE3FF9E005765EA /* GroupSendEndorsementRecord.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F54C2E2CE3FF9E005765EA /* GroupSendEndorsementRecord.swift */; };
50F75E312AD9F18F0032530F /* RecipientDatabaseTableTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F75E302AD9F18F0032530F /* RecipientDatabaseTableTest.swift */; };
50F77AA02AAA7B8A00FB70C5 /* ProfileWhitelistMerger.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F77A9F2AAA7B8A00FB70C5 /* ProfileWhitelistMerger.swift */; };
50F86FC42AFEFEC20045F58B /* TimeGatedBatch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F86FC32AFEFEC20045F58B /* TimeGatedBatch.swift */; };
50F946102AD768AF002EF293 /* MockIdentityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 50F9460F2AD768AF002EF293 /* MockIdentityManager.swift */; };
5AA002E62CA24566002D1CC2 /* SessionStoreTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5AA002E52CA2455F002D1CC2 /* SessionStoreTest.swift */; };
@ -4927,7 +4926,6 @@
50F54C2C2CE3FF7C005765EA /* GroupSendEndorsementStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSendEndorsementStore.swift; sourceTree = "<group>"; };
50F54C2E2CE3FF9E005765EA /* GroupSendEndorsementRecord.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupSendEndorsementRecord.swift; sourceTree = "<group>"; };
50F75E302AD9F18F0032530F /* RecipientDatabaseTableTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientDatabaseTableTest.swift; sourceTree = "<group>"; };
50F77A9F2AAA7B8A00FB70C5 /* ProfileWhitelistMerger.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileWhitelistMerger.swift; sourceTree = "<group>"; };
50F86FC32AFEFEC20045F58B /* TimeGatedBatch.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimeGatedBatch.swift; sourceTree = "<group>"; };
50F9460F2AD768AF002EF293 /* MockIdentityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockIdentityManager.swift; sourceTree = "<group>"; };
50F96F3A28ECBC3200541EED /* ms */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = ms; path = translations/ms.lproj/InfoPlist.strings; sourceTree = "<group>"; };
@ -14690,7 +14688,6 @@
F9C5C9F0289453B100548EEE /* PhoneNumberUtil.swift */,
50C98A402B69D9340065BD2E /* PhoneNumberVisibilityFetcher.swift */,
B95BBAC12BB36025009EFB4A /* ProfileName.swift */,
50F77A9F2AAA7B8A00FB70C5 /* ProfileWhitelistMerger.swift */,
506695E429C29C2F00B6D8D0 /* RecipientDatabaseTable.swift */,
50AA3EC229F1C4B900EC50A3 /* RecipientFetcher.swift */,
506695E229C29BCE00B6D8D0 /* RecipientMerger.swift */,
@ -19212,7 +19209,6 @@
503BD2892B44D666009624FC /* ProfileManager.swift in Sources */,
72901D2D2C9B129B000406DC /* ProfileManagerProtocol.swift in Sources */,
B93296692BBB3FF200B8BD39 /* ProfileName.swift in Sources */,
50F77AA02AAA7B8A00FB70C5 /* ProfileWhitelistMerger.swift in Sources */,
668A01322C2B6088007B8808 /* Promise.swift in Sources */,
72901D2F2C9B1918000406DC /* ProtoUtils.swift in Sources */,
F9C5CC9D289453B300548EEE /* Provisioning.pb.swift in Sources */,

View File

@ -253,23 +253,23 @@ private extension ConversationViewController {
return
}
}
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
let blockingManager = SSKEnvironment.shared.blockingManagerRef
let hidingManager = DependenciesBridge.shared.recipientHidingManager
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
func unblockThreadIfNeeded(transaction: DBWriteTransaction) {
if unblockThread {
SSKEnvironment.shared.blockingManagerRef.removeBlockedThread(
blockingManager.removeBlockedThread(
thread,
wasLocallyInitiated: true,
transaction: transaction,
)
}
}
if unhideRecipient, let thread = thread as? TSContactThread {
DependenciesBridge.shared.recipientHidingManager.removeHiddenRecipient(
thread.contactAddress,
wasLocallyInitiated: true,
tx: transaction,
)
}
func acceptMessageRequestIfNeeded(transaction: DBWriteTransaction) {
/// If we're not in "unblock" mode, we should take "accept message
/// request" actions. (Bleh.)
if !unblockThread {
@ -290,18 +290,36 @@ private extension ConversationViewController {
transaction: transaction,
)
}
}
// Whitelist the thread
SSKEnvironment.shared.profileManagerRef.addThread(
toProfileWhitelist: thread,
userProfileWriter: .localUser,
transaction: transaction,
)
await SSKEnvironment.shared.databaseStorageRef.awaitableWrite { transaction in
switch thread {
case let thread as TSGroupThread:
unblockThreadIfNeeded(transaction: transaction)
acceptMessageRequestIfNeeded(transaction: transaction)
profileManager.addGroupId(
toProfileWhitelist: thread.groupModel.groupId,
userProfileWriter: .localUser,
transaction: transaction,
)
if !thread.isGroupThread {
case let thread as TSContactThread:
unblockThreadIfNeeded(transaction: transaction)
// Might be nil if thread.contactAddress isn't valid.
var recipient = recipientFetcher.fetchOrCreate(address: thread.contactAddress, tx: transaction)
if var innerRecipient = recipient {
if unhideRecipient, !thread.contactAddress.isLocalAddress {
hidingManager.removeHiddenRecipient(&innerRecipient, wasLocallyInitiated: true, tx: transaction)
}
recipient = innerRecipient
}
acceptMessageRequestIfNeeded(transaction: transaction)
if var innerRecipient = recipient {
profileManager.addRecipientToProfileWhitelist(&innerRecipient, userProfileWriter: .localUser, tx: transaction)
recipient = innerRecipient
}
// If this is a contact thread, we should give the
// now-unblocked contact our profile key.
let profileManager = SSKEnvironment.shared.profileManagerRef
let profileKeyMessage = OWSProfileKeyMessage(
thread: thread,
profileKey: profileManager.localProfileKey(tx: transaction)!.serialize(),
@ -311,6 +329,9 @@ private extension ConversationViewController {
transientMessageWithoutAttachments: profileKeyMessage,
)
SSKEnvironment.shared.messageSenderJobQueueRef.add(message: preparedMessage, transaction: transaction)
default:
owsFailDebug("can't accept message request for \(type(of: thread))")
}
NotificationCenter.default.post(name: ChatListViewController.clearSearch, object: nil)

View File

@ -15,6 +15,7 @@ public class RegistrationStateChangeManagerImpl: RegistrationStateChangeManager
private let backupCDNCredentialStore: BackupCDNCredentialStore
private let backupSubscriptionManager: BackupSubscriptionManager
private let backupTestFlightEntitlementManager: BackupTestFlightEntitlementManager
private let blockedRecipientStore: BlockedRecipientStore
private var chatConnectionManager: any ChatConnectionManager {
// TODO: Fix circular dependency.
return DependenciesBridge.shared.chatConnectionManager
@ -42,6 +43,7 @@ public class RegistrationStateChangeManagerImpl: RegistrationStateChangeManager
backupCDNCredentialStore: BackupCDNCredentialStore,
backupSubscriptionManager: BackupSubscriptionManager,
backupTestFlightEntitlementManager: BackupTestFlightEntitlementManager,
blockedRecipientStore: BlockedRecipientStore,
cron: Cron,
db: DB,
dmConfigurationStore: DisappearingMessagesConfigurationStore,
@ -63,6 +65,7 @@ public class RegistrationStateChangeManagerImpl: RegistrationStateChangeManager
self.backupCDNCredentialStore = backupCDNCredentialStore
self.backupSubscriptionManager = backupSubscriptionManager
self.backupTestFlightEntitlementManager = backupTestFlightEntitlementManager
self.blockedRecipientStore = blockedRecipientStore
self.cron = cron
self.db = db
self.dmConfigurationStore = dmConfigurationStore
@ -349,6 +352,9 @@ public class RegistrationStateChangeManagerImpl: RegistrationStateChangeManager
shouldUpdateStorageService: false,
tx: tx,
)
// Always make sure we haven't blocked ourselves. (It's logically
// impossible to do so, so we set the bit in the database directly.)
blockedRecipientStore.setBlocked(false, recipientId: recipient.id, tx: tx)
}
// MARK: Notifications

View File

@ -204,7 +204,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit
visibility: { () -> BackupProto_Contact.Visibility in
guard
let hiddenRecipient = self.recipientHidingManager.fetchHiddenRecipient(
signalRecipient: recipient,
recipientId: recipient.id,
tx: context.tx,
)
else {
@ -669,7 +669,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit
}
let recipientProto = recipient
let recipient: SignalRecipient
var recipient: SignalRecipient
do throws(GRDB.DatabaseError) {
recipient = try SignalRecipient.insertRecord(
aci: backupContactAddress.aci,
@ -758,7 +758,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit
if contactProto.profileSharing {
// Add to the whitelist.
profileManager.addToWhitelist(recipient.address, tx: context.tx)
profileManager.addRecipientToProfileWhitelist(&recipient, tx: context.tx)
}
if contactProto.blocked {
@ -768,7 +768,7 @@ public class BackupArchiveContactRecipientArchiver: BackupArchiveProtoStreamWrit
do {
func addHiddenRecipient(isHiddenInKnownMessageRequestState: Bool) throws {
try recipientHidingManager.addHiddenRecipient(
recipient,
&recipient,
inKnownMessageRequestState: isHiddenInKnownMessageRequestState,
wasLocallyInitiated: false,
tx: context.tx,

View File

@ -95,7 +95,7 @@ public class BackupArchiveLocalRecipientArchiver: BackupArchiveProtoStreamWriter
) -> BackupArchive.RestoreLocalRecipientResult {
context[recipient.recipientId] = .localAddress
let localSignalRecipient: SignalRecipient
var localSignalRecipient: SignalRecipient
do throws(GRDB.DatabaseError) {
localSignalRecipient = try SignalRecipient.insertRecord(
aci: context.localIdentifiers.aci,
@ -125,8 +125,8 @@ public class BackupArchiveLocalRecipientArchiver: BackupArchiveProtoStreamWriter
}
}
profileManager.addToWhitelist(
context.localIdentifiers.aciAddress,
profileManager.addRecipientToProfileWhitelist(
&localSignalRecipient,
tx: context.tx,
)

View File

@ -249,7 +249,7 @@ public protocol _MessageBackup_ProfileManagerShim {
func isGroupId(inProfileWhitelist groupId: Data, tx: DBReadTransaction) -> Bool
func addToWhitelist(_ address: SignalServiceAddress, tx: DBWriteTransaction)
func addRecipientToProfileWhitelist(_ recipient: inout SignalRecipient, tx: DBWriteTransaction)
func addToWhitelist(_ thread: TSGroupThread, tx: DBWriteTransaction)
@ -301,17 +301,17 @@ public class _MessageBackup_ProfileManagerWrapper: _MessageBackup_ProfileManager
profileManager.isGroupId(inProfileWhitelist: groupId, transaction: tx)
}
public func addToWhitelist(_ address: SignalServiceAddress, tx: DBWriteTransaction) {
profileManager.addUser(
toProfileWhitelist: address,
public func addRecipientToProfileWhitelist(_ recipient: inout SignalRecipient, tx: DBWriteTransaction) {
profileManager.addRecipientToProfileWhitelist(
&recipient,
userProfileWriter: .backupRestore,
transaction: tx,
tx: tx,
)
}
public func addToWhitelist(_ thread: TSGroupThread, tx: DBWriteTransaction) {
profileManager.addThread(
toProfileWhitelist: thread,
profileManager.addGroupId(
toProfileWhitelist: thread.groupModel.groupId,
userProfileWriter: .backupRestore,
transaction: tx,
)

View File

@ -925,13 +925,18 @@ extension OWSContactsManager: ContactManager {
Logger.info("Updated \(signalAccountChanges.count) SignalAccounts; now have \(newSignalAccountsMap.count) total")
}
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
// Add system contacts to the profile whitelist immediately so that they do
// not see the "message request" UI.
SSKEnvironment.shared.profileManagerRef.addUsers(
toProfileWhitelist: newSignalAccountsMap.values.map { $0.recipientAddress },
userProfileWriter: .systemContactsFetch,
transaction: tx,
)
for phoneNumber in newSignalAccountsMap.keys {
guard let phoneNumberObj = E164(phoneNumber) else {
continue
}
var recipient = recipientFetcher.fetchOrCreate(phoneNumber: phoneNumberObj, tx: tx)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .systemContactsFetch, tx: tx)
}
}
// Once we've persisted new SignalAccount state, we should let
@ -1185,7 +1190,7 @@ extension OWSContactsManager: ContactManager {
) throws {
let recipientHidingManager = DependenciesBridge.shared.recipientHidingManager
let phoneNumbers = Set(addressBookPhoneNumbers.lazy.map { $0.rawValue.stringValue })
for hiddenRecipient in recipientHidingManager.hiddenRecipients(tx: tx) {
for var hiddenRecipient in recipientHidingManager.hiddenRecipients(tx: tx) {
guard let phoneNumber = hiddenRecipient.phoneNumber else {
continue // We can't unhide because of the address book w/o a phone number.
}
@ -1195,7 +1200,7 @@ extension OWSContactsManager: ContactManager {
guard phoneNumbers.contains(phoneNumber.stringValue) else {
continue // Not in the address book -- no unhiding.
}
recipientHidingManager.removeHiddenRecipient(hiddenRecipient, wasLocallyInitiated: true, tx: tx)
recipientHidingManager.removeHiddenRecipient(&hiddenRecipient, wasLocallyInitiated: true, tx: tx)
}
}

View File

@ -309,27 +309,38 @@ extension OWSSyncManager: SyncManagerProtocol, SyncManagerProtocolSwift {
return owsFailDebug("message request response couldn't find thread")
}
let blockingManager = SSKEnvironment.shared.blockingManagerRef
let hidingManager = DependenciesBridge.shared.recipientHidingManager
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
switch syncMessage.type {
case .accept:
SSKEnvironment.shared.blockingManagerRef.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,
blockingManager.removeBlockedThread(thread, wasLocallyInitiated: false, transaction: transaction)
switch thread {
case let thread as TSGroupThread:
// TODO: Fix userProfileWriter.
profileManager.addGroupId(
toProfileWhitelist: thread.groupModel.groupId,
userProfileWriter: .localUser,
transaction: transaction,
)
case let 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.
if var recipient = recipientFetcher.fetchOrCreate(address: thread.contactAddress, tx: transaction) {
hidingManager.removeHiddenRecipient(&recipient, wasLocallyInitiated: false, tx: transaction)
// TODO: Fix userProfileWriter.
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: transaction)
}
default:
owsFailDebug("can't accept messages request for \(type(of: thread))")
}
SSKEnvironment.shared.profileManagerRef.addThread(
toProfileWhitelist: thread,
userProfileWriter: .localUser,
transaction: transaction,
)
case .delete:
DependenciesBridge.shared.threadSoftDeleteManager.softDelete(
threads: [thread],

View File

@ -1,25 +0,0 @@
//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
class ProfileWhitelistMerger: RecipientMergeObserver {
private let profileManager: ProfileManager
init(profileManager: ProfileManager) {
self.profileManager = profileManager
}
func willBreakAssociation(for recipient: SignalRecipient, mightReplaceNonnilPhoneNumber: Bool, tx: DBWriteTransaction) {
profileManager.normalizeRecipientInProfileWhitelist(recipient, tx: tx)
}
func didLearnAssociation(mergedRecipient: MergedRecipient, tx: DBWriteTransaction) {
if mergedRecipient.isLocalRecipient {
return
}
profileManager.normalizeRecipientInProfileWhitelist(mergedRecipient.newRecipient, tx: tx)
}
}

View File

@ -4,6 +4,7 @@
//
import Foundation
import GRDB
public import LibSignalClient
public struct RecipientDatabaseTable {
@ -122,6 +123,13 @@ public struct RecipientDatabaseTable {
}
}
public func fetchWhitelistedRecipients(tx: DBReadTransaction) -> [SignalRecipient] {
let fetchRequest = SignalRecipient.filter(
Column(SignalRecipient.CodingKeys.status.rawValue) == SignalRecipient.Status.whitelisted.rawValue,
)
return failIfThrows { try fetchRequest.fetchAll(tx.database) }
}
public func fetchAllPhoneNumbers(tx: DBReadTransaction) -> [String: Bool] {
var result = [String: Bool]()
enumerateAll(tx: tx) { signalRecipient in

View File

@ -43,4 +43,14 @@ public struct RecipientFetcher {
searchableNameIndexer.insert(result, tx: tx)
return result
}
public func fetchOrCreate(address: SignalServiceAddress, tx: DBWriteTransaction) -> SignalRecipient? {
if let serviceId = address.serviceId {
return fetchOrCreate(serviceId: serviceId, tx: tx)
}
if let phoneNumber = address.e164 {
return fetchOrCreate(phoneNumber: phoneNumber, tx: tx)
}
return nil
}
}

View File

@ -165,7 +165,6 @@ class RecipientMergerImpl: RecipientMerger {
signalServiceAddressCache,
AuthorMergeObserver(authorMergeHelper: authorMergeHelper),
SignalAccountMergeObserver(),
ProfileWhitelistMerger(profileManager: profileManager),
UserProfileMerger(userProfileStore: userProfileStore),
],
threadMerger: ThreadMerger(

View File

@ -34,6 +34,11 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
public var isDiscoverable: Bool
}
public enum Status: Int64, Codable {
case unspecified = 0
case whitelisted = 1
}
public typealias RowId = Int64
public let id: RowId
@ -53,6 +58,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
public var phoneNumber: PhoneNumber?
public fileprivate(set) var deviceIds: [DeviceId]
public fileprivate(set) var unregisteredAtTimestamp: UInt64?
public var status: Status
public var aci: Aci? {
get { Aci.parseFrom(aciString: aciString) }
@ -84,6 +90,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
pni: Pni? = nil,
deviceIds: [DeviceId] = [],
unregisteredAtTimestamp: UInt64?? = nil,
status: Status = .unspecified,
tx: DBWriteTransaction,
) throws(GRDB.DatabaseError) -> Self {
do {
@ -97,8 +104,9 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
\(signalRecipientColumn: .phoneNumber),
\(signalRecipientColumn: .pni),
\(signalRecipientColumn: .deviceIds),
\(signalRecipientColumn: .unregisteredAtTimestamp)
) VALUES (?, ?, ?, ?, ?, ?, ?) RETURNING *
\(signalRecipientColumn: .unregisteredAtTimestamp),
\(signalRecipientColumn: .status)
) VALUES (?, ?, ?, ?, ?, ?, ?, ?) RETURNING *
""",
arguments: [
SDSRecordType.signalRecipient.rawValue,
@ -108,6 +116,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
pni?.serviceIdUppercaseString,
Data(deviceIds.map(\.uint8Value)),
unregisteredAtTimestamp ?? (deviceIds.isEmpty ? Constants.distantPastUnregisteredTimestamp : nil),
status.rawValue,
],
)!
} catch {
@ -125,6 +134,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
case deviceIds = "devices"
case unregisteredAtTimestamp
case isPhoneNumberDiscoverable
case status
}
public init(from decoder: Decoder) throws {
@ -151,6 +161,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
let encodedDeviceIds = try container.decode(Data.self, forKey: .deviceIds)
deviceIds = encodedDeviceIds.compactMap(DeviceId.init(validating:))
unregisteredAtTimestamp = try container.decodeIfPresent(UInt64.self, forKey: .unregisteredAtTimestamp)
status = try container.decode(Status.self, forKey: .status)
}
public func encode(to encoder: Encoder) throws {
@ -164,6 +175,7 @@ public struct SignalRecipient: FetchableRecord, PersistableRecord, Codable {
try container.encodeIfPresent(phoneNumber?.isDiscoverable, forKey: .isPhoneNumberDiscoverable)
try container.encode(Data(deviceIds.map(\.uint8Value)), forKey: .deviceIds)
try container.encodeIfPresent(unregisteredAtTimestamp, forKey: .unregisteredAtTimestamp)
try container.encode(status, forKey: .status)
}
// MARK: - Fetching

View File

@ -1124,6 +1124,7 @@ extension AppSetup.GlobalsContinuation {
backupCDNCredentialStore: backupCDNCredentialStore,
backupSubscriptionManager: backupSubscriptionManager,
backupTestFlightEntitlementManager: backupTestFlightEntitlementManager,
blockedRecipientStore: blockedRecipientStore,
cron: cron,
db: db,
dmConfigurationStore: disappearingMessagesConfigurationStore,
@ -1667,6 +1668,7 @@ extension AppSetup.GlobalsContinuation {
backupSubscriptionManager: backupSubscriptionManager,
backupTestFlightEntitlementManager: backupTestFlightEntitlementManager,
badgeCountFetcher: badgeCountFetcher,
blockedRecipientStore: blockedRecipientStore,
callLinkStore: callLinkStore,
callRecordDeleteManager: callRecordDeleteManager,
callRecordMissedCallManager: callRecordMissedCallManager,

View File

@ -85,6 +85,7 @@ public class DependenciesBridge {
public let backupSubscriptionManager: BackupSubscriptionManager
public let backupTestFlightEntitlementManager: BackupTestFlightEntitlementManager
public let badgeCountFetcher: BadgeCountFetcher
let blockedRecipientStore: BlockedRecipientStore
public let callLinkStore: any CallLinkRecordStore
public let callRecordDeleteManager: any CallRecordDeleteManager
public let callRecordMissedCallManager: CallRecordMissedCallManager
@ -225,6 +226,7 @@ public class DependenciesBridge {
backupSubscriptionManager: BackupSubscriptionManager,
backupTestFlightEntitlementManager: BackupTestFlightEntitlementManager,
badgeCountFetcher: BadgeCountFetcher,
blockedRecipientStore: BlockedRecipientStore,
callLinkStore: any CallLinkRecordStore,
callRecordDeleteManager: CallRecordDeleteManager,
callRecordMissedCallManager: CallRecordMissedCallManager,
@ -364,6 +366,7 @@ public class DependenciesBridge {
self.backupSubscriptionManager = backupSubscriptionManager
self.backupTestFlightEntitlementManager = backupTestFlightEntitlementManager
self.badgeCountFetcher = badgeCountFetcher
self.blockedRecipientStore = blockedRecipientStore
self.callLinkStore = callLinkStore
self.callRecordDeleteManager = callRecordDeleteManager
self.callRecordMissedCallManager = callRecordMissedCallManager

View File

@ -294,20 +294,25 @@ public class SSKEnvironment: NSObject {
/// Pni (a one-time migration), but it also helps ensure that the value is
/// always consistent with TSAccountManager's values.
private func fixLocalRecipientIfNeeded(dependenciesBridge: DependenciesBridge) {
self.databaseStorageRef.write { tx in
guard let localIdentifiers = dependenciesBridge.tsAccountManager.localIdentifiers(tx: tx) else {
let blockedRecipientStore = dependenciesBridge.blockedRecipientStore
let databaseStorage = self.databaseStorageRef
let recipientMerger = dependenciesBridge.recipientMerger
let tsAccountManager = dependenciesBridge.tsAccountManager
databaseStorage.write { tx in
guard let localIdentifiers = tsAccountManager.localIdentifiers(tx: tx) else {
return // Not registered yet.
}
guard let phoneNumber = E164(localIdentifiers.phoneNumber) else {
return // Registered with an invalid phone number.
}
let recipientMerger = dependenciesBridge.recipientMerger
_ = recipientMerger.applyMergeForLocalAccount(
let localRecipient = recipientMerger.applyMergeForLocalAccount(
aci: localIdentifiers.aci,
phoneNumber: phoneNumber,
pni: localIdentifiers.pni,
tx: tx,
)
blockedRecipientStore.setBlocked(false, recipientId: localRecipient.id, tx: tx)
}
}

View File

@ -183,8 +183,8 @@ public class GroupManager: NSObject {
spamReportingMetadata: .createdByLocalAction,
transaction: tx,
)
SSKEnvironment.shared.profileManagerRef.addThread(
toProfileWhitelist: thread,
SSKEnvironment.shared.profileManagerRef.addGroupId(
toProfileWhitelist: groupModel.groupId,
userProfileWriter: .localUser,
transaction: tx,
)

View File

@ -80,7 +80,11 @@ public class BlockingManager {
guard let recipientId = recipientDatabaseTable.fetchRecipient(address: address, tx: transaction)?.id else {
return false
}
return blockedRecipientStore.isBlocked(recipientId: recipientId, tx: transaction)
return isRecipientBlocked(recipientId: recipientId, tx: transaction)
}
public func isRecipientBlocked(recipientId: SignalRecipient.RowId, tx: DBReadTransaction) -> Bool {
return blockedRecipientStore.isBlocked(recipientId: recipientId, tx: tx)
}
public func isGroupIdBlocked(_ groupId: GroupIdentifier, transaction tx: DBReadTransaction) -> Bool {

View File

@ -432,12 +432,10 @@ public final class MessageReceiver {
legacyPhoneNumber: sent.destinationE164?.nilIfEmpty,
cache: SSKEnvironment.shared.signalServiceAddressCacheRef,
)
if destinationAddress.isValid {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: destinationAddress,
userProfileWriter: .localUser,
transaction: tx,
)
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
if var recipient = recipientFetcher.fetchOrCreate(address: destinationAddress, tx: tx) {
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: tx)
}
}
}

View File

@ -755,7 +755,7 @@ public class OWSIdentityManagerImpl: OWSIdentityManager {
) -> ChangeVerificationStateResult {
owsAssertDebug(identityKey.count == Constants.storedIdentityKeyLength)
guard let recipient = recipientDatabaseTable.fetchRecipient(address: address, tx: tx) else {
guard var recipient = recipientDatabaseTable.fetchRecipient(address: address, tx: tx) else {
owsFailDebug("Missing SignalRecipient")
return .error
}
@ -805,11 +805,7 @@ public class OWSIdentityManagerImpl: OWSIdentityManager {
// to the profile whitelist so they become a "Signal
// Connection". (Other devices will learn about this via
// Storage Service like normal.)
profileManager.addUser(
toProfileWhitelist: recipient.address,
userProfileWriter: .localUser,
transaction: tx,
)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: tx)
case .noLongerVerified, .implicit:
break
}

View File

@ -48,7 +48,7 @@ extension RecipientHidingManager {
guard let recipient = recipient(from: address, tx: tx) else {
return false
}
return isHiddenRecipient(recipient, tx: tx)
return isHiddenRecipient(recipientId: recipient.id, tx: tx)
}
// MARK: Write
@ -73,17 +73,15 @@ extension RecipientHidingManager {
throw RecipientHidingError.cannotHideLocalAddress
}
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let recipient = try { () throws -> SignalRecipient in
if let serviceId = address.serviceId {
return recipientFetcher.fetchOrCreate(serviceId: serviceId, tx: tx)
}
if let phoneNumber = address.e164 {
return recipientFetcher.fetchOrCreate(phoneNumber: phoneNumber, tx: tx)
var recipient = try { () throws -> SignalRecipient in
let recipient = recipientFetcher.fetchOrCreate(address: address, tx: tx)
if let recipient {
return recipient
}
throw RecipientHidingError.invalidRecipientAddress(address)
}()
try addHiddenRecipient(
recipient,
&recipient,
inKnownMessageRequestState: inKnownMessageRequestState,
wasLocallyInitiated: wasLocallyInitiated,
tx: tx,
@ -109,8 +107,8 @@ extension RecipientHidingManager {
owsFailDebug("Cannot unhide the local address")
return
}
if let recipient = recipient(from: address, tx: tx) {
removeHiddenRecipient(recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
if var recipient = recipient(from: address, tx: tx) {
removeHiddenRecipient(&recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
}
}

View File

@ -31,7 +31,7 @@ public protocol RecipientHidingManager {
/// Fetch the hidden-recipient state for the given `SignalRecipient`, if the
/// `SignalRecipient` is currently hidden.
func fetchHiddenRecipient(
signalRecipient: SignalRecipient,
recipientId: SignalRecipient.RowId,
tx: DBReadTransaction,
) -> HiddenRecipient?
@ -60,7 +60,7 @@ public protocol RecipientHidingManager {
/// Whether this hide represents one initiated on this device, or one that
/// occurred on a linked device.
func addHiddenRecipient(
_ recipient: SignalRecipient,
_ recipient: inout SignalRecipient,
inKnownMessageRequestState: Bool,
wasLocallyInitiated: Bool,
tx: DBWriteTransaction,
@ -73,7 +73,7 @@ public protocol RecipientHidingManager {
/// the hide on this device (true) or a linked device (false).
/// - Parameter tx: The transaction to use for database operations.
func removeHiddenRecipient(
_ recipient: SignalRecipient,
_ recipient: inout SignalRecipient,
wasLocallyInitiated: Bool,
tx: DBWriteTransaction,
)
@ -83,10 +83,10 @@ public extension RecipientHidingManager {
/// Whether the given `SignalRecipient` is currently hidden.
func isHiddenRecipient(
_ recipient: SignalRecipient,
recipientId: SignalRecipient.RowId,
tx: DBReadTransaction,
) -> Bool {
return fetchHiddenRecipient(signalRecipient: recipient, tx: tx) != nil
return fetchHiddenRecipient(recipientId: recipientId, tx: tx) != nil
}
}
@ -167,11 +167,11 @@ public final class RecipientHidingManagerImpl: RecipientHidingManager {
}
public func fetchHiddenRecipient(
signalRecipient: SignalRecipient,
recipientId: SignalRecipient.RowId,
tx: DBReadTransaction,
) -> HiddenRecipient? {
return failIfThrows {
return try HiddenRecipient.fetchOne(tx.database, key: signalRecipient.id)
return try HiddenRecipient.fetchOne(tx.database, key: recipientId)
}
}
@ -252,13 +252,13 @@ public final class RecipientHidingManagerImpl: RecipientHidingManager {
// MARK: -
public func addHiddenRecipient(
_ recipient: SignalRecipient,
_ recipient: inout SignalRecipient,
inKnownMessageRequestState: Bool,
wasLocallyInitiated: Bool,
tx: DBWriteTransaction,
) throws {
Logger.info("Hiding recipient")
guard !isHiddenRecipient(recipient, tx: tx) else {
guard !isHiddenRecipient(recipientId: recipient.id, tx: tx) else {
// This is a perhaps extraneous safeguard against
// hiding an already-hidden address. I say extraneous
// because theoretically the UI should not be available to
@ -277,20 +277,20 @@ public final class RecipientHidingManagerImpl: RecipientHidingManager {
try record.insert(tx.database)
}
didSetAsHidden(recipient: recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
didSetAsHidden(recipient: &recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
}
public func removeHiddenRecipient(
_ recipient: SignalRecipient,
_ recipient: inout SignalRecipient,
wasLocallyInitiated: Bool,
tx: DBWriteTransaction,
) {
if isHiddenRecipient(recipient, tx: tx) {
if isHiddenRecipient(recipientId: recipient.id, tx: tx) {
Logger.info("Unhiding recipient")
failIfThrows {
try HiddenRecipient.deleteOne(tx.database, key: recipient.id)
}
didSetAsUnhidden(recipient: recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
didSetAsUnhidden(recipient: &recipient, wasLocallyInitiated: wasLocallyInitiated, tx: tx)
}
}
}
@ -306,7 +306,7 @@ private extension RecipientHidingManagerImpl {
/// the hide on this device (true) or a linked device (false).
/// - Parameter tx: The transaction to use for database operations.
func didSetAsHidden(
recipient: SignalRecipient,
recipient: inout SignalRecipient,
wasLocallyInitiated: Bool,
tx: DBWriteTransaction,
) {
@ -331,11 +331,7 @@ private extension RecipientHidingManagerImpl {
if wasLocallyInitiated {
Logger.info("[Recipient hiding][side effects] Remove from whitelist.")
profileManager.removeUser(
fromProfileWhitelist: recipient.address,
userProfileWriter: .localUser,
transaction: tx,
)
profileManager.removeRecipientFromProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: tx)
Logger.info("[Recipient hiding][side effects] Remove from story distribution lists.")
let storyRecipientManager = DependenciesBridge.shared.storyRecipientManager
storyRecipientManager.removeRecipientIdFromAllPrivateStoryThreads(
@ -390,18 +386,14 @@ private extension RecipientHidingManagerImpl {
/// rule is in place that will also delete the corresponding
/// `HiddenRecipient` entry. This method does not get hit in
/// that case.
func didSetAsUnhidden(recipient: SignalRecipient, wasLocallyInitiated: Bool, tx: DBWriteTransaction) {
func didSetAsUnhidden(recipient: inout SignalRecipient, wasLocallyInitiated: Bool, tx: DBWriteTransaction) {
// Triggers UI updates of recipient lists.
NotificationCenter.default.postOnMainThread(name: Self.hideListDidChange, object: nil)
Logger.info("[Recipient hiding][side effects] Beginning side effects of setting as unhidden.")
if wasLocallyInitiated {
Logger.info("[Recipient hiding][side effects] Add to whitelist.")
profileManager.addUser(
toProfileWhitelist: recipient.address,
userProfileWriter: .localUser,
transaction: tx,
)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: tx)
Logger.info("[Recipient hiding][side effects] Sync with storage service.")
storageServiceManager.recordPendingUpdates(updatedAddresses: [recipient.address])
}

View File

@ -17,8 +17,6 @@ public class OWSProfileManager: ProfileManagerProtocol {
public static let notificationKeyUserProfileWriter = "kNSNotificationKey_UserProfileWriter"
private let metadataStore = KeyValueStore(collection: "kOWSProfileManager_Metadata")
private let whitelistedPhoneNumbersStore = KeyValueStore(collection: "kOWSProfileManager_UserWhitelistCollection")
private let whitelistedServiceIdsStore = KeyValueStore(collection: "kOWSProfileManager_UserUUIDWhitelistCollection")
private let whitelistedGroupsStore = KeyValueStore(collection: "kOWSProfileManager_GroupWhitelistCollection")
private let settingsStore = KeyValueStore(collection: "kOWSProfileManager_SettingsStore")
@ -82,166 +80,83 @@ public class OWSProfileManager: ProfileManagerProtocol {
localUserProfile.update(profileKey: .setTo(key), userProfileWriter: userProfileWriter, transaction: transaction)
}
public func normalizeRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBWriteTransaction) {
swift_normalizeRecipientInProfileWhitelist(recipient, tx: tx)
}
public func addRecipientToProfileWhitelist(
_ recipient: inout SignalRecipient,
userProfileWriter: UserProfileWriter,
tx: DBWriteTransaction,
) {
let blockingManager = SSKEnvironment.shared.blockingManagerRef
let hidingManager = DependenciesBridge.shared.recipientHidingManager
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
public func addUser(toProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
owsAssertDebug(address.isValid)
addUsers(toProfileWhitelist: [address], userProfileWriter: userProfileWriter, transaction: transaction)
}
public func addUsers(toProfileWhitelist addresses: [SignalServiceAddress], userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
let addressesToAdd = addressesNotBlockedOrInWhitelist(addresses, transaction: transaction)
addConfirmedUnwhitelistedAddresses(addressesToAdd, userProfileWriter: userProfileWriter, transaction: transaction)
}
public func removeUser(fromProfileWhitelist address: SignalServiceAddress) {
owsAssertDebug(address.isValid)
removeUsers(fromProfileWhitelist: [address])
}
public func removeUser(fromProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
owsAssertDebug(address.isValid)
let addressesToRemove = addressesInWhitelist([address], transaction: transaction)
removeConfirmedWhitelistedAddresses(addressesToRemove, userProfileWriter: userProfileWriter, transaction: transaction)
}
// TODO: We could add a userProfileWriter parameter.
private func removeUsers(fromProfileWhitelist addresses: [SignalServiceAddress]) {
// Try to avoid opening a write transaction.
SSKEnvironment.shared.databaseStorageRef.asyncRead { readTransaction in
let addressesToRemove = self.addressesInWhitelist(addresses, transaction: readTransaction)
if addressesToRemove.isEmpty {
return
}
SSKEnvironment.shared.databaseStorageRef.asyncWrite { writeTransaction in
self.removeConfirmedWhitelistedAddresses(addressesToRemove, userProfileWriter: .localUser, transaction: writeTransaction)
}
}
}
private func addressesNotBlockedOrInWhitelist(_ addresses: [SignalServiceAddress], transaction: DBReadTransaction) -> Set<SignalServiceAddress> {
var notBlockedOrInWhitelist = Set<SignalServiceAddress>()
for address in addresses {
// If the address is blocked, we don't want to include it
if SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(address, transaction: transaction) || RecipientHidingManagerObjcBridge.isHiddenAddress(address, tx: transaction) {
continue
}
if !isAddressInWhitelist(address, tx: transaction) {
notBlockedOrInWhitelist.insert(address)
}
}
return notBlockedOrInWhitelist
}
private func addressesInWhitelist(_ addresses: [SignalServiceAddress], transaction: DBReadTransaction) -> Set<SignalServiceAddress> {
var whitelistedAddresses = Set<SignalServiceAddress>()
for address in addresses {
if isAddressInWhitelist(address, tx: transaction) {
whitelistedAddresses.insert(address)
}
}
return whitelistedAddresses
}
private func isAddressInWhitelist(_ address: SignalServiceAddress, tx: DBReadTransaction) -> Bool {
if let uppercaseServiceId = address.serviceIdUppercaseString, whitelistedServiceIdsStore.hasValue(uppercaseServiceId, transaction: tx) {
return true
}
if let phoneNumber = address.phoneNumber, whitelistedPhoneNumbersStore.hasValue(phoneNumber, transaction: tx) {
return true
}
return false
}
private func removeConfirmedWhitelistedAddresses(_ addressesToRemove: Set<SignalServiceAddress>, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
guard !addressesToRemove.isEmpty else {
if blockingManager.isRecipientBlocked(recipientId: recipient.id, tx: tx) {
return
}
for address in addressesToRemove {
// Historically we put both the ACI and phone number into their respective
// stores. We currently save only the best identifier, but we should still
// try and remove both to handle these historical cases.
if let uppercaseServiceId = address.serviceIdUppercaseString {
whitelistedServiceIdsStore.removeValue(forKey: uppercaseServiceId, transaction: transaction)
}
if let phoneNumber = address.phoneNumber {
whitelistedPhoneNumbersStore.removeValue(forKey: phoneNumber, transaction: transaction)
}
if let thread = TSContactThread.getWithContactAddress(address, transaction: transaction) {
SSKEnvironment.shared.databaseStorageRef.touch(thread: thread, shouldReindex: false, tx: transaction)
}
}
transaction.addSyncCompletion {
// Mark the removed whitelisted addresses for update
if OWSUserProfile.shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter) {
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedAddresses: Array(addressesToRemove))
}
for address in addressesToRemove {
NotificationCenter.default.postOnMainThread(name: UserProfileNotifications.profileWhitelistDidChange, object: nil, userInfo: [
UserProfileNotifications.profileAddressKey: address,
Self.notificationKeyUserProfileWriter: NSNumber(value: userProfileWriter.rawValue),
])
}
}
}
private func addConfirmedUnwhitelistedAddresses(_ addressesToAdd: Set<SignalServiceAddress>, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
guard !addressesToAdd.isEmpty else {
if hidingManager.isHiddenRecipient(recipientId: recipient.id, tx: tx) {
return
}
switch recipient.status {
case .whitelisted:
return
case .unspecified:
break
}
recipient.status = .whitelisted
recipientStore.updateRecipient(recipient, transaction: tx)
_didUpdateRecipientInWhitelist(recipient, userProfileWriter: userProfileWriter, tx: tx)
}
for address in addressesToAdd {
let serviceId = address.serviceId
if let serviceId = serviceId as? Aci {
whitelistedServiceIdsStore.setBool(true, key: serviceId.serviceIdUppercaseString, transaction: transaction)
} else if let phoneNumber = address.phoneNumber {
whitelistedPhoneNumbersStore.setBool(true, key: phoneNumber, transaction: transaction)
} else if let serviceId = serviceId as? Pni {
whitelistedServiceIdsStore.setBool(true, key: serviceId.serviceIdUppercaseString, transaction: transaction)
}
public func removeRecipientFromProfileWhitelist(
_ recipient: inout SignalRecipient,
userProfileWriter: UserProfileWriter,
tx: DBWriteTransaction,
) {
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
switch recipient.status {
case .unspecified:
return
case .whitelisted:
break
}
recipient.status = .unspecified
recipientStore.updateRecipient(recipient, transaction: tx)
_didUpdateRecipientInWhitelist(recipient, userProfileWriter: userProfileWriter, tx: tx)
}
if let thread = TSContactThread.getWithContactAddress(address, transaction: transaction) {
SSKEnvironment.shared.databaseStorageRef.touch(thread: thread, shouldReindex: false, tx: transaction)
}
private func _didUpdateRecipientInWhitelist(
_ recipient: SignalRecipient,
userProfileWriter: UserProfileWriter,
tx: DBWriteTransaction,
) {
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
let storageServiceManager = SSKEnvironment.shared.storageServiceManagerRef
if let thread = TSContactThread.getWithContactAddress(recipient.address, transaction: tx) {
databaseStorage.touch(thread: thread, shouldReindex: false, tx: tx)
}
transaction.addSyncCompletion {
tx.addSyncCompletion {
// Mark the new whitelisted addresses for update
if OWSUserProfile.shouldUpdateStorageServiceForUserProfileWriter(userProfileWriter) {
SSKEnvironment.shared.storageServiceManagerRef.recordPendingUpdates(updatedAddresses: Array(addressesToAdd))
storageServiceManager.recordPendingUpdates(updatedAddresses: [recipient.address])
}
for address in addressesToAdd {
NotificationCenter.default.postOnMainThread(name: UserProfileNotifications.profileWhitelistDidChange, object: nil, userInfo: [
UserProfileNotifications.profileAddressKey: address,
Self.notificationKeyUserProfileWriter: NSNumber(value: userProfileWriter.rawValue),
])
}
NotificationCenter.default.postOnMainThread(name: UserProfileNotifications.profileWhitelistDidChange, object: nil, userInfo: [
UserProfileNotifications.profileAddressKey: recipient.address,
Self.notificationKeyUserProfileWriter: NSNumber(value: userProfileWriter.rawValue),
])
}
}
public func isUser(inProfileWhitelist address: SignalServiceAddress, transaction: DBReadTransaction) -> Bool {
owsAssertDebug(address.isValid)
public func isRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBReadTransaction) -> Bool {
let blockingManager = SSKEnvironment.shared.blockingManagerRef
let hidingManager = DependenciesBridge.shared.recipientHidingManager
if SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(address, transaction: transaction) || RecipientHidingManagerObjcBridge.isHiddenAddress(address, tx: transaction) {
return false
}
return
!blockingManager.isRecipientBlocked(recipientId: recipient.id, tx: tx)
&& !hidingManager.isHiddenRecipient(recipientId: recipient.id, tx: tx)
&& recipient.status == .whitelisted
return isAddressInWhitelist(address, tx: transaction)
}
public func addGroupId(toProfileWhitelist groupId: Data, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
@ -303,14 +218,6 @@ public class OWSProfileManager: ProfileManagerProtocol {
}
}
public func addThread(toProfileWhitelist thread: TSThread, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
if thread.isGroupThread, let groupThread = thread as? TSGroupThread {
addGroupId(toProfileWhitelist: groupThread.groupModel.groupId, userProfileWriter: userProfileWriter, transaction: transaction)
} else if !thread.isGroupThread, let contactThread = thread as? TSContactThread {
addUser(toProfileWhitelist: contactThread.contactAddress, userProfileWriter: userProfileWriter, transaction: transaction)
}
}
public func isGroupId(inProfileWhitelist groupId: Data, transaction: DBReadTransaction) -> Bool {
owsAssertDebug(!groupId.isEmpty)
if SSKEnvironment.shared.blockingManagerRef.isGroupIdBlocked_deprecated(groupId, tx: transaction) {
@ -320,16 +227,6 @@ public class OWSProfileManager: ProfileManagerProtocol {
return whitelistedGroupsStore.hasValue(groupIdKey, transaction: transaction)
}
public func isThread(inProfileWhitelist thread: TSThread, transaction: DBReadTransaction) -> Bool {
if thread.isGroupThread, let groupThread = thread as? TSGroupThread {
return isGroupId(inProfileWhitelist: groupThread.groupModel.groupId, transaction: transaction)
} else if !thread.isGroupThread, let contactThread = thread as? TSContactThread {
return isUser(inProfileWhitelist: contactThread.contactAddress, transaction: transaction)
} else {
return false
}
}
// MARK: Other User's Profiles
public func localUserProfile(tx: DBReadTransaction) -> OWSUserProfile? {
@ -544,29 +441,13 @@ extension OWSProfileManager: ProfileManager {
// MARK: -
public func allWhitelistedAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] {
var addresses = Set<SignalServiceAddress>()
for serviceIdString in whitelistedServiceIdsStore.allKeys(transaction: tx) {
addresses.insert(SignalServiceAddress(serviceIdString: serviceIdString))
}
for phoneNumber in whitelistedPhoneNumbersStore.allKeys(transaction: tx) {
addresses.insert(SignalServiceAddress.legacyAddress(serviceId: nil, phoneNumber: phoneNumber))
}
return Array(addresses)
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
return recipientStore.fetchWhitelistedRecipients(tx: tx).map(\.address)
}
public func allWhitelistedRegisteredAddresses(tx: DBReadTransaction) -> [SignalServiceAddress] {
return allWhitelistedAddresses(tx: tx).lazy.compactMap { address in
guard
let recipient = DependenciesBridge.shared.recipientDatabaseTable
.fetchRecipient(address: address, tx: tx),
recipient.isRegistered
else {
return nil
}
return recipient.address
}
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
return recipientStore.fetchWhitelistedRecipients(tx: tx).lazy.filter(\.isRegistered).map(\.address)
}
// MARK: -
@ -633,8 +514,7 @@ extension OWSProfileManager: ProfileManager {
case blocklistChange(BlocklistChange)
struct BlocklistChange {
let phoneNumbers: [String]
let serviceIds: [ServiceId]
let recipientIds: [SignalRecipient.RowId]
let groupIds: [Data]
}
@ -644,17 +524,15 @@ extension OWSProfileManager: ProfileManager {
}
private func blocklistRotationTriggerIfNeeded(tx: DBReadTransaction) -> RotateProfileKeyTrigger? {
let victimPhoneNumbers = self.blockedPhoneNumbersInWhitelist(tx: tx)
let victimServiceIds = self.blockedServiceIdsInWhitelist(tx: tx)
let victimRecipientIds = self.blockedRecipientIdsInWhitelist(tx: tx)
let victimGroupIds = self.blockedGroupIDsInWhitelist(tx: tx)
if victimPhoneNumbers.isEmpty, victimServiceIds.isEmpty, victimGroupIds.isEmpty {
if victimRecipientIds.isEmpty, victimGroupIds.isEmpty {
// No need to rotate the profile key.
return nil
}
return .blocklistChange(.init(
phoneNumbers: victimPhoneNumbers,
serviceIds: victimServiceIds,
return .blocklistChange(RotateProfileKeyTrigger.BlocklistChange(
recipientIds: victimRecipientIds,
groupIds: victimGroupIds,
))
}
@ -776,14 +654,15 @@ extension OWSProfileManager: ProfileManager {
// It's absolutely essential that these values are persisted in the same transaction
// in which we persist our new profile key, since storing them is what marks the
// profile key rotation as "complete" (removing newly blocked users from the whitelist).
self.whitelistedPhoneNumbersStore.removeValues(
forKeys: trigger.phoneNumbers,
transaction: tx,
)
self.whitelistedServiceIdsStore.removeValues(
forKeys: trigger.serviceIds.map { $0.serviceIdUppercaseString },
transaction: tx,
)
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
trigger.recipientIds.forEach { recipientId in
let recipient = recipientStore.fetchRecipient(rowId: recipientId, tx: tx)
guard var recipient else {
return
}
recipient.status = .unspecified
recipientStore.updateRecipient(recipient, transaction: tx)
}
self.whitelistedGroupsStore.removeValues(
forKeys: trigger.groupIds.map { self.groupKey(groupId: $0) },
transaction: tx,
@ -801,22 +680,12 @@ extension OWSProfileManager: ProfileManager {
return true
}
private func blockedPhoneNumbersInWhitelist(tx: DBReadTransaction) -> [String] {
let allWhitelistedNumbers = whitelistedPhoneNumbersStore.allKeys(transaction: tx)
private func blockedRecipientIdsInWhitelist(tx: DBReadTransaction) -> [SignalRecipient.RowId] {
let blockingManager = SSKEnvironment.shared.blockingManagerRef
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
return allWhitelistedNumbers.filter { candidate in
let address = SignalServiceAddress.legacyAddress(serviceId: nil, phoneNumber: candidate)
return SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(address, transaction: tx)
}
}
private func blockedServiceIdsInWhitelist(tx: DBReadTransaction) -> [ServiceId] {
let allWhitelistedServiceIds = whitelistedServiceIdsStore.allKeys(transaction: tx).compactMap {
try? ServiceId.parseFrom(serviceIdString: $0)
}
return allWhitelistedServiceIds.filter { candidate in
return SSKEnvironment.shared.blockingManagerRef.isAddressBlocked(SignalServiceAddress(candidate), transaction: tx)
return blockingManager.blockedRecipientIds(tx: tx).filter {
return recipientStore.fetchRecipient(rowId: $0, tx: tx)!.status == .whitelisted
}
}
@ -839,57 +708,6 @@ extension OWSProfileManager: ProfileManager {
}
}
func swift_normalizeRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBWriteTransaction) {
Self.swift_normalizeRecipientInProfileWhitelist(
recipient,
serviceIdStore: whitelistedServiceIdsStore,
phoneNumberStore: whitelistedPhoneNumbersStore,
tx: tx,
)
}
public static func swift_normalizeRecipientInProfileWhitelist(
_ recipient: SignalRecipient,
serviceIdStore: KeyValueStore,
phoneNumberStore: KeyValueStore,
tx: DBWriteTransaction,
) {
// First, we figure out which identifiers are whitelisted.
let orderedIdentifiers: [(store: KeyValueStore, key: String, isInWhitelist: Bool)] = [
(serviceIdStore, recipient.aci?.serviceIdUppercaseString),
(phoneNumberStore, recipient.phoneNumber?.stringValue),
(serviceIdStore, recipient.pni?.serviceIdUppercaseString),
].compactMap { store, key -> (KeyValueStore, String, Bool)? in
guard let key else { return nil }
return (store, key, store.hasValue(key, transaction: tx))
}
guard let preferredIdentifier = orderedIdentifiers.first else {
return
}
// If any identifier is in the whitelist, make sure the preferred
// identifier is in the whitelist.
let isAnyInWhitelist = orderedIdentifiers.contains(where: { $0.isInWhitelist })
if isAnyInWhitelist {
if !preferredIdentifier.isInWhitelist {
preferredIdentifier.store.setBool(true, key: preferredIdentifier.key, transaction: tx)
}
} else {
if preferredIdentifier.isInWhitelist {
preferredIdentifier.store.removeValue(forKey: preferredIdentifier.key, transaction: tx)
}
}
// Always remove all the other identifiers from the whitelist. If the user
// should be in the whitelist, we add the preferred identifier above.
for remainingIdentifier in orderedIdentifiers.dropFirst() {
if remainingIdentifier.isInWhitelist {
remainingIdentifier.store.removeValue(forKey: remainingIdentifier.key, transaction: tx)
}
}
}
class func updateStorageServiceIfNecessary() {
guard
CurrentAppContext().isMainApp,

View File

@ -14,14 +14,10 @@ public protocol ProfileManagerProtocol {
/// Fetch the locally-cached profile for an address.
func userProfile(for address: SignalServiceAddress, tx: DBReadTransaction) -> OWSUserProfile?
func isUser(inProfileWhitelist address: SignalServiceAddress, transaction: DBReadTransaction) -> Bool
func normalizeRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBWriteTransaction)
func isThread(inProfileWhitelist thread: TSThread, transaction: DBReadTransaction) -> Bool
func addThread(toProfileWhitelist thread: TSThread, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
func addUser(toProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
func addUsers(toProfileWhitelist addresses: [SignalServiceAddress], userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
func removeUser(fromProfileWhitelist address: SignalServiceAddress)
func removeUser(fromProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
func addRecipientToProfileWhitelist(_ recipient: inout SignalRecipient, userProfileWriter: UserProfileWriter, tx: DBWriteTransaction)
func removeRecipientFromProfileWhitelist(_ recipient: inout SignalRecipient, userProfileWriter: UserProfileWriter, tx: DBWriteTransaction)
func isRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBReadTransaction) -> Bool
func isGroupId(inProfileWhitelist groupId: Data, transaction: DBReadTransaction) -> Bool
func addGroupId(toProfileWhitelist groupId: Data, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
func removeGroupId(fromProfileWhitelist groupId: Data, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction)
@ -41,3 +37,24 @@ public protocol ProfileManagerProtocol {
/// force a rotation independently of whitelist state.
func forceRotateLocalProfileKeyForGroupDeparture(with transaction: DBWriteTransaction)
}
extension ProfileManagerProtocol {
public func isUser(inProfileWhitelist address: SignalServiceAddress, transaction: DBReadTransaction) -> Bool {
owsAssertDebug(address.isValid)
let recipientStore = DependenciesBridge.shared.recipientDatabaseTable
guard let recipient = recipientStore.fetchRecipient(address: address, tx: transaction) else {
return false
}
return isRecipientInProfileWhitelist(recipient, tx: transaction)
}
public func isThread(inProfileWhitelist thread: TSThread, transaction: DBReadTransaction) -> Bool {
if thread.isGroupThread, let groupThread = thread as? TSGroupThread {
return isGroupId(inProfileWhitelist: groupThread.groupModel.groupId, transaction: transaction)
} else if !thread.isGroupThread, let contactThread = thread as? TSContactThread {
return isUser(inProfileWhitelist: contactThread.contactAddress, transaction: transaction)
} else {
return false
}
}
}

View File

@ -316,6 +316,7 @@ public class GRDBSchemaMigrator {
case dropTSAttachment
case deprecateStoredShouldStartExpireTimer
case addSession
case addRecipientStatus
// NOTE: Every time we add a migration id, consider
// incrementing grdbSchemaVersionLatest.
@ -439,7 +440,7 @@ public class GRDBSchemaMigrator {
}
public static let grdbSchemaVersionDefault: UInt = 0
public static let grdbSchemaVersionLatest: UInt = 138
public static let grdbSchemaVersionLatest: UInt = 139
private class DatabaseMigratorWrapper {
var migrator = DatabaseMigrator()
@ -4985,6 +4986,12 @@ public class GRDBSchemaMigrator {
return .success(())
}
migrator.registerMigration(.addRecipientStatus) { tx in
try addRecipientStatus(tx: tx)
try migrateRecipientWhitelist(tx: tx)
return .success(())
}
// MARK: - Schema Migration Insertion Point
}
@ -6285,12 +6292,12 @@ public class GRDBSchemaMigrator {
var outdatedRecipientIds = Set<Int64>()
for blockedAciString in blockedAciStrings {
let recipientId = try fetchOrCreateRecipientV1(aciString: blockedAciString, tx: tx)
let recipientId = try fetchOrCreateRecipient(.V1, aciString: blockedAciString, tx: tx)
blockedRecipientIds.insert(recipientId)
}
for blockedPhoneNumber in blockedPhoneNumbers {
let recipientId = try fetchOrCreateRecipientV1(phoneNumber: blockedPhoneNumber, tx: tx)
let recipientId = try fetchOrCreateRecipient(.V1, phoneNumber: blockedPhoneNumber, tx: tx)
if blockedRecipientIds.contains(recipientId) {
// They're already blocked by their ACI.
continue
@ -6386,56 +6393,73 @@ public class GRDBSchemaMigrator {
}
}
private static func fetchOrCreateRecipientV1(aciString: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
/// The representation of recipients has changed over time. Old migrations
/// need to continue writing in the old format; new migrations need to write
/// in the new format; when adding a migraton, you may need to define a new
/// version if the latest version isn't compatible with the current schema.
private enum RecipientVersion {
case V1
/// See migrateRecipientDeviceIds.
case V2
}
private static func fetchOrCreateRecipient(_ version: RecipientVersion, aciString: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
let db = tx.database
let existingRecipientId = try Int64.fetchOne(db, sql: "SELECT id FROM model_SignalRecipient WHERE recipientUUID IS ?", arguments: [aciString])
if let existingRecipientId {
return existingRecipientId
}
return try createRecipientV1(aciString: aciString, phoneNumber: nil, pniString: nil, tx: tx)
return try createRecipient(version, aciString: aciString, phoneNumber: nil, pniString: nil, tx: tx)
}
private static func fetchOrCreateRecipientV1(phoneNumber: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
private static func fetchOrCreateRecipient(_ version: RecipientVersion, phoneNumber: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
let db = tx.database
let existingRecipientId = try Int64.fetchOne(db, sql: "SELECT id FROM model_SignalRecipient WHERE recipientPhoneNumber IS ?", arguments: [phoneNumber])
if let existingRecipientId {
return existingRecipientId
}
return try createRecipientV1(aciString: nil, phoneNumber: phoneNumber, pniString: nil, tx: tx)
return try createRecipient(version, aciString: nil, phoneNumber: phoneNumber, pniString: nil, tx: tx)
}
private static func fetchOrCreateRecipientV1(pniString: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
private static func fetchOrCreateRecipient(_ version: RecipientVersion, pniString: String, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
let db = tx.database
let existingRecipientId = try Int64.fetchOne(db, sql: "SELECT id FROM model_SignalRecipient WHERE pni IS ?", arguments: [pniString])
if let existingRecipientId {
return existingRecipientId
}
return try createRecipientV1(aciString: nil, phoneNumber: nil, pniString: pniString, tx: tx)
return try createRecipient(version, aciString: nil, phoneNumber: nil, pniString: pniString, tx: tx)
}
private static func fetchOrCreateRecipientV1(address: FrozenSignalServiceAddress, tx: DBWriteTransaction) throws -> SignalRecipient.RowId? {
private static func fetchOrCreateRecipient(_ version: RecipientVersion, address: FrozenSignalServiceAddress, tx: DBWriteTransaction) throws -> SignalRecipient.RowId? {
if let aci = address.serviceId as? Aci {
let aciString = aci.serviceIdUppercaseString
return try fetchOrCreateRecipientV1(aciString: aciString, tx: tx)
return try fetchOrCreateRecipient(version, aciString: aciString, tx: tx)
}
if let phoneNumber = address.phoneNumber {
return try fetchOrCreateRecipientV1(phoneNumber: phoneNumber, tx: tx)
return try fetchOrCreateRecipient(version, phoneNumber: phoneNumber, tx: tx)
}
if let pni = address.serviceId as? Pni {
let pniString = pni.serviceIdUppercaseString
return try fetchOrCreateRecipientV1(pniString: pniString, tx: tx)
return try fetchOrCreateRecipient(version, pniString: pniString, tx: tx)
}
return nil
}
private static func createRecipientV1(aciString: String?, phoneNumber: String?, pniString: String?, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
private static func createRecipient(_ version: RecipientVersion, aciString: String?, phoneNumber: String?, pniString: String?, tx: DBWriteTransaction) throws -> SignalRecipient.RowId {
let emptyDevices: Data
switch version {
case .V1:
emptyDevices = try NSKeyedArchiver.archivedData(withRootObject: NSOrderedSet(array: [] as [NSNumber]), requiringSecureCoding: true)
case .V2:
emptyDevices = Data()
}
try tx.database.execute(
sql: """
INSERT INTO "model_SignalRecipient" ("recordType", "uniqueId", "devices", "recipientPhoneNumber", "recipientUUID", "pni") VALUES (31, ?, ?, ?, ?, ?)
""",
arguments: [
UUID().uuidString,
NSKeyedArchiver.archivedData(withRootObject: NSOrderedSet(array: [] as [NSNumber]), requiringSecureCoding: true),
emptyDevices,
phoneNumber,
aciString,
pniString,
@ -6992,7 +7016,7 @@ public class GRDBSchemaMigrator {
continue
}
for address in addresses {
guard let recipientId = try fetchOrCreateRecipientV1(address: address, tx: tx) else {
guard let recipientId = try fetchOrCreateRecipient(.V1, address: address, tx: tx) else {
owsFailDebug("Couldn't include empty story recipient address")
continue
}
@ -7405,6 +7429,62 @@ public class GRDBSchemaMigrator {
try tx.database.execute(sql: "DELETE FROM keyvalue WHERE collection = ?", arguments: [collection])
}
}
static func addRecipientStatus(tx: DBWriteTransaction) throws {
try tx.database.alter(table: "model_SignalRecipient") {
$0.add(column: "status", .integer).notNull().defaults(to: 0)
}
// For fetching whitelisted recipients.
try tx.database.create(index: "Recipient_Status", on: "model_SignalRecipient", columns: ["status"])
}
static func migrateRecipientWhitelist(tx: DBWriteTransaction) throws {
let serviceIdCollection = "kOWSProfileManager_UserUUIDWhitelistCollection"
let phoneNumberCollection = "kOWSProfileManager_UserWhitelistCollection"
var recipientIds = Set<SignalRecipient.RowId>()
let serviceIdStrings = try String.fetchAll(
tx.database,
sql: "SELECT key FROM keyvalue WHERE collection = ?",
arguments: [serviceIdCollection],
)
for serviceIdString in serviceIdStrings {
guard let serviceId = try? ServiceId.parseFrom(serviceIdString: serviceIdString) else {
continue
}
switch serviceId.concreteType {
case .aci(let aci):
recipientIds.insert(try fetchOrCreateRecipient(.V2, aciString: aci.serviceIdUppercaseString, tx: tx))
case .pni(let pni):
recipientIds.insert(try fetchOrCreateRecipient(.V2, pniString: pni.serviceIdUppercaseString, tx: tx))
}
}
let phoneNumberStrings = try String.fetchAll(
tx.database,
sql: "SELECT key FROM keyvalue WHERE collection = ?",
arguments: [phoneNumberCollection],
)
for phoneNumberString in phoneNumberStrings {
guard let phoneNumber = E164(phoneNumberString) else {
continue
}
recipientIds.insert(try fetchOrCreateRecipient(.V2, phoneNumber: phoneNumber.stringValue, tx: tx))
}
if !recipientIds.isEmpty {
Logger.info("adding \(recipientIds.count) recipients to the whitelist (from \(serviceIdStrings.count) service ids & \(phoneNumberStrings.count) phone numbers)")
}
for recipientId in recipientIds {
try tx.database.execute(sql: "UPDATE model_SignalRecipient SET status = 1 WHERE id = ?", arguments: [recipientId])
}
for collection in [serviceIdCollection, phoneNumberCollection] {
try tx.database.execute(sql: "DELETE FROM keyvalue WHERE collection = ?", arguments: [collection])
}
}
}
// MARK: -

View File

@ -233,7 +233,7 @@ public class ThreadFinder {
tx: transaction,
),
let hiddenRecipient = recipientHidingManager.fetchHiddenRecipient(
signalRecipient: signalRecipient,
recipientId: signalRecipient.id,
tx: transaction,
)
{

View File

@ -296,7 +296,7 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater {
// This could be an ACI or a PNI address.
let anyAddress = SignalServiceAddress(contact.serviceIds.aciOrElsePni)
let isInWhitelist = profileManager.isUser(inProfileWhitelist: anyAddress, transaction: tx)
let isInWhitelist = profileManager.isRecipientInProfileWhitelist(recipient, tx: tx)
builder.setWhitelisted(isInWhitelist)
builder.setBlocked(blockingManager.isAddressBlocked(anyAddress, transaction: tx))
@ -491,7 +491,7 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater {
return _mergeRecord(
record,
recipient: recipient,
recipient: &recipient,
serviceIds: serviceIds,
// If we merge and don't end up with what's in Storage Service, then it
// probably means that a linked device is wrong or we've hit a race
@ -509,7 +509,7 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater {
private func _mergeRecord(
_ record: StorageServiceProtoContactRecord,
recipient: SignalRecipient,
recipient: inout SignalRecipient,
serviceIds: AtLeastOneServiceId,
needsUpdate: Bool,
tx: DBWriteTransaction,
@ -521,7 +521,7 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater {
// Gather some local contact state to do comparisons against.
let localIsBlocked = blockingManager.isAddressBlocked(anyAddress, transaction: tx)
let localIsHidden = recipientHidingManager.isHiddenAddress(anyAddress, tx: tx)
let localIsWhitelisted = profileManager.isUser(inProfileWhitelist: anyAddress, transaction: tx)
let localIsWhitelisted = profileManager.isRecipientInProfileWhitelist(recipient, tx: tx)
let localUserProfile = profileManager.userProfile(for: anyAddress, tx: tx)
// If our local profile key record differs from what's on the service, use the service's value.
@ -622,17 +622,9 @@ class StorageServiceContactRecordUpdater: StorageServiceRecordUpdater {
// If our local whitelisted state differs from the service state, use the service's value.
if record.whitelisted != localIsWhitelisted {
if record.whitelisted {
profileManager.addUser(
toProfileWhitelist: anyAddress,
userProfileWriter: .storageService,
transaction: tx,
)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .storageService, tx: tx)
} else {
profileManager.removeUser(
fromProfileWhitelist: anyAddress,
userProfileWriter: .storageService,
transaction: tx,
)
profileManager.removeRecipientFromProfileWhitelist(&recipient, userProfileWriter: .storageService, tx: tx)
}
}

View File

@ -14,8 +14,8 @@ class OWSFakeProfileManager {
var localProfile: OWSUserProfile?
var localProfileKey: Aes256Key?
private var recipientWhitelist: Set<SignalServiceAddress> = []
private var threadWhitelist: Set<String> = []
private var recipientWhitelist: Set<SignalRecipient.RowId> = []
private var groupIdWhitelist: Set<Data> = []
}
extension OWSFakeProfileManager: ProfileManagerProtocol {
@ -28,51 +28,30 @@ extension OWSFakeProfileManager: ProfileManagerProtocol {
return fakeUserProfiles![addressParam]
}
func normalizeRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBWriteTransaction) {
func addRecipientToProfileWhitelist(_ recipient: inout SignalRecipient, userProfileWriter: UserProfileWriter, tx: DBWriteTransaction) {
recipient.status = .whitelisted
recipientWhitelist.insert(recipient.id)
}
func isUser(inProfileWhitelist address: SignalServiceAddress, transaction: DBReadTransaction) -> Bool {
recipientWhitelist.contains(address)
func removeRecipientFromProfileWhitelist(_ recipient: inout SignalRecipient, userProfileWriter: UserProfileWriter, tx: DBWriteTransaction) {
recipient.status = .unspecified
recipientWhitelist.remove(recipient.id)
}
func isThread(inProfileWhitelist thread: TSThread, transaction: DBReadTransaction) -> Bool {
threadWhitelist.contains(thread.uniqueId)
}
func addUser(toProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
recipientWhitelist.insert(address)
}
func addUsers(toProfileWhitelist addresses: [SignalServiceAddress], userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
recipientWhitelist.formUnion(addresses)
}
func removeUser(fromProfileWhitelist address: SignalServiceAddress) {
recipientWhitelist.remove(address)
}
func removeUser(fromProfileWhitelist address: SignalServiceAddress, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
recipientWhitelist.remove(address)
func isRecipientInProfileWhitelist(_ recipient: SignalRecipient, tx: DBReadTransaction) -> Bool {
return recipientWhitelist.contains(recipient.id)
}
func isGroupId(inProfileWhitelist groupId: Data, transaction: DBReadTransaction) -> Bool {
threadWhitelist.contains(groupId.hexadecimalString)
return groupIdWhitelist.contains(groupId)
}
func addGroupId(toProfileWhitelist groupId: Data, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
threadWhitelist.insert(groupId.hexadecimalString)
groupIdWhitelist.insert(groupId)
}
func removeGroupId(fromProfileWhitelist groupId: Data, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
threadWhitelist.remove(groupId.hexadecimalString)
}
func addThread(toProfileWhitelist thread: TSThread, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {
if thread.isGroupThread, let groupThread = thread as? TSGroupThread {
addGroupId(toProfileWhitelist: groupThread.groupModel.groupId, userProfileWriter: userProfileWriter, transaction: transaction)
} else if !thread.isGroupThread, let contactThread = thread as? TSContactThread {
addUser(toProfileWhitelist: contactThread.contactAddress, userProfileWriter: userProfileWriter, transaction: transaction)
}
groupIdWhitelist.remove(groupId)
}
func setLocalProfileKey(_ key: Aes256Key, userProfileWriter: UserProfileWriter, transaction: DBWriteTransaction) {

View File

@ -282,9 +282,13 @@ extension ThreadUtil {
public class func addThreadToProfileWhitelistIfEmptyOrPendingRequestAndSetDefaultTimerWithSneakyTransaction(
_ thread: TSThread,
) -> Bool {
let databaseStorage = SSKEnvironment.shared.databaseStorageRef
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
let threadAsContactThread = thread as? TSContactThread
let (shouldSetUniversalTimer, shouldAddToProfileWhitelist) = SSKEnvironment.shared.databaseStorageRef.read { tx -> (Bool, Bool) in
let (shouldSetUniversalTimer, shouldAddToProfileWhitelist) = databaseStorage.read { tx -> (Bool, Bool) in
let universalTimer: Bool = {
guard let threadAsContactThread else { return false }
return Self.shouldSetUniversalTimer(contactThread: threadAsContactThread, tx: tx)
@ -294,15 +298,28 @@ extension ThreadUtil {
return (universalTimer, profileWhitelist)
}
if shouldSetUniversalTimer, let threadAsContactThread {
SSKEnvironment.shared.databaseStorageRef.write { tx in setUniversalTimer(contactThread: threadAsContactThread, tx: tx) }
databaseStorage.write { tx in setUniversalTimer(contactThread: threadAsContactThread, tx: tx) }
}
if shouldAddToProfileWhitelist {
SSKEnvironment.shared.databaseStorageRef.write { tx in
SSKEnvironment.shared.profileManagerRef.addThread(
toProfileWhitelist: thread,
userProfileWriter: .localUser,
transaction: tx,
)
databaseStorage.write { tx in
switch thread {
case let thread as TSGroupThread:
profileManager.addGroupId(
toProfileWhitelist: thread.groupModel.groupId,
userProfileWriter: .localUser,
transaction: tx,
)
case let thread as TSContactThread:
if var recipient = recipientFetcher.fetchOrCreate(address: thread.contactAddress, tx: tx) {
profileManager.addRecipientToProfileWhitelist(
&recipient,
userProfileWriter: .localUser,
tx: tx,
)
}
default:
owsFailDebug("can't whitelist \(type(of: thread))")
}
}
}
return shouldAddToProfileWhitelist
@ -314,6 +331,9 @@ extension ThreadUtil {
setDefaultTimerIfNecessary: Bool,
tx: DBWriteTransaction,
) -> Bool {
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
if
setDefaultTimerIfNecessary,
let contactThread = thread as? TSContactThread,
@ -323,11 +343,24 @@ extension ThreadUtil {
}
let shouldAddToProfileWhitelist = shouldAddThreadToProfileWhitelist(thread, tx: tx)
if shouldAddToProfileWhitelist {
SSKEnvironment.shared.profileManagerRef.addThread(
toProfileWhitelist: thread,
userProfileWriter: .localUser,
transaction: tx,
)
switch thread {
case let thread as TSGroupThread:
profileManager.addGroupId(
toProfileWhitelist: thread.groupModel.groupId,
userProfileWriter: .localUser,
transaction: tx,
)
case let thread as TSContactThread:
if var recipient = recipientFetcher.fetchOrCreate(address: thread.contactAddress, tx: tx) {
profileManager.addRecipientToProfileWhitelist(
&recipient,
userProfileWriter: .localUser,
tx: tx,
)
}
default:
owsFailDebug("can't whitelist \(type(of: thread))")
}
}
return shouldAddToProfileWhitelist
}

View File

@ -9,67 +9,6 @@ import XCTest
@testable import SignalServiceKit
class ProfileManagerTest: XCTestCase {
func testNormalizeRecipientInProfileWhitelist() {
let aci = Aci.constantForTesting("00000000-0000-4000-8000-000000000aaa")
let phoneNumber = E164("+16505550100")!
let pni = Pni.constantForTesting("PNI:00000000-0000-4000-8000-000000000bbb")
let serviceIdStore = KeyValueStore(collection: "serviceId")
let phoneNumberStore = KeyValueStore(collection: "phoneNumber")
let db = InMemoryDB()
func normalizeRecipient(aci: Aci?, phoneNumber: E164?, pni: Pni?, tx: DBWriteTransaction) {
try! SignalRecipient.deleteAll(tx.database)
let recipient = try! SignalRecipient.insertRecord(
aci: aci,
phoneNumber: phoneNumber,
pni: pni,
tx: tx,
)
OWSProfileManager.swift_normalizeRecipientInProfileWhitelist(
recipient,
serviceIdStore: serviceIdStore,
phoneNumberStore: phoneNumberStore,
tx: tx,
)
}
// Don't add any values unless one is already present.
db.write { tx in
normalizeRecipient(aci: aci, phoneNumber: phoneNumber, pni: pni, tx: tx)
XCTAssertFalse(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx))
XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx))
XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx))
}
// Move the PNI identifier to the phone number.
db.write { tx in
serviceIdStore.setBool(true, key: pni.serviceIdUppercaseString, transaction: tx)
normalizeRecipient(aci: nil, phoneNumber: phoneNumber, pni: pni, tx: tx)
XCTAssertFalse(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx))
XCTAssertTrue(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx))
XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx))
}
// Clear lower priority identifiers when multiple are present.
db.write { tx in
serviceIdStore.setBool(true, key: aci.serviceIdUppercaseString, transaction: tx)
normalizeRecipient(aci: aci, phoneNumber: phoneNumber, pni: pni, tx: tx)
XCTAssertTrue(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx))
XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx))
XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx))
}
// Keep the highest priority identifier if it's already present.
db.write { tx in
normalizeRecipient(aci: aci, phoneNumber: phoneNumber, pni: pni, tx: tx)
XCTAssertTrue(serviceIdStore.hasValue(aci.serviceIdUppercaseString, transaction: tx))
XCTAssertFalse(phoneNumberStore.hasValue(phoneNumber.stringValue, transaction: tx))
XCTAssertFalse(serviceIdStore.hasValue(pni.serviceIdUppercaseString, transaction: tx))
}
}
func testEncodeDecodeProfileChanges() throws {
let testCases: [(PendingProfileUpdate, String)] = [
(

View File

@ -1284,4 +1284,60 @@ class GRDBSchemaMigratorTest: XCTestCase {
XCTAssertEqual(sessions[5]["serializedRecord"] as Data?, nil)
}
}
func testMigrateWhitelist() throws {
let databaseQueue = DatabaseQueue()
try databaseQueue.write { db in
try db.execute(sql: """
CREATE TABLE "model_SignalRecipient" (
"id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
"recordType" INTEGER NOT NULL,
"uniqueId" TEXT NOT NULL,
"recipientPhoneNumber" TEXT UNIQUE,
"recipientUUID" TEXT UNIQUE,
"pni" TEXT UNIQUE,
"devices" BLOB NOT NULL
);
CREATE TABLE "keyvalue" (
"collection" TEXT NOT NULL,
"key" TEXT NOT NULL,
"value" BLOB NOT NULL
);
INSERT INTO "model_SignalRecipient" (
"id", "recordType", "uniqueId", "recipientPhoneNumber", "recipientUUID", "pni", "devices"
) VALUES
(1, 0, '', '+17635550100', '00000000-0000-4000-A000-000000000000', NULL, X''),
(2, 0, '', '+17635550101', NULL, NULL, X''),
(3, 0, '', NULL, NULL, 'PNI:00000000-0000-4000-A000-000000000FFF', X'');
INSERT INTO "keyvalue" (
"collection", "key", "value"
) VALUES
('kOWSProfileManager_UserWhitelistCollection', '+17635550100', X''),
('kOWSProfileManager_UserWhitelistCollection', '+17635550102', X''),
('kOWSProfileManager_UserUUIDWhitelistCollection', '00000000-0000-4000-A000-000000000000', X''),
('kOWSProfileManager_UserUUIDWhitelistCollection', '00000000-0000-4000-A000-000000000001', X''),
('kOWSProfileManager_UserUUIDWhitelistCollection', 'PNI:00000000-0000-4000-A000-000000000FFF', X''),
('kOWSProfileManager_UserUUIDWhitelistCollection', 'PNI:00000000-0000-4000-A000-000000000FFE', X'');
""")
do {
let tx = DBWriteTransaction(database: db)
defer { tx.finalizeTransaction() }
try GRDBSchemaMigrator.addRecipientStatus(tx: tx)
try GRDBSchemaMigrator.migrateRecipientWhitelist(tx: tx)
}
let recipients = try Row.fetchAll(db, sql: "SELECT * FROM model_SignalRecipient ORDER BY id")
XCTAssertEqual(recipients.count, 6)
XCTAssertEqual(recipients[0]["status"] as Int64, 1)
XCTAssertEqual(recipients[1]["status"] as Int64, 0)
XCTAssertEqual(recipients[2]["status"] as Int64, 1)
XCTAssertEqual(recipients[3]["status"] as Int64, 1)
XCTAssertEqual(recipients[4]["status"] as Int64, 1)
XCTAssertEqual(recipients[5]["status"] as Int64, 1)
}
}
}

View File

@ -34,13 +34,11 @@ class StoryManagerTest: SSKBaseTest {
profileManager.fakeUserProfiles = [
SignalServiceAddress(author): OWSUserProfile(address: .otherUser(SignalServiceAddress(author))),
]
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
@ -92,12 +90,12 @@ class StoryManagerTest: SSKBaseTest {
let privateStoryMessage = try Self.makePrivateStory()
let groupStoryMessage = try Self.makeGroupStory()
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
SSKEnvironment.shared.blockingManagerRef.addBlockedAddress(
SignalServiceAddress(author),
@ -140,12 +138,12 @@ class StoryManagerTest: SSKBaseTest {
let groupMasterKey = try GroupMasterKey(contents: storyMessage.group!.masterKey!)
let groupId = try GroupSecretParams.deriveFromMasterKey(groupMasterKey: groupMasterKey).getPublicParams().getGroupIdentifier().serialize()
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
TSGroupThread.forUnitTest(
masterKey: groupMasterKey,
@ -182,12 +180,12 @@ class StoryManagerTest: SSKBaseTest {
let secretParams = try GroupV2ContextInfo.deriveFrom(masterKeyData: storyMessage.group!.masterKey!).groupSecretParams
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try Self.makeGroupThread(secretParams: secretParams, transaction: $0)
@ -217,12 +215,12 @@ class StoryManagerTest: SSKBaseTest {
let secretParams = try GroupV2ContextInfo.deriveFrom(masterKeyData: storyMessage.group!.masterKey!).groupSecretParams
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try Self.makeGroupThread(secretParams: secretParams, announcementOnly: true, members: [author], transaction: $0)
@ -256,13 +254,11 @@ class StoryManagerTest: SSKBaseTest {
profileManager.fakeUserProfiles = [
SignalServiceAddress(author): OWSUserProfile(address: .otherUser(SignalServiceAddress(author))),
]
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try Self.makeGroupThread(secretParams: secretParams, announcementOnly: true, admins: [author], transaction: $0)
@ -296,13 +292,11 @@ class StoryManagerTest: SSKBaseTest {
profileManager.fakeUserProfiles = [
SignalServiceAddress(author): OWSUserProfile(address: .otherUser(SignalServiceAddress(author))),
]
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try Self.makeGroupThread(secretParams: secretParams, members: [author], transaction: $0)
@ -334,13 +328,11 @@ class StoryManagerTest: SSKBaseTest {
profileManager.fakeUserProfiles = [
SignalServiceAddress(author): OWSUserProfile(address: .otherUser(SignalServiceAddress(author))),
]
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,
@ -365,6 +357,9 @@ class StoryManagerTest: SSKBaseTest {
let author = Aci.randomForTesting()
let storyMessage = try Self.makePrivateStory()
let profileManager = SSKEnvironment.shared.profileManagerRef
let recipientFetcher = DependenciesBridge.shared.recipientFetcher
try write {
try StoryMessage.create(
withIncomingStoryMessage: storyMessage,
@ -374,11 +369,8 @@ class StoryManagerTest: SSKBaseTest {
transaction: $0,
)
SSKEnvironment.shared.profileManagerRef.addUser(
toProfileWhitelist: SignalServiceAddress(author),
userProfileWriter: .localUser,
transaction: $0,
)
var recipient = recipientFetcher.fetchOrCreate(serviceId: author, tx: $0)
profileManager.addRecipientToProfileWhitelist(&recipient, userProfileWriter: .localUser, tx: $0)
try StoryManager.processIncomingStoryMessage(
storyMessage,