diff --git a/NotificationServiceExtension/Info.plist b/NotificationServiceExtension/Info.plist
index 60a67f4a64..916ce8418c 100644
--- a/NotificationServiceExtension/Info.plist
+++ b/NotificationServiceExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
3.8.0
CFBundleVersion
- 3.8.0.15
+ 3.8.0.19
NSAppTransportSecurity
NSExceptionDomains
diff --git a/Pods b/Pods
index 311cd60862..21e3b748d4 160000
--- a/Pods
+++ b/Pods
@@ -1 +1 @@
-Subproject commit 311cd6086281c648c095adfc858d515a03bd8131
+Subproject commit 21e3b748d44b7afe23928e11f3e605f0bed7fe51
diff --git a/Signal.xcodeproj/project.pbxproj b/Signal.xcodeproj/project.pbxproj
index 556b5f895b..8da6451f04 100644
--- a/Signal.xcodeproj/project.pbxproj
+++ b/Signal.xcodeproj/project.pbxproj
@@ -132,7 +132,6 @@
346129AF1FD1F5D900532771 /* SystemContactsFetcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129AE1FD1F5D900532771 /* SystemContactsFetcher.swift */; };
346129B41FD1F7E800532771 /* OWSProfileManager.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129B11FD1F7E800532771 /* OWSProfileManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
346129B51FD1F7E800532771 /* OWSProfileManager.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129B21FD1F7E800532771 /* OWSProfileManager.m */; };
- 346129B61FD1F7E800532771 /* ProfileFetcherJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346129B31FD1F7E800532771 /* ProfileFetcherJob.swift */; };
346129D51FD20ADC00532771 /* UIViewController+OWS.m in Sources */ = {isa = PBXBuildFile; fileRef = 346129D31FD20ADB00532771 /* UIViewController+OWS.m */; };
346129D61FD20ADC00532771 /* UIViewController+OWS.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129D41FD20ADC00532771 /* UIViewController+OWS.h */; settings = {ATTRIBUTES = (Public, ); }; };
346129E21FD5C0BE00532771 /* VersionMigrations.h in Headers */ = {isa = PBXBuildFile; fileRef = 346129E01FD5C0BE00532771 /* VersionMigrations.h */; settings = {ATTRIBUTES = (Public, ); }; };
@@ -177,7 +176,7 @@
346E9D5421B040B700562252 /* RegistrationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346E9D5321B040B600562252 /* RegistrationController.swift */; };
3470249C238367EA0078D72C /* TextApprovalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470249B238367E90078D72C /* TextApprovalViewController.swift */; };
3470249E2385B6360078D72C /* OWSProfileManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470249D2385B6360078D72C /* OWSProfileManager.swift */; };
- 347024A0238C85850078D72C /* VersionedProfiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470249F238C85850078D72C /* VersionedProfiles.swift */; };
+ 347024A0238C85850078D72C /* VersionedProfilesImpl.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3470249F238C85850078D72C /* VersionedProfilesImpl.swift */; };
347137FC22A1BCE800F43A63 /* OWSMessageViewOnceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 347137FB22A1BCE800F43A63 /* OWSMessageViewOnceView.m */; };
347191F923F457BD003A3106 /* GroupsV2AvatarDownloadOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 347191F823F457BD003A3106 /* GroupsV2AvatarDownloadOperation.swift */; };
3476236224081F8900D4FB6F /* GroupManager+SignalMessaging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3476236124081F8900D4FB6F /* GroupManager+SignalMessaging.swift */; };
@@ -1006,7 +1005,6 @@
346129AE1FD1F5D900532771 /* SystemContactsFetcher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SystemContactsFetcher.swift; sourceTree = ""; };
346129B11FD1F7E800532771 /* OWSProfileManager.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProfileManager.h; sourceTree = ""; };
346129B21FD1F7E800532771 /* OWSProfileManager.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProfileManager.m; sourceTree = ""; };
- 346129B31FD1F7E800532771 /* ProfileFetcherJob.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ProfileFetcherJob.swift; sourceTree = ""; };
346129BD1FD2068600532771 /* ThreadUtil.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ThreadUtil.h; sourceTree = ""; };
346129BE1FD2068600532771 /* ThreadUtil.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ThreadUtil.m; sourceTree = ""; };
346129D31FD20ADB00532771 /* UIViewController+OWS.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "UIViewController+OWS.m"; sourceTree = ""; };
@@ -1056,7 +1054,7 @@
346E9D5321B040B600562252 /* RegistrationController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RegistrationController.swift; sourceTree = ""; };
3470249B238367E90078D72C /* TextApprovalViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TextApprovalViewController.swift; sourceTree = ""; };
3470249D2385B6360078D72C /* OWSProfileManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OWSProfileManager.swift; sourceTree = ""; };
- 3470249F238C85850078D72C /* VersionedProfiles.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionedProfiles.swift; sourceTree = ""; };
+ 3470249F238C85850078D72C /* VersionedProfilesImpl.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = VersionedProfilesImpl.swift; sourceTree = ""; };
347137FA22A1BCE800F43A63 /* OWSMessageViewOnceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageViewOnceView.h; sourceTree = ""; };
347137FB22A1BCE800F43A63 /* OWSMessageViewOnceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageViewOnceView.m; sourceTree = ""; };
347191F823F457BD003A3106 /* GroupsV2AvatarDownloadOperation.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GroupsV2AvatarDownloadOperation.swift; sourceTree = ""; };
@@ -2232,8 +2230,7 @@
346129B11FD1F7E800532771 /* OWSProfileManager.h */,
346129B21FD1F7E800532771 /* OWSProfileManager.m */,
3470249D2385B6360078D72C /* OWSProfileManager.swift */,
- 346129B31FD1F7E800532771 /* ProfileFetcherJob.swift */,
- 3470249F238C85850078D72C /* VersionedProfiles.swift */,
+ 3470249F238C85850078D72C /* VersionedProfilesImpl.swift */,
);
path = profiles;
sourceTree = "";
@@ -2747,12 +2744,12 @@
454A96571FD600B4008D2A0E /* attachments */ = {
isa = PBXGroup;
children = (
+ 8809CE8422F8DB2D00D38867 /* AttachmentKeyboard.swift */,
34B3F8391E8DF1700035BE1A /* AttachmentSharing.h */,
34B3F83A1E8DF1700035BE1A /* AttachmentSharing.m */,
- 34D913491F62D4A500722898 /* SignalAttachment.swift */,
- 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */,
45F59A092029140500E8D2B0 /* OWSVideoPlayer.swift */,
- 8809CE8422F8DB2D00D38867 /* AttachmentKeyboard.swift */,
+ 45BC829C1FD9C4B400011CF3 /* ShareViewDelegate.swift */,
+ 34D913491F62D4A500722898 /* SignalAttachment.swift */,
);
path = attachments;
sourceTree = "";
@@ -2762,9 +2759,9 @@
children = (
451166BF1FD86B98000739BA /* AccountManager.swift */,
45DF5DF11DDB843F00C936C7 /* CompareSafetyNumbersActivity.swift */,
+ 4CB5F26820F7D060004D1B42 /* MessageActions.swift */,
458E38351D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.h */,
458E38361D668EBF0094BD24 /* OWSDeviceProvisioningURLParser.m */,
- 4CB5F26820F7D060004D1B42 /* MessageActions.swift */,
4C5250D121E7BD7D00CE3D95 /* PhoneNumberValidator.swift */,
4C0C36F7226647FE0083F19A /* ThreadMapping.swift */,
);
@@ -4388,7 +4385,6 @@
34AC0A10211B39EA00997B47 /* TappableView.swift in Sources */,
346129F91FD5F31400532771 /* OWS104CreateRecipientIdentities.m in Sources */,
34C2EEAC2270A69400BCA1D0 /* CircleView.swift in Sources */,
- 346129B61FD1F7E800532771 /* ProfileFetcherJob.swift in Sources */,
34AC09E9211B39B100997B47 /* OWSTableViewController.m in Sources */,
340872CE2239596100CB25B0 /* AttachmentApprovalInputAccessoryView.swift in Sources */,
346129F51FD5F31400532771 /* OWS102MoveLoggingPreferenceToUserDefaults.m in Sources */,
@@ -4460,7 +4456,7 @@
34C529092361D3540009DB2A /* ApprovalFooterView.swift in Sources */,
451F8A471FD715BA005CB9DA /* OWSAvatarBuilder.m in Sources */,
349ED992221EE80D008045B0 /* AppPreferences.swift in Sources */,
- 347024A0238C85850078D72C /* VersionedProfiles.swift in Sources */,
+ 347024A0238C85850078D72C /* VersionedProfilesImpl.swift in Sources */,
4C9C50FE22F36FA50054A33F /* OutboundMessage+OWS.swift in Sources */,
34480B591FD0A7A400BC14EF /* OWSScrubbingLogFormatter.m in Sources */,
342BF24D227A03F8005CEAA9 /* StickerHorizontalListView.swift in Sources */,
diff --git a/Signal/Signal-Info.plist b/Signal/Signal-Info.plist
index ae14803dbd..d6de9cb989 100644
--- a/Signal/Signal-Info.plist
+++ b/Signal/Signal-Info.plist
@@ -45,7 +45,7 @@
CFBundleVersion
- 3.8.0.15
+ 3.8.0.19
ITSAppUsesNonExemptEncryption
LOGS_EMAIL
diff --git a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
index 952fbf2e78..d122bd929a 100644
--- a/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
+++ b/Signal/src/ViewControllers/ConversationView/ConversationViewController.m
@@ -362,6 +362,11 @@ typedef enum : NSUInteger {
return SSKEnvironment.shared.syncManager;
}
+- (BulkProfileFetch *)bulkProfileFetch
+{
+ return SSKEnvironment.shared.bulkProfileFetch;
+}
+
#pragma mark -
- (void)addNotificationListeners
@@ -1202,7 +1207,7 @@ typedef enum : NSUInteger {
// recover status bar when returning from PhotoPicker, which is dark (uses light status bar)
[self setNeedsStatusBarAppearanceUpdate];
- [ProfileFetcherJob fetchAndUpdateProfilesWithThread:self.thread];
+ [self.bulkProfileFetch fetchAndUpdateProfilesWithThread:self.thread];
[self markVisibleMessagesAsRead];
[self startReadTimer];
[self updateNavigationBarSubtitleLabel];
diff --git a/Signal/src/ViewControllers/NewGroupView/BaseGroupMemberViewController.swift b/Signal/src/ViewControllers/NewGroupView/BaseGroupMemberViewController.swift
index f2dd4a1296..8d6c8066cb 100644
--- a/Signal/src/ViewControllers/NewGroupView/BaseGroupMemberViewController.swift
+++ b/Signal/src/ViewControllers/NewGroupView/BaseGroupMemberViewController.swift
@@ -127,6 +127,16 @@ public class BaseGroupMemberViewController: OWSViewController {
autoPinView(toBottomOfViewControllerOrKeyboard: recipientPicker.view, avoidNotch: false)
updateMemberCount()
+ tryToFillInMissingUuuids()
+ }
+
+ private func tryToFillInMissingUuuids() {
+ let addresses = recipientPicker.contactsViewHelper.signalAccounts.map { $0.recipientAddress }
+ firstly {
+ GroupManager.tryToFillInMissingUuuids(for: addresses, isBlocking: false)
+ }.catch { error in
+ owsFailDebug("Error: \(error)")
+ }.retainUntilComplete()
}
@objc
@@ -395,9 +405,12 @@ extension BaseGroupMemberViewController: RecipientPickerDelegate {
RemoteConfig.groupsV2IncomingMessages else {
return
}
- // GroupsV2 TODO: We could debounce this effort.
- if !doesRecipientSupportGroupsV2(recipient) {
- tryToEnableGroupsV2ForAddress(address, ignoreErrors: true).retainUntilComplete()
+ DispatchQueue.global().async {
+ if !self.doesRecipientSupportGroupsV2(recipient) {
+ self.tryToEnableGroupsV2ForAddress(address,
+ isBlocking: false,
+ ignoreErrors: true).retainUntilComplete()
+ }
}
}
@@ -420,7 +433,9 @@ extension BaseGroupMemberViewController: RecipientPickerDelegate {
return AnyPromise(Promise.value(()))
}
let ignoreErrors = !groupMemberViewDelegate.groupMemberViewIsGroupsV2Required()
- return AnyPromise(tryToEnableGroupsV2ForAddress(address, ignoreErrors: ignoreErrors))
+ return AnyPromise(tryToEnableGroupsV2ForAddress(address,
+ isBlocking: true,
+ ignoreErrors: ignoreErrors))
}
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController,
@@ -434,19 +449,31 @@ extension BaseGroupMemberViewController: RecipientPickerDelegate {
owsFailDebug("Invalid recipient.")
return false
}
+ return doesRecipientSupportGroupsV2(address)
+ }
+
+ private func doesRecipientSupportGroupsV2(_ address: SignalServiceAddress) -> Bool {
return databaseStorage.read { transaction in
return GroupManager.doesUserSupportGroupsV2(address: address, transaction: transaction)
}
}
func tryToEnableGroupsV2ForAddress(_ address: SignalServiceAddress,
+ isBlocking: Bool,
ignoreErrors: Bool) -> Promise {
return firstly { () -> Promise in
return GroupManager.tryToEnableGroupsV2(for: [address],
+ isBlocking: isBlocking,
ignoreErrors: ignoreErrors)
- }.done { [weak self] _ in
- // Reload view content.
- self?.recipientPicker.reloadContent()
+ }.done(on: .global() ) { [weak self] _ in
+ // If we succeeded in enable groups v2 for this address,
+ // reload the recipient picker to reflect that.
+ if self?.doesRecipientSupportGroupsV2(address) ?? false {
+ DispatchQueue.main.async {
+ // Reload view content.
+ self?.recipientPicker.reloadContent()
+ }
+ }
}
}
diff --git a/Signal/src/ViewControllers/NewGroupView/NewGroupViewController.m b/Signal/src/ViewControllers/NewGroupView/NewGroupViewController.m
index dfea4b4edc..e10d974e45 100644
--- a/Signal/src/ViewControllers/NewGroupView/NewGroupViewController.m
+++ b/Signal/src/ViewControllers/NewGroupView/NewGroupViewController.m
@@ -445,6 +445,7 @@ NS_ASSUME_NONNULL_BEGIN
- (AnyPromise *)recipientPicker:(RecipientPickerViewController *)recipientPickerViewController
prepareToSelectRecipient:(PickedRecipient *)recipient
{
+ OWSFailDebug(@"This method should not called.");
return [AnyPromise promiseWithValue:@(1)];
}
diff --git a/Signal/src/ViewControllers/RecipientPicker/AddToBlockListViewController.swift b/Signal/src/ViewControllers/RecipientPicker/AddToBlockListViewController.swift
index 3697ddc186..6d68d75097 100644
--- a/Signal/src/ViewControllers/RecipientPicker/AddToBlockListViewController.swift
+++ b/Signal/src/ViewControllers/RecipientPicker/AddToBlockListViewController.swift
@@ -88,6 +88,7 @@ extension AddToBlockListViewController: RecipientPickerDelegate {
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController,
prepareToSelectRecipient recipient: PickedRecipient) -> AnyPromise {
+ owsFailDebug("This method should not called.")
return AnyPromise(Promise.value(()))
}
diff --git a/Signal/src/ViewControllers/RecipientPicker/ComposeViewController.swift b/Signal/src/ViewControllers/RecipientPicker/ComposeViewController.swift
index 1e23a1f46b..bcf1a22758 100644
--- a/Signal/src/ViewControllers/RecipientPicker/ComposeViewController.swift
+++ b/Signal/src/ViewControllers/RecipientPicker/ComposeViewController.swift
@@ -86,6 +86,7 @@ extension ComposeViewController: RecipientPickerDelegate {
func recipientPicker(_ recipientPickerViewController: RecipientPickerViewController,
prepareToSelectRecipient recipient: PickedRecipient) -> AnyPromise {
+ owsFailDebug("This method should not called.")
return AnyPromise(Promise.value(()))
}
diff --git a/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m b/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m
index 4de6d91e60..02a6730859 100644
--- a/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m
+++ b/Signal/src/ViewControllers/ThreadSettings/UpdateGroupViewController.m
@@ -521,6 +521,7 @@ NS_ASSUME_NONNULL_BEGIN
- (AnyPromise *)recipientPicker:(RecipientPickerViewController *)recipientPickerViewController
prepareToSelectRecipient:(PickedRecipient *)recipient
{
+ OWSFailDebug(@"This method should not called.");
return [AnyPromise promiseWithValue:@(1)];
}
diff --git a/SignalMessaging/categories/UIDevice+featureSupport.swift b/SignalMessaging/categories/UIDevice+featureSupport.swift
index c02fcbfcad..3c73579ee1 100644
--- a/SignalMessaging/categories/UIDevice+featureSupport.swift
+++ b/SignalMessaging/categories/UIDevice+featureSupport.swift
@@ -58,7 +58,7 @@ public extension UIDevice {
return false
case 1792:
// iPhone XR
- return false
+ return true
case 1920, 2208:
// iPhone 6+/6S+/7+/8+//
return true
diff --git a/SignalMessaging/environment/AppSetup.m b/SignalMessaging/environment/AppSetup.m
index abde31502a..1757dc84a4 100644
--- a/SignalMessaging/environment/AppSetup.m
+++ b/SignalMessaging/environment/AppSetup.m
@@ -113,6 +113,9 @@ NS_ASSUME_NONNULL_BEGIN
OWSWindowManager *windowManager = [[OWSWindowManager alloc] initDefault];
MessageProcessing *messageProcessing = [MessageProcessing new];
MessageFetcherJob *messageFetcherJob = [MessageFetcherJob new];
+ BulkProfileFetch *bulkProfileFetch = [BulkProfileFetch new];
+ BulkUUIDLookup *bulkUUIDLookup = [BulkUUIDLookup new];
+ id versionedProfiles = [VersionedProfilesImpl new];
[Environment setShared:[[Environment alloc] initWithAudioSession:audioSession
incomingContactSyncJobQueue:incomingContactSyncJobQueue
@@ -167,7 +170,10 @@ NS_ASSUME_NONNULL_BEGIN
groupsV2:groupsV2
groupV2Updates:groupV2Updates
messageProcessing:messageProcessing
- messageFetcherJob:messageFetcherJob]];
+ messageFetcherJob:messageFetcherJob
+ bulkProfileFetch:bulkProfileFetch
+ bulkUUIDLookup:bulkUUIDLookup
+ versionedProfiles:versionedProfiles]];
appSpecificSingletonBlock();
diff --git a/SignalMessaging/environment/migrations/YDBToGRDBMigration.swift b/SignalMessaging/environment/migrations/YDBToGRDBMigration.swift
index 81ef5987ec..d7e8e4f148 100644
--- a/SignalMessaging/environment/migrations/YDBToGRDBMigration.swift
+++ b/SignalMessaging/environment/migrations/YDBToGRDBMigration.swift
@@ -299,7 +299,7 @@ extension YDBToGRDBMigration {
GRDBKeyValueStoreMigrator(label: "OWSSyncManager", keyStore: OWSSyncManager.keyValueStore(), ydbTransaction: ydbTransaction),
GRDBKeyValueStoreMigrator(label: "OWSOutgoingReceiptManager.deliveryReceiptStore", keyStore: OWSOutgoingReceiptManager.deliveryReceiptStore(), ydbTransaction: ydbTransaction),
GRDBKeyValueStoreMigrator(label: "OWSOutgoingReceiptManager.readReceiptStore", keyStore: OWSOutgoingReceiptManager.readReceiptStore(), ydbTransaction: ydbTransaction),
- GRDBKeyValueStoreMigrator(label: "VersionedProfiles.credentialStore", keyStore: VersionedProfiles.credentialStore, ydbTransaction: ydbTransaction),
+ GRDBKeyValueStoreMigrator(label: "VersionedProfiles.credentialStore", keyStore: VersionedProfilesImpl.credentialStore, ydbTransaction: ydbTransaction),
GRDBKeyValueStoreMigrator(label: "KeyBackupService.keyValueStore", keyStore: KeyBackupService.keyValueStore, ydbTransaction: ydbTransaction),
GRDBKeyValueStoreMigrator(label: "KeyBackupService.tokenStore", keyStore: KeyBackupService.tokenStore, ydbTransaction: ydbTransaction),
GRDBKeyValueStoreMigrator(label: "StorageServiceOperation", keyStore: StorageServiceOperation.keyValueStore, ydbTransaction: ydbTransaction)
diff --git a/SignalMessaging/groups/GroupsV2Impl.swift b/SignalMessaging/groups/GroupsV2Impl.swift
index aee60f63b8..5f5b02a23b 100644
--- a/SignalMessaging/groups/GroupsV2Impl.swift
+++ b/SignalMessaging/groups/GroupsV2Impl.swift
@@ -45,8 +45,8 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
return SSKEnvironment.shared.contactsUpdater
}
- private var reachabilityManager: SSKReachabilityManager {
- return SSKEnvironment.shared.reachabilityManager
+ private var versionedProfiles: VersionedProfilesImpl {
+ return SSKEnvironment.shared.versionedProfiles as! VersionedProfilesImpl
}
// MARK: -
@@ -114,7 +114,7 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
databaseStorage.write { transaction in
self.clearTemporalCredentials(transaction: transaction)
- VersionedProfiles.clearProfileKeyCredentials(transaction: transaction)
+ self.versionedProfiles.clearProfileKeyCredentials(transaction: transaction)
self.serviceStore.setString(serverPublicParamsBase64, key: lastServerPublicParamsKey, transaction: transaction)
self.serviceStore.setInt(zkgroupVersionCounter, key: lastZKgroupVersionCounterKey, transaction: transaction)
}
@@ -933,8 +933,8 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
for uuid in Set(uuids) {
do {
let address = SignalServiceAddress(uuid: uuid)
- if let credential = try VersionedProfiles.profileKeyCredential(for: address,
- transaction: transaction) {
+ if let credential = try self.versionedProfiles.profileKeyCredential(for: address,
+ transaction: transaction) {
credentialMap[uuid] = credential
continue
}
@@ -977,9 +977,9 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
try self.databaseStorage.read { transaction in
for uuid in uuids {
let address = SignalServiceAddress(uuid: uuid)
- guard let credential = try VersionedProfiles.profileKeyCredential(for: address,
- transaction: transaction) else {
- throw OWSAssertionError("Could load credential.")
+ guard let credential = try self.versionedProfiles.profileKeyCredential(for: address,
+ transaction: transaction) else {
+ throw OWSAssertionError("Could load credential.")
}
credentialMap[uuid] = credential
}
@@ -992,8 +992,8 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
public func hasProfileKeyCredential(for address: SignalServiceAddress,
transaction: SDSAnyReadTransaction) -> Bool {
do {
- return try VersionedProfiles.profileKeyCredential(for: address,
- transaction: transaction) != nil
+ return try self.versionedProfiles.profileKeyCredential(for: address,
+ transaction: transaction) != nil
} catch {
owsFailDebug("Error: \(error)")
return false
@@ -1049,25 +1049,6 @@ public class GroupsV2Impl: NSObject, GroupsV2Swift {
return when(fulfilled: promises).asVoid()
}
- // MARK: - UUIDs
-
- public func tryToEnsureUuidsForGroupMembers(for addresses: [SignalServiceAddress]) -> Promise {
- guard FeatureFlags.useOnlyModernContactDiscovery else {
- // Can't fill in UUIDs using legacy contact intersections.
- return Promise.value(())
- }
-
- let phoneNumbersWithoutUuids = addresses.filter { $0.uuid == nil }.compactMap { $0.phoneNumber }
- guard phoneNumbersWithoutUuids.count > 0 else {
- return Promise.value(())
- }
-
- return firstly {
- contactsUpdater.lookupIdentifiersPromise(phoneNumbers:
- phoneNumbersWithoutUuids)
- }.asVoid()
- }
-
// MARK: - Auth Credentials
private let authCredentialStore = SDSKeyValueStore(collection: "GroupsV2Impl.authCredentialStoreStore")
diff --git a/SignalMessaging/profiles/OWSProfileManager.h b/SignalMessaging/profiles/OWSProfileManager.h
index 6ef06722e6..c1924eaeac 100644
--- a/SignalMessaging/profiles/OWSProfileManager.h
+++ b/SignalMessaging/profiles/OWSProfileManager.h
@@ -140,12 +140,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
- (nullable NSString *)usernameForAddress:(SignalServiceAddress *)address
transaction:(SDSAnyReadTransaction *)transaction;
-- (void)updateProfileForAddress:(SignalServiceAddress *)address
- profileNameEncrypted:(nullable NSData *)profileNameEncrypted
- username:(nullable NSString *)username
- isUuidCapable:(BOOL)isUuidCapable
- avatarUrlPath:(nullable NSString *)avatarUrlPath;
-
#pragma mark - Clean Up
+ (NSSet *)allProfileAvatarFilePathsWithTransaction:(SDSAnyReadTransaction *)transaction;
diff --git a/SignalMessaging/profiles/OWSProfileManager.m b/SignalMessaging/profiles/OWSProfileManager.m
index 927738ec2f..81d3a9df9b 100644
--- a/SignalMessaging/profiles/OWSProfileManager.m
+++ b/SignalMessaging/profiles/OWSProfileManager.m
@@ -77,6 +77,11 @@ const NSString *kNSNotificationKey_WasLocallyInitiated = @"kNSNotificationKey_Wa
return SSKEnvironment.shared.storageServiceManager;
}
+- (id)versionedProfiles
+{
+ return SSKEnvironment.shared.versionedProfiles;
+}
+
#pragma mark -
@synthesize localUserProfile = _localUserProfile;
@@ -449,6 +454,15 @@ const NSString *kNSNotificationKey_WasLocallyInitiated = @"kNSNotificationKey_Wa
return [ProfileFetcherJob fetchAndUpdateProfilePromiseObjcWithAddress:address mainAppOnly:NO ignoreThrottling:YES];
}
+- (AnyPromise *)updateProfileForAddressPromise:(SignalServiceAddress *)address
+ mainAppOnly:(BOOL)mainAppOnly
+ ignoreThrottling:(BOOL)ignoreThrottling
+{
+ return [ProfileFetcherJob fetchAndUpdateProfilePromiseObjcWithAddress:address
+ mainAppOnly:mainAppOnly
+ ignoreThrottling:ignoreThrottling];
+}
+
- (void)fetchAndUpdateProfileForUsername:(NSString *)username
success:(void (^)(SignalServiceAddress *))success
notFound:(void (^)(void))notFound
@@ -656,7 +670,7 @@ const NSString *kNSNotificationKey_WasLocallyInitiated = @"kNSNotificationKey_Wa
// Whenever a user's profile key changes, we need to fetch a new
// profile key credential for them.
- [VersionedProfiles clearProfileKeyCredentialForAddress:localAddress transaction:transaction];
+ [self.versionedProfiles clearProfileKeyCredentialForAddress:localAddress transaction:transaction];
// We schedule the updates here but process them below using processProfileKeyUpdates.
// It's more efficient to process them after the intermediary steps are done.
@@ -1382,7 +1396,7 @@ const NSString *kNSNotificationKey_WasLocallyInitiated = @"kNSNotificationKey_Wa
// Whenever a user's profile key changes, we need to fetch a new
// profile key credential for them.
- [VersionedProfiles clearProfileKeyCredentialForAddress:address transaction:transaction];
+ [self.versionedProfiles clearProfileKeyCredentialForAddress:address transaction:transaction];
[userProfile clearWithProfileKey:profileKey
wasLocallyInitiated:wasLocallyInitiated
diff --git a/SignalMessaging/profiles/OWSProfileManager.swift b/SignalMessaging/profiles/OWSProfileManager.swift
index f7899d006b..62965f92d1 100644
--- a/SignalMessaging/profiles/OWSProfileManager.swift
+++ b/SignalMessaging/profiles/OWSProfileManager.swift
@@ -8,6 +8,14 @@ import SignalServiceKit
public extension OWSProfileManager {
+ // MARK: - Dependencies
+
+ private class var versionedProfiles: VersionedProfilesSwift {
+ return SSKEnvironment.shared.versionedProfiles as! VersionedProfilesSwift
+ }
+
+ // MARK: -
+
// The main entry point for updating the local profile. It will:
//
// * Update local state optimistically.
@@ -22,15 +30,15 @@ public extension OWSProfileManager {
return DispatchQueue.global().async(.promise) {
return enqueueProfileUpdate(profileGivenName: profileGivenName, profileFamilyName: profileFamilyName, profileAvatarData: profileAvatarData)
- }.then { update in
- return self.attemptToUpdateProfileOnService(update: update)
- }.then { (_) throws -> Promise in
- guard let localAddress = TSAccountManager.sharedInstance().localAddress else {
- throw OWSAssertionError("missing local address")
- }
- return ProfileFetcherJob.fetchAndUpdateProfilePromise(address: localAddress, mainAppOnly: false, ignoreThrottling: true, fetchType: .default).asVoid()
- }.done(on: .global()) { () -> Void in
- Logger.verbose("Profile update did complete.")
+ }.then { update in
+ return self.attemptToUpdateProfileOnService(update: update)
+ }.then { (_) throws -> Promise in
+ guard let localAddress = TSAccountManager.sharedInstance().localAddress else {
+ throw OWSAssertionError("missing local address")
+ }
+ return ProfileFetcherJob.fetchAndUpdateProfilePromise(address: localAddress, mainAppOnly: false, ignoreThrottling: true, fetchType: .default).asVoid()
+ }.done(on: .global()) { () -> Void in
+ Logger.verbose("Profile update did complete.")
}
}
@@ -109,13 +117,14 @@ extension OWSProfileManager {
guard let update = pendingUpdate else {
return
}
- attemptToUpdateProfileOnService(update: update,
- retryDelay: retryDelay)
- .done { _ in
- Logger.info("Update succeeded.")
- }.catch { error in
- Logger.error("Update failed: \(error)")
- }.retainUntilComplete()
+ firstly {
+ attemptToUpdateProfileOnService(update: update,
+ retryDelay: retryDelay)
+ }.done { _ in
+ Logger.info("Update succeeded.")
+ }.catch { error in
+ Logger.error("Update failed: \(error)")
+ }.retainUntilComplete()
}
fileprivate class func attemptToUpdateProfileOnService(update: PendingProfileUpdate,
@@ -133,60 +142,61 @@ extension OWSProfileManager {
let attempt = ProfileUpdateAttempt(update: update,
userProfile: userProfile)
- let promise = writeProfileAvatarToDisk(attempt: attempt)
- .then(on: DispatchQueue.global()) { () -> Promise in
- // Optimistically update local profile state.
- databaseStorage.write { transaction in
- self.updateLocalProfile(with: attempt, transaction: transaction)
- }
-
- if FeatureFlags.versionedProfiledUpdate {
- return updateProfileOnServiceVersioned(attempt: attempt)
- } else {
- return updateProfileOnServiceUnversioned(attempt: attempt)
- }
- }.done(on: DispatchQueue.global()) { _ in
- _ = self.databaseStorage.write { (transaction: SDSAnyWriteTransaction) -> Void in
- guard tryToDequeueProfileUpdate(update: attempt.update, transaction: transaction) else {
- return
- }
-
- if attempt.update.profileAvatarData != nil {
- if attempt.avatarFilename == nil {
- owsFailDebug("Missing avatarFilename.")
- }
- if attempt.avatarUrlPath == nil {
- owsFailDebug("Missing avatarUrlPath.")
- }
- }
-
- self.updateLocalProfile(with: attempt, transaction: transaction)
- }
-
- self.attemptDidComplete(retryDelay: retryDelay, didSucceed: true)
- }.recover(on: .global()) { error in
- // We retry network errors forever (with exponential backoff).
- // Other errors cause us to give up immediately.
- // Note that we only ever retry the latest profile update.
- if IsNetworkConnectivityFailure(error) {
- Logger.warn("Retrying after error: \(error)")
- } else {
- owsFailDebug("Error: \(error)")
-
- // Dequeue to avoid getting stuck in retry loop.
- self.databaseStorage.write { transaction in
- _ = tryToDequeueProfileUpdate(update: attempt.update, transaction: transaction)
- }
- }
- self.attemptDidComplete(retryDelay: retryDelay, didSucceed: false)
-
- // We don't actually want to recover; in this block we
- // handle the business logic consequences of the error,
- // but we re-throw so that the UI can distinguish
- // success and failure.
- throw error
+ let promise = firstly {
+ writeProfileAvatarToDisk(attempt: attempt)
+ }.then(on: DispatchQueue.global()) { () -> Promise in
+ // Optimistically update local profile state.
+ databaseStorage.write { transaction in
+ self.updateLocalProfile(with: attempt, transaction: transaction)
}
+ if FeatureFlags.versionedProfiledUpdate {
+ return updateProfileOnServiceVersioned(attempt: attempt)
+ } else {
+ return updateProfileOnServiceUnversioned(attempt: attempt)
+ }
+ }.done(on: DispatchQueue.global()) { _ in
+ _ = self.databaseStorage.write { (transaction: SDSAnyWriteTransaction) -> Void in
+ guard tryToDequeueProfileUpdate(update: attempt.update, transaction: transaction) else {
+ return
+ }
+
+ if attempt.update.profileAvatarData != nil {
+ if attempt.avatarFilename == nil {
+ owsFailDebug("Missing avatarFilename.")
+ }
+ if attempt.avatarUrlPath == nil {
+ owsFailDebug("Missing avatarUrlPath.")
+ }
+ }
+
+ self.updateLocalProfile(with: attempt, transaction: transaction)
+ }
+
+ self.attemptDidComplete(retryDelay: retryDelay, didSucceed: true)
+ }.recover(on: .global()) { error in
+ // We retry network errors forever (with exponential backoff).
+ // Other errors cause us to give up immediately.
+ // Note that we only ever retry the latest profile update.
+ if IsNetworkConnectivityFailure(error) {
+ Logger.warn("Retrying after error: \(error)")
+ } else {
+ owsFailDebug("Error: \(error)")
+
+ // Dequeue to avoid getting stuck in retry loop.
+ self.databaseStorage.write { transaction in
+ _ = tryToDequeueProfileUpdate(update: attempt.update, transaction: transaction)
+ }
+ }
+ self.attemptDidComplete(retryDelay: retryDelay, didSucceed: false)
+
+ // We don't actually want to recover; in this block we
+ // handle the business logic consequences of the error,
+ // but we re-throw so that the UI can distinguish
+ // success and failure.
+ throw error
+ }
+
return promise
}
@@ -277,9 +287,9 @@ extension OWSProfileManager {
}
private class func updateProfileOnServiceVersioned(attempt: ProfileUpdateAttempt) -> Promise {
- return VersionedProfiles.updateProfilePromise(profileGivenName: attempt.update.profileGivenName,
- profileFamilyName: attempt.update.profileFamilyName,
- profileAvatarData: attempt.update.profileAvatarData)
+ return self.versionedProfiles.updateProfilePromise(profileGivenName: attempt.update.profileGivenName,
+ profileFamilyName: attempt.update.profileFamilyName,
+ profileAvatarData: attempt.update.profileAvatarData)
.map(on: .global()) { versionedUpdate in
attempt.avatarUrlPath = versionedUpdate.avatarUrlPath
}
@@ -290,11 +300,11 @@ extension OWSProfileManager {
Logger.verbose("profile givenName: \(String(describing: attempt.update.profileGivenName)), familyName: \(String(describing: attempt.update.profileFamilyName)), avatarFilename: \(String(describing: attempt.avatarFilename))")
attempt.userProfile.updateWith(givenName: attempt.update.profileGivenName,
- familyName: attempt.update.profileFamilyName,
- avatarUrlPath: attempt.avatarUrlPath,
- avatarFileName: attempt.avatarFilename,
- transaction: transaction,
- completion: nil)
+ familyName: attempt.update.profileFamilyName,
+ avatarUrlPath: attempt.avatarUrlPath,
+ avatarFileName: attempt.avatarFilename,
+ transaction: transaction,
+ completion: nil)
}
// MARK: - Update Queue
@@ -383,8 +393,8 @@ class PendingProfileUpdate: NSObject, NSCoding {
public required init?(coder aDecoder: NSCoder) {
guard let idString = aDecoder.decodeObject(forKey: "id") as? String,
let id = UUID(uuidString: idString) else {
- owsFailDebug("Missing id")
- return nil
+ owsFailDebug("Missing id")
+ return nil
}
self.id = id
self.profileGivenName = aDecoder.decodeObject(forKey: "profileGivenName") as? String
@@ -442,8 +452,8 @@ public extension OWSProfileManager {
// Given name is required
guard let givenNameData = nameSegments[safe: 0],
let givenName = String(data: givenNameData, encoding: .utf8), !givenName.isEmpty else {
- owsFailDebug("unexpectedly missing first name")
- return nil
+ owsFailDebug("unexpectedly missing first name")
+ return nil
}
// Family name is optional
diff --git a/SignalMessaging/profiles/VersionedProfiles.swift b/SignalMessaging/profiles/VersionedProfilesImpl.swift
similarity index 72%
rename from SignalMessaging/profiles/VersionedProfiles.swift
rename to SignalMessaging/profiles/VersionedProfilesImpl.swift
index 9904fcfed4..69b3a79124 100644
--- a/SignalMessaging/profiles/VersionedProfiles.swift
+++ b/SignalMessaging/profiles/VersionedProfilesImpl.swift
@@ -8,44 +8,37 @@ import SignalServiceKit
import SignalMetadataKit
import ZKGroup
-@objc
-public class VersionedProfileUpdate: NSObject {
- // This will only be set if there is a profile avatar.
- @objc
- public let avatarUrlPath: String?
+public class VersionedProfileRequestImpl: NSObject, VersionedProfileRequest {
+ public let request: TSRequest
+ public let requestContext: ProfileKeyCredentialRequestContext?
- required init(avatarUrlPath: String? = nil) {
- self.avatarUrlPath = avatarUrlPath
+ public required init(request: TSRequest,
+ requestContext: ProfileKeyCredentialRequestContext?) {
+ self.request = request
+ self.requestContext = requestContext
}
}
// MARK: -
-public struct VersionedProfileRequest {
- public let request: TSRequest
- public let requestContext: ProfileKeyCredentialRequestContext?
-}
-
-// MARK: -
-
@objc
-public class VersionedProfiles: NSObject {
+public class VersionedProfilesImpl: NSObject, VersionedProfilesSwift {
// MARK: - Dependencies
- private class var profileManager: OWSProfileManager {
+ private var profileManager: OWSProfileManager {
return OWSProfileManager.shared()
}
- private class var networkManager: TSNetworkManager {
+ private var networkManager: TSNetworkManager {
return SSKEnvironment.shared.networkManager
}
- private class var databaseStorage: SDSDatabaseStorage {
+ private var databaseStorage: SDSDatabaseStorage {
return SDSDatabaseStorage.shared
}
- private class var tsAccountManager: TSAccountManager {
+ private var tsAccountManager: TSAccountManager {
return .sharedInstance()
}
@@ -53,46 +46,45 @@ public class VersionedProfiles: NSObject {
public static let credentialStore = SDSKeyValueStore(collection: "VersionedProfiles.credentialStore")
- // Never instantiate this class.
- private override init() {}
-
// MARK: -
- public class func clientZkProfileOperations() throws -> ClientZkProfileOperations {
+ public func clientZkProfileOperations() throws -> ClientZkProfileOperations {
return ClientZkProfileOperations(serverPublicParams: try GroupsV2Protos.serverPublicParams())
}
// MARK: - Update
@objc
- public class func updateProfileOnService(profileGivenName: String?,
- profileFamilyName: String?,
- profileAvatarData: Data?) {
- updateProfilePromise(profileGivenName: profileGivenName,
- profileFamilyName: profileFamilyName,
- profileAvatarData: profileAvatarData)
- .done { _ in
- Logger.verbose("success")
+ public func updateProfileOnService(profileGivenName: String?,
+ profileFamilyName: String?,
+ profileAvatarData: Data?) {
+ firstly {
+ updateProfilePromise(profileGivenName: profileGivenName,
+ profileFamilyName: profileFamilyName,
+ profileAvatarData: profileAvatarData)
+ }.done { _ in
+ Logger.verbose("success")
- // TODO: This is temporary for testing.
- let localAddress = TSAccountManager.sharedInstance().localAddress!
+ // TODO: This is temporary for testing.
+ let localAddress = TSAccountManager.sharedInstance().localAddress!
+ firstly {
ProfileFetcherJob.fetchAndUpdateProfilePromise(address: localAddress,
mainAppOnly: false,
ignoreThrottling: true,
fetchType: .versioned)
- .done { _ in
- Logger.verbose("success")
- }.catch { error in
- owsFailDebug("error: \(error)")
- }.retainUntilComplete()
+ }.done { _ in
+ Logger.verbose("success")
}.catch { error in
owsFailDebug("error: \(error)")
}.retainUntilComplete()
+ }.catch { error in
+ owsFailDebug("error: \(error)")
+ }.retainUntilComplete()
}
- public class func updateProfilePromise(profileGivenName: String?,
- profileFamilyName: String?,
- profileAvatarData: Data?) -> Promise {
+ public func updateProfilePromise(profileGivenName: String?,
+ profileFamilyName: String?,
+ profileAvatarData: Data?) -> Promise {
return DispatchQueue.global().async(.promise) {
guard let localUuid = self.tsAccountManager.localUuid else {
@@ -114,7 +106,7 @@ public class VersionedProfiles: NSObject {
guard let encryptedPaddedProfileName = self.profileManager.encrypt(profileNameComponents: nameComponents,
profileKey: profileKey) else {
- throw OWSAssertionError("Could not encrypt profile name.")
+ throw OWSAssertionError("Could not encrypt profile name.")
}
nameData = encryptedPaddedProfileName
}
@@ -128,7 +120,7 @@ public class VersionedProfiles: NSObject {
let profileKey: OWSAES256Key = self.profileManager.localProfileKey()
guard let encryptedProfileAvatarData = self.profileManager.encrypt(profileData: profileAvatarData,
profileKey: profileKey) else {
- throw OWSAssertionError("Could not encrypt profile avatar.")
+ throw OWSAssertionError("Could not encrypt profile avatar.")
}
return self.parseFormAndUpload(formResponseObject: responseObject,
profileAvatarData: encryptedProfileAvatarData)
@@ -137,8 +129,8 @@ public class VersionedProfiles: NSObject {
}
}
- private class func parseFormAndUpload(formResponseObject: Any?,
- profileAvatarData: Data) -> Promise {
+ private func parseFormAndUpload(formResponseObject: Any?,
+ profileAvatarData: Data) -> Promise {
return firstly { () throws -> Promise in
guard let response = formResponseObject as? [AnyHashable: Any] else {
throw OWSAssertionError("Unexpected response.")
@@ -156,8 +148,8 @@ public class VersionedProfiles: NSObject {
// MARK: - Get
- public class func versionedProfileRequest(address: SignalServiceAddress,
- udAccessKey: SMKUDAccessKey?) throws -> VersionedProfileRequest {
+ public func versionedProfileRequest(address: SignalServiceAddress,
+ udAccessKey: SMKUDAccessKey?) throws -> VersionedProfileRequest {
guard address.isValid,
let uuid: UUID = address.uuid else {
throw OWSAssertionError("Invalid address: \(address)")
@@ -193,20 +185,23 @@ public class VersionedProfiles: NSObject {
credentialRequest: credentialRequestArg,
udAccessKey: udAccessKey)
- return VersionedProfileRequest(request: request, requestContext: requestContext)
+ return VersionedProfileRequestImpl(request: request, requestContext: requestContext)
}
// MARK: -
- public class func parseProfileKey(profileKey: OWSAES256Key) throws -> ProfileKey {
+ public func parseProfileKey(profileKey: OWSAES256Key) throws -> ProfileKey {
let profileKeyData: Data = profileKey.keyData
let profileKeyDataBytes = [UInt8](profileKeyData)
return try ProfileKey(contents: profileKeyDataBytes)
}
- public class func didFetchProfile(profile: SignalServiceProfile,
- profileRequest: VersionedProfileRequest) {
+ public func didFetchProfile(profile: SignalServiceProfile,
+ profileRequest: VersionedProfileRequest) {
do {
+ guard let profileRequest = profileRequest as? VersionedProfileRequestImpl else {
+ return
+ }
guard let credentialResponseData = profile.credential else {
return
}
@@ -233,7 +228,7 @@ public class VersionedProfiles: NSObject {
Logger.verbose("Updating credential for: \(uuid)")
databaseStorage.write { transaction in
- credentialStore.setData(credentialData, key: uuid.uuidString, transaction: transaction)
+ Self.credentialStore.setData(credentialData, key: uuid.uuidString, transaction: transaction)
}
} catch {
owsFailDebug("Invalid credential: \(error).")
@@ -243,17 +238,17 @@ public class VersionedProfiles: NSObject {
// MARK: - Credentials
- public class func profileKeyCredentialData(for address: SignalServiceAddress,
- transaction: SDSAnyReadTransaction) throws -> Data? {
+ public func profileKeyCredentialData(for address: SignalServiceAddress,
+ transaction: SDSAnyReadTransaction) throws -> Data? {
guard address.isValid,
let uuid = address.uuid else {
throw OWSAssertionError("Invalid address: \(address)")
}
- return credentialStore.getData(uuid.uuidString, transaction: transaction)
+ return Self.credentialStore.getData(uuid.uuidString, transaction: transaction)
}
- public class func profileKeyCredential(for address: SignalServiceAddress,
- transaction: SDSAnyReadTransaction) throws -> ProfileKeyCredential? {
+ public func profileKeyCredential(for address: SignalServiceAddress,
+ transaction: SDSAnyReadTransaction) throws -> ProfileKeyCredential? {
guard let data = try profileKeyCredentialData(for: address, transaction: transaction) else {
return nil
}
@@ -263,8 +258,8 @@ public class VersionedProfiles: NSObject {
}
@objc(clearProfileKeyCredentialForAddress:transaction:)
- public class func clearProfileKeyCredential(for address: SignalServiceAddress,
- transaction: SDSAnyWriteTransaction) {
+ public func clearProfileKeyCredential(for address: SignalServiceAddress,
+ transaction: SDSAnyWriteTransaction) {
guard address.isValid else {
owsFailDebug("Invalid address: \(address)")
return
@@ -272,11 +267,11 @@ public class VersionedProfiles: NSObject {
guard let uuid = address.uuid else {
return
}
- credentialStore.removeValue(forKey: uuid.uuidString, transaction: transaction)
+ Self.credentialStore.removeValue(forKey: uuid.uuidString, transaction: transaction)
}
- public class func clearProfileKeyCredentials(transaction: SDSAnyWriteTransaction) {
- credentialStore.removeAll(transaction: transaction)
+ public func clearProfileKeyCredentials(transaction: SDSAnyWriteTransaction) {
+ Self.credentialStore.removeAll(transaction: transaction)
}
}
diff --git a/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift b/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift
index 01b7f24532..993cc14321 100644
--- a/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift
+++ b/SignalServiceKit/src/Messages/Stickers/DefaultStickers.swift
@@ -19,6 +19,9 @@ class DefaultStickerPack {
}
private class func parseAll() -> [StickerPackInfo: DefaultStickerPack] {
+ guard FeatureFlags.isUsingProductionService else {
+ return [:]
+ }
let packs = [
// Bandit the Cat
DefaultStickerPack(packIdHex: "9acc9e8aba563d26a4994e69263e3b25", packKeyHex: "5a6dff3948c28efb9b7aaf93ecc375c69fc316e78077ed26867a14d10a0f6a12", shouldAutoInstall: false)!,
diff --git a/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift
index ff70a2dc27..971e55fea8 100644
--- a/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift
+++ b/SignalServiceKit/src/Messages/UD/OWSRequestMaker.swift
@@ -145,6 +145,11 @@ public class RequestMaker: NSObject {
resolver.reject(RequestMakerError.websocketRequestError(statusCode: statusCode, responseData: responseData, underlyingError: error))
})
}.recover { (error: Error) -> Promise in
+ if error.httpStatusCode == 413 {
+ // We've hit rate limit; don't retry.
+ throw error
+ }
+
switch error {
case RequestMakerError.websocketRequestError(let statusCode, _, _):
if isUDRequest && (statusCode == 401 || statusCode == 403) {
@@ -193,6 +198,11 @@ public class RequestMaker: NSObject {
wasSentByUD: isUDRequest,
wasSentByWebsocket: false)
}.recover { (error: Error) -> Promise in
+ if error.httpStatusCode == 413 {
+ // We've hit rate limit; don't retry.
+ throw error
+ }
+
if isUDRequest,
let statusCode = error.httpStatusCode,
statusCode == 401 || statusCode == 403 {
diff --git a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h
index 56686da68f..bd940610ea 100644
--- a/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h
+++ b/SignalServiceKit/src/Protocols/ProfileManagerProtocol.h
@@ -85,6 +85,15 @@ NS_ASSUME_NONNULL_BEGIN
- (void)updateProfileForAddress:(SignalServiceAddress *)address;
- (AnyPromise *)updateProfileForAddressPromise:(SignalServiceAddress *)address;
+- (AnyPromise *)updateProfileForAddressPromise:(SignalServiceAddress *)address
+ mainAppOnly:(BOOL)mainAppOnly
+ ignoreThrottling:(BOOL)ignoreThrottling;
+
+- (void)updateProfileForAddress:(SignalServiceAddress *)address
+ profileNameEncrypted:(nullable NSData *)profileNameEncrypted
+ username:(nullable NSString *)username
+ isUuidCapable:(BOOL)isUuidCapable
+ avatarUrlPath:(nullable NSString *)avatarUrlPath;
- (BOOL)recipientAddressIsUuidCapable:(SignalServiceAddress *)address transaction:(SDSAnyReadTransaction *)transaction;
diff --git a/SignalServiceKit/src/SSKEnvironment.h b/SignalServiceKit/src/SSKEnvironment.h
index b12a4b32a4..7f8be52d06 100644
--- a/SignalServiceKit/src/SSKEnvironment.h
+++ b/SignalServiceKit/src/SSKEnvironment.h
@@ -5,6 +5,8 @@
NS_ASSUME_NONNULL_BEGIN
@class AccountServiceClient;
+@class BulkProfileFetch;
+@class BulkUUIDLookup;
@class ContactsUpdater;
@class GroupsV2MessageProcessor;
@class MessageFetcherJob;
@@ -51,6 +53,7 @@ NS_ASSUME_NONNULL_BEGIN
@protocol GroupsV2;
@protocol GroupV2Updates;
@protocol PendingReadReceiptRecorder;
+@protocol VersionedProfiles;
@interface SSKEnvironment : NSObject
@@ -99,7 +102,10 @@ NS_ASSUME_NONNULL_BEGIN
groupsV2:(id)groupsV2
groupV2Updates:(id)groupV2Updates
messageProcessing:(MessageProcessing *)messageProcessing
- messageFetcherJob:(MessageFetcherJob *)messageFetcherJob NS_DESIGNATED_INITIALIZER;
+ messageFetcherJob:(MessageFetcherJob *)messageFetcherJob
+ bulkProfileFetch:(BulkProfileFetch *)bulkProfileFetch
+ bulkUUIDLookup:(BulkUUIDLookup *)bulkUUIDLookup
+ versionedProfiles:(id)versionedProfiles NS_DESIGNATED_INITIALIZER;
@property (nonatomic, readonly, class) SSKEnvironment *shared;
@@ -148,13 +154,15 @@ NS_ASSUME_NONNULL_BEGIN
@property (nonatomic, readonly) id storageServiceManager;
@property (nonatomic, readonly) id groupsV2;
@property (nonatomic, readonly) id groupV2Updates;
-
@property (nonatomic, readonly) StickerManager *stickerManager;
@property (nonatomic, readonly) SDSDatabaseStorage *databaseStorage;
@property (nonatomic, readonly) StorageCoordinator *storageCoordinator;
@property (nonatomic, readonly) SSKPreferences *sskPreferences;
@property (nonatomic, readonly) MessageProcessing *messageProcessing;
@property (nonatomic, readonly) MessageFetcherJob *messageFetcherJob;
+@property (nonatomic, readonly) BulkProfileFetch *bulkProfileFetch;
+@property (nonatomic, readonly) BulkUUIDLookup *bulkUUIDLookup;
+@property (nonatomic, readonly) id versionedProfiles;
@property (nonatomic, readonly, nullable) OWSPrimaryStorage *primaryStorage;
diff --git a/SignalServiceKit/src/SSKEnvironment.m b/SignalServiceKit/src/SSKEnvironment.m
index 4b73b0214a..8d0df16a57 100644
--- a/SignalServiceKit/src/SSKEnvironment.m
+++ b/SignalServiceKit/src/SSKEnvironment.m
@@ -49,6 +49,9 @@ static SSKEnvironment *sharedSSKEnvironment;
@property (nonatomic) id groupV2Updates;
@property (nonatomic) MessageProcessing *messageProcessing;
@property (nonatomic) MessageFetcherJob *messageFetcherJob;
+@property (nonatomic) BulkProfileFetch *bulkProfileFetch;
+@property (nonatomic) BulkUUIDLookup *bulkUUIDLookup;
+@property (nonatomic) id versionedProfiles;
@end
@@ -103,6 +106,9 @@ static SSKEnvironment *sharedSSKEnvironment;
groupV2Updates:(id)groupV2Updates
messageProcessing:(MessageProcessing *)messageProcessing
messageFetcherJob:(MessageFetcherJob *)messageFetcherJob
+ bulkProfileFetch:(BulkProfileFetch *)bulkProfileFetch
+ bulkUUIDLookup:(BulkUUIDLookup *)bulkUUIDLookup
+ versionedProfiles:(id)versionedProfiles
{
self = [super init];
if (!self) {
@@ -150,7 +156,10 @@ static SSKEnvironment *sharedSSKEnvironment;
OWSAssertDebug(groupsV2);
OWSAssertDebug(groupV2Updates);
OWSAssertDebug(messageProcessing);
- OWSCAssertDebug(messageFetcherJob);
+ OWSAssertDebug(messageFetcherJob);
+ OWSAssertDebug(bulkProfileFetch);
+ OWSAssertDebug(versionedProfiles);
+ OWSAssertDebug(bulkUUIDLookup);
_contactsManager = contactsManager;
_linkPreviewManager = linkPreviewManager;
@@ -195,6 +204,9 @@ static SSKEnvironment *sharedSSKEnvironment;
_groupV2Updates = groupV2Updates;
_messageProcessing = messageProcessing;
_messageFetcherJob = messageFetcherJob;
+ _bulkProfileFetch = bulkProfileFetch;
+ _versionedProfiles = versionedProfiles;
+ _bulkUUIDLookup = bulkUUIDLookup;
return self;
}
diff --git a/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m b/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m
index 49fb9c8165..2c03ee514e 100644
--- a/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m
+++ b/SignalServiceKit/src/TestUtils/MockSSKEnvironment.m
@@ -104,6 +104,9 @@ NS_ASSUME_NONNULL_BEGIN
id groupV2Updates = [[MockGroupV2Updates alloc] init];
MessageProcessing *messageProcessing = [MessageProcessing new];
MessageFetcherJob *messageFetcherJob = [MessageFetcherJob new];
+ BulkProfileFetch *bulkProfileFetch = [BulkProfileFetch new];
+ BulkUUIDLookup *bulkUUIDLookup = [BulkUUIDLookup new];
+ id versionedProfiles = [MockVersionedProfiles new];
self = [super initWithContactsManager:contactsManager
linkPreviewManager:linkPreviewManager
@@ -147,7 +150,10 @@ NS_ASSUME_NONNULL_BEGIN
groupsV2:groupsV2
groupV2Updates:groupV2Updates
messageProcessing:messageProcessing
- messageFetcherJob:messageFetcherJob];
+ messageFetcherJob:messageFetcherJob
+ bulkProfileFetch:bulkProfileFetch
+ bulkUUIDLookup:bulkUUIDLookup
+ versionedProfiles:versionedProfiles];
if (!self) {
return nil;
diff --git a/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m b/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m
index 07c499b392..9c92fe3f39 100644
--- a/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m
+++ b/SignalServiceKit/src/TestUtils/OWSFakeProfileManager.m
@@ -216,6 +216,13 @@ NS_ASSUME_NONNULL_BEGIN
return [AnyPromise promiseWithValue:@(1)];
}
+- (AnyPromise *)updateProfileForAddressPromise:(SignalServiceAddress *)address
+ mainAppOnly:(BOOL)mainAppOnly
+ ignoreThrottling:(BOOL)ignoreThrottling
+{
+ return [AnyPromise promiseWithValue:@(1)];
+}
+
- (void)warmCaches
{
// Do nothing.
@@ -257,6 +264,15 @@ NS_ASSUME_NONNULL_BEGIN
return self.hasLocalProfile;
}
+- (void)updateProfileForAddress:(SignalServiceAddress *)address
+ profileNameEncrypted:(nullable NSData *)profileNameEncrypted
+ username:(nullable NSString *)username
+ isUuidCapable:(BOOL)isUuidCapable
+ avatarUrlPath:(nullable NSString *)avatarUrlPath
+{
+ // Do nothing.
+}
+
@end
#endif
diff --git a/SignalServiceKit/src/Util/BulkUUIDLookup.swift b/SignalServiceKit/src/Util/BulkUUIDLookup.swift
new file mode 100644
index 0000000000..bc4b8905ef
--- /dev/null
+++ b/SignalServiceKit/src/Util/BulkUUIDLookup.swift
@@ -0,0 +1,223 @@
+//
+// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
+//
+
+import PromiseKit
+
+@objc
+public class BulkUUIDLookup: NSObject {
+
+ // MARK: - Dependencies
+
+ private var tsAccountManager: TSAccountManager {
+ return .sharedInstance()
+ }
+
+ private var reachabilityManager: SSKReachabilityManager {
+ return SSKEnvironment.shared.reachabilityManager
+ }
+
+ private var contactsUpdater: ContactsUpdater {
+ return SSKEnvironment.shared.contactsUpdater
+ }
+
+ // MARK: -
+
+ private let serialQueue = DispatchQueue(label: "BulkUUIDLookup")
+
+ // This property should only be accessed on serialQueue.
+ private var phoneNumberQueue = Set()
+
+ // This property should only be accessed on serialQueue.
+ private var isUpdateInFlight = false
+
+ struct UpdateOutcome {
+ let outcome: Outcome
+ enum Outcome {
+ case networkFailure
+ case retryLimit
+ case serviceError
+ case success
+ case unknownError
+ }
+ let date: Date
+
+ init(_ outcome: Outcome) {
+ self.outcome = outcome
+ self.date = Date()
+ }
+ }
+
+ // This property should only be accessed on serialQueue.
+ private var lastOutcomeMap = [String: UpdateOutcome]()
+
+ // This property should only be accessed on serialQueue.
+ private var lastRateLimitErrorDate: Date?
+
+ @objc
+ public required override init() {
+ super.init()
+
+ SwiftSingletons.register(self)
+
+ observeNotifications()
+ }
+
+ private func observeNotifications() {
+ NotificationCenter.default.addObserver(forName: SSKReachability.owsReachabilityDidChange,
+ object: nil, queue: nil) { [weak self] _ in
+ guard let self = self else { return }
+ self.serialQueue.async {
+ self.process()
+ }
+ }
+ NotificationCenter.default.addObserver(forName: .registrationStateDidChange, object: nil, queue: nil) { [weak self] _ in
+ guard let self = self else { return }
+ self.serialQueue.async {
+ self.process()
+ }
+ }
+ }
+
+ // This should be used for non-urgent uuid lookups.
+ @objc
+ public func lookupUuids(phoneNumbers: [String]) {
+ serialQueue.async {
+ self.phoneNumberQueue.formUnion(phoneNumbers)
+ self.process()
+ }
+ }
+
+ private func process() {
+ assertOnQueue(serialQueue)
+
+ guard CurrentAppContext().isMainApp else {
+ return
+ }
+ guard reachabilityManager.isReachable else {
+ return
+ }
+ guard tsAccountManager.isRegisteredAndReady else {
+ return
+ }
+ guard FeatureFlags.useOnlyModernContactDiscovery ||
+ FeatureFlags.compareLegacyContactDiscoveryAgainstModern else {
+ // Can't fill in UUIDs using legacy contact intersections.
+ return
+ }
+
+ // Only one update in flight at a time.
+ guard !self.isUpdateInFlight else {
+ return
+ }
+
+ // Build batch.
+ let phoneNumbers = phoneNumberQueue.filter { self.shouldUpdatePhoneNumber($0) }
+ phoneNumberQueue.removeAll()
+
+ // De-bounce.
+
+ Logger.verbose("Updating: \(phoneNumbers)")
+
+ // Perform update.
+ isUpdateInFlight = true
+ firstly {
+ return contactsUpdater.lookupIdentifiersPromise(phoneNumbers: Array(phoneNumbers)).asVoid()
+ }.done {
+ self.serialQueue.async {
+ self.isUpdateInFlight = false
+ let outcome = UpdateOutcome(.success)
+ for phoneNumber in phoneNumbers {
+ self.lastOutcomeMap[phoneNumber] = outcome
+ }
+ self.process()
+ }
+ }.catch { error in
+ self.serialQueue.async {
+ self.isUpdateInFlight = false
+
+ let outcome: UpdateOutcome
+ let nsError = error as NSError
+ if nsError.domain == OWSSignalServiceKitErrorDomain &&
+ nsError.code == OWSErrorCode.contactsUpdaterRateLimit.rawValue {
+ Logger.error("Error: \(error)")
+ outcome = UpdateOutcome(.retryLimit)
+ self.lastRateLimitErrorDate = Date()
+ } else {
+ switch error {
+ case ContactDiscoveryService.ServiceError.error4xx,
+ ContactDiscoveryService.ServiceError.error5xx:
+ owsFailDebug("Error: \(error)")
+ outcome = UpdateOutcome(.serviceError)
+ case ContactDiscoveryService.ServiceError.tooManyRequests:
+ Logger.error("Error: \(error)")
+ outcome = UpdateOutcome(.retryLimit)
+ self.lastRateLimitErrorDate = Date()
+ default:
+ if IsNetworkConnectivityFailure(error) {
+ Logger.warn("Error: \(error)")
+ outcome = UpdateOutcome(.networkFailure)
+ } else if error.httpStatusCode == 413 {
+ Logger.error("Error: \(error)")
+ outcome = UpdateOutcome(.retryLimit)
+ self.lastRateLimitErrorDate = Date()
+ } else if let httpStatusCode = error.httpStatusCode,
+ httpStatusCode >= 400,
+ httpStatusCode <= 599 {
+ owsFailDebug("Error: \(error)")
+ outcome = UpdateOutcome(.serviceError)
+ } else {
+ owsFailDebug("Error: \(error)")
+ outcome = UpdateOutcome(.unknownError)
+ }
+ }
+ }
+
+ for phoneNumber in phoneNumbers {
+ self.lastOutcomeMap[phoneNumber] = outcome
+ }
+
+ self.process()
+ }
+ }.retainUntilComplete()
+ }
+
+ private func shouldUpdatePhoneNumber(_ phoneNumber: String) -> Bool {
+ assertOnQueue(serialQueue)
+
+ guard SignalServiceAddress(phoneNumber: phoneNumber).uuid == nil else {
+ return false
+ }
+
+ // Skip if we've recently had a rate limit error.
+ if let lastRateLimitErrorDate = self.lastRateLimitErrorDate {
+ let minElapsedSeconds = 5 * kMinuteInterval
+ let elapsedSeconds = lastRateLimitErrorDate.timeIntervalSinceNow
+ guard elapsedSeconds >= minElapsedSeconds else {
+ return false
+ }
+ }
+
+ guard let lastOutcome = lastOutcomeMap[phoneNumber] else {
+ return true
+ }
+
+ let minElapsedSeconds: TimeInterval
+ let elapsedSeconds = lastOutcome.date.timeIntervalSinceNow
+
+ switch lastOutcome.outcome {
+ case .networkFailure:
+ minElapsedSeconds = 1 * kMinuteInterval
+ case .retryLimit:
+ minElapsedSeconds = 15 * kMinuteInterval
+ case .serviceError:
+ minElapsedSeconds = 30 * kMinuteInterval
+ case .unknownError:
+ minElapsedSeconds = 60 * kMinuteInterval
+ case .success:
+ minElapsedSeconds = 60 * kMinuteInterval
+ }
+
+ return elapsedSeconds >= minElapsedSeconds
+ }
+}
diff --git a/SignalServiceKit/src/Util/OrderedSet.swift b/SignalServiceKit/src/Util/OrderedSet.swift
index d1cf8d819f..040eb79ce5 100644
--- a/SignalServiceKit/src/Util/OrderedSet.swift
+++ b/SignalServiceKit/src/Util/OrderedSet.swift
@@ -45,6 +45,10 @@ public struct OrderedSet where Element: Hashable {
return set.count
}
+ public var first: Element? {
+ return orderedMembers.first
+ }
+
// TODO: I only implemented the minimum API that I needed. There's lots more that could
// reasonably be added to an OrderedSet (union, etc.)
}
diff --git a/SignalServiceKit/src/Util/Profiles/BulkProfileFetch.swift b/SignalServiceKit/src/Util/Profiles/BulkProfileFetch.swift
new file mode 100644
index 0000000000..ca9dcd486a
--- /dev/null
+++ b/SignalServiceKit/src/Util/Profiles/BulkProfileFetch.swift
@@ -0,0 +1,239 @@
+//
+// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
+//
+
+import PromiseKit
+
+@objc
+public class BulkProfileFetch: NSObject {
+
+ // MARK: - Dependencies
+
+ private var profileManager: ProfileManagerProtocol {
+ return SSKEnvironment.shared.profileManager
+ }
+
+ private var tsAccountManager: TSAccountManager {
+ return .sharedInstance()
+ }
+
+ private var reachabilityManager: SSKReachabilityManager {
+ return SSKEnvironment.shared.reachabilityManager
+ }
+
+ // MARK: -
+
+ private let serialQueue = DispatchQueue(label: "BulkProfileFetch")
+
+ // This property should only be accessed on serialQueue.
+ private var addressQueue = OrderedSet()
+
+ // This property should only be accessed on serialQueue.
+ private var isUpdateInFlight = false
+
+ struct UpdateOutcome {
+ let outcome: Outcome
+ enum Outcome {
+ case networkFailure
+ case retryLimit
+ case noProfile
+ case serviceError
+ case success
+ case throttled
+ }
+ let date: Date
+
+ init(_ outcome: Outcome) {
+ self.outcome = outcome
+ self.date = Date()
+ }
+ }
+
+ // This property should only be accessed on serialQueue.
+ private var lastOutcomeMap = [SignalServiceAddress: UpdateOutcome]()
+
+ // This property should only be accessed on serialQueue.
+ private var lastRateLimitErrorDate: Date?
+
+ @objc
+ public required override init() {
+ super.init()
+
+ SwiftSingletons.register(self)
+
+ AppReadiness.runNowOrWhenAppDidBecomeReady {
+ // TODO: There would be benefit to trying to update
+ // missing & stale profiles on launch.
+ }
+
+ observeNotifications()
+ }
+
+ private func observeNotifications() {
+ NotificationCenter.default.addObserver(forName: SSKReachability.owsReachabilityDidChange,
+ object: nil, queue: nil) { [weak self] _ in
+ guard let self = self else { return }
+ self.serialQueue.async {
+ self.process()
+ }
+ }
+ NotificationCenter.default.addObserver(forName: .registrationStateDidChange, object: nil, queue: nil) { [weak self] _ in
+ guard let self = self else { return }
+ self.serialQueue.async {
+ self.process()
+ }
+ }
+ }
+
+ // This should be used for non-urgent profile updates.
+ @objc
+ public func fetchAndUpdateProfiles(thread: TSThread) {
+ fetchAndUpdateProfiles(addresses: thread.recipientAddresses)
+ }
+
+ // This should be used for non-urgent profile updates.
+ @objc
+ public func fetchAndUpdateProfile(address: SignalServiceAddress) {
+ fetchAndUpdateProfiles(addresses: [address])
+ }
+
+ // This should be used for non-urgent profile updates.
+ @objc
+ public func fetchAndUpdateProfiles(addresses: [SignalServiceAddress]) {
+ serialQueue.async {
+ guard let localAddress = self.tsAccountManager.localAddress else {
+ owsFailDebug("missing local address")
+ return
+ }
+ for address in addresses {
+ guard address != localAddress else {
+ continue
+ }
+ guard !self.addressQueue.contains(address) else {
+ continue
+ }
+ self.addressQueue.append(address)
+ }
+ self.process()
+ }
+ }
+
+ private func process() {
+ assertOnQueue(serialQueue)
+
+ guard CurrentAppContext().isMainApp else {
+ return
+ }
+ guard reachabilityManager.isReachable else {
+ return
+ }
+ guard tsAccountManager.isRegisteredAndReady else {
+ return
+ }
+
+ // Only one update in flight at a time.
+ guard !self.isUpdateInFlight else {
+ return
+ }
+
+ // Dequeue.
+ guard let address = self.addressQueue.first else {
+ return
+ }
+ self.addressQueue.remove(address)
+
+ // De-bounce.
+ guard self.shouldUpdateAddress(address) else {
+ return
+ }
+
+ Logger.verbose("Updating: \(address)")
+
+ // Perform update.
+ isUpdateInFlight = true
+ // We need to throttle these jobs.
+ // Always wait N seconds between update jobs.
+ let updateDelaySeconds: TimeInterval = 3
+ firstly {
+ self.profileManager.updateProfile(forAddressPromise: address,
+ mainAppOnly: true,
+ ignoreThrottling: false).asVoid()
+ }.done {
+ self.serialQueue.asyncAfter(deadline: DispatchTime.now() + updateDelaySeconds) {
+ self.isUpdateInFlight = false
+ self.lastOutcomeMap[address] = UpdateOutcome(.success)
+ self.process()
+ }
+ }.catch { error in
+ self.serialQueue.asyncAfter(deadline: DispatchTime.now() + updateDelaySeconds) {
+ self.isUpdateInFlight = false
+ switch error {
+ case ProfileFetchError.missing:
+ Logger.error("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.noProfile)
+ case ProfileFetchError.throttled:
+ self.lastOutcomeMap[address] = UpdateOutcome(.throttled)
+ case ProfileFetchError.rateLimit:
+ Logger.error("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.retryLimit)
+ self.lastRateLimitErrorDate = Date()
+ default:
+ if IsNetworkConnectivityFailure(error) {
+ Logger.warn("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.networkFailure)
+ } else if error.httpStatusCode == 413 {
+ Logger.error("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.retryLimit)
+ self.lastRateLimitErrorDate = Date()
+ } else if error.httpStatusCode == 404 {
+ Logger.error("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.noProfile)
+ } else {
+ // TODO: We may need to handle more status codes.
+ owsFailDebug("Error: \(error)")
+ self.lastOutcomeMap[address] = UpdateOutcome(.serviceError)
+ }
+ }
+
+ self.process()
+ }
+ }.retainUntilComplete()
+ }
+
+ private func shouldUpdateAddress(_ address: SignalServiceAddress) -> Bool {
+ assertOnQueue(serialQueue)
+
+ // Skip if we've recently had a rate limit error.
+ if let lastRateLimitErrorDate = self.lastRateLimitErrorDate {
+ let minElapsedSeconds = 5 * kMinuteInterval
+ let elapsedSeconds = lastRateLimitErrorDate.timeIntervalSinceNow
+ guard elapsedSeconds >= minElapsedSeconds else {
+ return false
+ }
+ }
+
+ guard let lastOutcome = lastOutcomeMap[address] else {
+ return true
+ }
+
+ let minElapsedSeconds: TimeInterval
+ let elapsedSeconds = lastOutcome.date.timeIntervalSinceNow
+
+ switch lastOutcome.outcome {
+ case .networkFailure:
+ minElapsedSeconds = 1 * kMinuteInterval
+ case .retryLimit:
+ minElapsedSeconds = 5 * kMinuteInterval
+ case .throttled:
+ minElapsedSeconds = 5 * kMinuteInterval
+ case .noProfile:
+ minElapsedSeconds = 6 * kHourInterval
+ case .serviceError:
+ minElapsedSeconds = 30 * kMinuteInterval
+ case .success:
+ minElapsedSeconds = 15 * kMinuteInterval
+ }
+
+ return elapsedSeconds >= minElapsedSeconds
+ }
+}
diff --git a/SignalMessaging/profiles/ProfileFetcherJob.swift b/SignalServiceKit/src/Util/Profiles/ProfileFetcherJob.swift
similarity index 91%
rename from SignalMessaging/profiles/ProfileFetcherJob.swift
rename to SignalServiceKit/src/Util/Profiles/ProfileFetcherJob.swift
index 0d269ac2ff..a0aaf783dc 100644
--- a/SignalMessaging/profiles/ProfileFetcherJob.swift
+++ b/SignalServiceKit/src/Util/Profiles/ProfileFetcherJob.swift
@@ -4,7 +4,6 @@
import Foundation
import PromiseKit
-import SignalServiceKit
import SignalMetadataKit
@objc
@@ -13,6 +12,26 @@ public enum ProfileFetchError: Int, Error {
case throttled
case notMainApp
case cantRequestVersionedProfile
+ case rateLimit
+}
+
+// MARK: -
+
+extension ProfileFetchError: CustomStringConvertible {
+ public var description: String {
+ switch self {
+ case .missing:
+ return "ProfileFetchError.missing"
+ case .throttled:
+ return "ProfileFetchError.throttled"
+ case .notMainApp:
+ return "ProfileFetchError.notMainApp"
+ case .cantRequestVersionedProfile:
+ return "ProfileFetchError.cantRequestVersionedProfile"
+ case .rateLimit:
+ return "ProfileFetchError.rateLimit"
+ }
+ }
}
// MARK: -
@@ -155,19 +174,6 @@ public class ProfileFetcherJob: NSObject {
.retainUntilComplete()
}
- @objc(fetchAndUpdateProfilesWithThread:)
- public class func fetchAndUpdateProfiles(thread: TSThread) {
- let addresses = thread.recipientAddresses
- let subjects = addresses.map { ProfileRequestSubject.address(address: $0) }
- let options = ProfileFetchOptions()
- var promises = [Promise]()
- for subject in subjects {
- let job = ProfileFetcherJob(subject: subject, options: options)
- promises.append(job.runAsPromise())
- }
- when(fulfilled: promises).retainUntilComplete()
- }
-
private init(subject: ProfileRequestSubject,
options: ProfileFetchOptions) {
self.subject = subject
@@ -188,8 +194,8 @@ public class ProfileFetcherJob: NSObject {
return SSKEnvironment.shared.udManager
}
- private var profileManager: OWSProfileManager {
- return OWSProfileManager.shared()
+ private var profileManager: ProfileManagerProtocol {
+ return SSKEnvironment.shared.profileManager
}
private var identityManager: OWSIdentityManager {
@@ -213,6 +219,10 @@ public class ProfileFetcherJob: NSObject {
return SDSDatabaseStorage.shared
}
+ private var versionedProfiles: VersionedProfiles {
+ return SSKEnvironment.shared.versionedProfiles
+ }
+
// MARK: -
private func runAsPromise() -> Promise {
@@ -220,9 +230,12 @@ public class ProfileFetcherJob: NSObject {
self.addBackgroundTask()
}.then(on: DispatchQueue.global()) { _ in
return self.requestProfile()
- }.map(on: DispatchQueue.global()) { fetchedProfile in
- self.updateProfile(fetchedProfile: fetchedProfile)
- return fetchedProfile.profile
+ }.then(on: DispatchQueue.global()) { fetchedProfile in
+ return firstly {
+ self.updateProfile(fetchedProfile: fetchedProfile)
+ }.map(on: DispatchQueue.global()) { _ in
+ return fetchedProfile.profile
+ }
}
}
@@ -255,7 +268,7 @@ public class ProfileFetcherJob: NSObject {
return requestProfileWithRetries()
}
- private func requestProfileWithRetries(remainingRetries: Int = 3) -> Promise {
+ private func requestProfileWithRetries(retryCount: Int = 0) -> Promise {
let subject = self.subject
let (promise, resolver) = Promise.pending()
@@ -265,8 +278,10 @@ public class ProfileFetcherJob: NSObject {
resolver.fulfill(fetchedProfile)
}.catch(on: DispatchQueue.global()) { error in
if error.httpStatusCode == 404 {
- resolver.reject(ProfileFetchError.missing)
- return
+ return resolver.reject(ProfileFetchError.missing)
+ }
+ if error.httpStatusCode == 413 {
+ return resolver.reject(ProfileFetchError.rateLimit)
}
switch error {
@@ -281,14 +296,15 @@ public class ProfileFetcherJob: NSObject {
resolver.reject(error)
return
default:
- guard remainingRetries > 0 else {
+ let maxRetries = 3
+ guard retryCount < maxRetries else {
Logger.warn("failed to get profile with error: \(error)")
resolver.reject(error)
return
}
firstly {
- self.requestProfileWithRetries(remainingRetries: remainingRetries - 1)
+ self.requestProfileWithRetries(retryCount: retryCount + 1)
}.done(on: DispatchQueue.global()) { fetchedProfile in
resolver.fulfill(fetchedProfile)
}.catch(on: DispatchQueue.global()) { error in
@@ -362,7 +378,7 @@ public class ProfileFetcherJob: NSObject {
if shouldUseVersionedFetch {
do {
- let request = try VersionedProfiles.versionedProfileRequest(address: address, udAccessKey: udAccessKeyForRequest)
+ let request = try self.versionedProfiles.versionedProfileRequest(address: address, udAccessKey: udAccessKeyForRequest)
currentVersionedProfileRequest = request
return request.request
} catch {
@@ -395,7 +411,7 @@ public class ProfileFetcherJob: NSObject {
let address = profile.address
if let profileRequest = fetchedProfile.versionedProfileRequest {
- VersionedProfiles.didFetchProfile(profile: profile, profileRequest: profileRequest)
+ self.versionedProfiles.didFetchProfile(profile: profile, profileRequest: profileRequest)
}
profileManager.updateProfile(for: address,
diff --git a/SignalServiceKit/src/Util/Profiles/VersionedProfiles.swift b/SignalServiceKit/src/Util/Profiles/VersionedProfiles.swift
new file mode 100644
index 0000000000..f289d5d18c
--- /dev/null
+++ b/SignalServiceKit/src/Util/Profiles/VersionedProfiles.swift
@@ -0,0 +1,76 @@
+//
+// Copyright (c) 2020 Open Whisper Systems. All rights reserved.
+//
+
+import Foundation
+import PromiseKit
+//import SignalServiceKit
+import SignalMetadataKit
+//import ZKGroup
+
+@objc
+public class VersionedProfileUpdate: NSObject {
+ // This will only be set if there is a profile avatar.
+ @objc
+ public let avatarUrlPath: String?
+
+ public required init(avatarUrlPath: String? = nil) {
+ self.avatarUrlPath = avatarUrlPath
+ }
+}
+
+// MARK: -
+
+@objc
+public protocol VersionedProfileRequest: class {
+ var request: TSRequest { get }
+}
+
+// MARK: -
+
+@objc
+public protocol VersionedProfiles: class {
+ @objc(clearProfileKeyCredentialForAddress:transaction:)
+ func clearProfileKeyCredential(for address: SignalServiceAddress,
+ transaction: SDSAnyWriteTransaction)
+
+ func clearProfileKeyCredentials(transaction: SDSAnyWriteTransaction)
+
+ func versionedProfileRequest(address: SignalServiceAddress,
+ udAccessKey: SMKUDAccessKey?) throws -> VersionedProfileRequest
+
+ func didFetchProfile(profile: SignalServiceProfile,
+ profileRequest: VersionedProfileRequest)
+}
+
+// MARK: -
+
+public protocol VersionedProfilesSwift: VersionedProfiles {
+ func updateProfilePromise(profileGivenName: String?,
+ profileFamilyName: String?,
+ profileAvatarData: Data?) -> Promise
+}
+
+// MARK: -
+
+@objc
+public class MockVersionedProfiles: NSObject, VersionedProfilesSwift {
+ public func clearProfileKeyCredential(for address: SignalServiceAddress,
+ transaction: SDSAnyWriteTransaction) {}
+
+ public func clearProfileKeyCredentials(transaction: SDSAnyWriteTransaction) {}
+
+ public func versionedProfileRequest(address: SignalServiceAddress,
+ udAccessKey: SMKUDAccessKey?) throws -> VersionedProfileRequest {
+ owsFail("Not implemented.")
+ }
+
+ public func didFetchProfile(profile: SignalServiceProfile,
+ profileRequest: VersionedProfileRequest) {}
+
+ public func updateProfilePromise(profileGivenName: String?,
+ profileFamilyName: String?,
+ profileAvatarData: Data?) -> Promise {
+ owsFail("Not implemented.")
+ }
+}
diff --git a/SignalServiceKit/src/groups/GroupManager.swift b/SignalServiceKit/src/groups/GroupManager.swift
index 20cb303f07..36c638fbb0 100644
--- a/SignalServiceKit/src/groups/GroupManager.swift
+++ b/SignalServiceKit/src/groups/GroupManager.swift
@@ -72,6 +72,18 @@ public class GroupManager: NSObject {
return SSKEnvironment.shared.messageProcessing
}
+ private class var bulkProfileFetch: BulkProfileFetch {
+ return SSKEnvironment.shared.bulkProfileFetch
+ }
+
+ private class var bulkUUIDLookup: BulkUUIDLookup {
+ return SSKEnvironment.shared.bulkUUIDLookup
+ }
+
+ private class var contactsUpdater: ContactsUpdater {
+ return SSKEnvironment.shared.contactsUpdater
+ }
+
// MARK: -
// Never instantiate this class.
@@ -222,7 +234,7 @@ public class GroupManager: NSObject {
}.then(on: .global()) { () -> Promise in
var memberSet = Set(membersParam)
memberSet.insert(localAddress)
- return self.tryToEnableGroupsV2(for: Array(memberSet), ignoreErrors: true)
+ return self.tryToEnableGroupsV2(for: Array(memberSet), isBlocking: true, ignoreErrors: true)
}.map(on: .global()) { () throws -> GroupMembership in
// Build member list.
//
@@ -719,7 +731,7 @@ public class GroupManager: NSObject {
guard RemoteConfig.groupsV2CreateGroups else {
return Promise.value(())
}
- return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allUsers), ignoreErrors: true)
+ return self.tryToEnableGroupsV2(for: Array(proposedGroupModel.groupMembership.allUsers), isBlocking: true, ignoreErrors: true)
}.then(on: .global()) { () -> Promise in
return self.ensureLocalProfileHasCommitmentIfNecessary()
}.then(on: DispatchQueue.global()) { () -> Promise in
@@ -1197,8 +1209,9 @@ public class GroupManager: NSObject {
// MARK: - UUIDs
public static func tryToEnableGroupsV2(for addresses: [SignalServiceAddress],
+ isBlocking: Bool,
ignoreErrors: Bool) -> Promise {
- let promise = tryToEnableGroupsV2(for: addresses)
+ let promise = tryToEnableGroupsV2(for: addresses, isBlocking: isBlocking)
if ignoreErrors {
return promise.recover { error -> Guarantee in
Logger.warn("Error: \(error).")
@@ -1209,7 +1222,8 @@ public class GroupManager: NSObject {
}
}
- private static func tryToEnableGroupsV2(for addresses: [SignalServiceAddress]) -> Promise {
+ private static func tryToEnableGroupsV2(for addresses: [SignalServiceAddress],
+ isBlocking: Bool) -> Promise {
return firstly { () -> Promise in
for address in addresses {
guard address.isValid else {
@@ -1218,9 +1232,45 @@ public class GroupManager: NSObject {
}
return Promise.value(())
}.then(on: .global()) { _ -> Promise in
- // Try to ensure UUIDs.
- return self.groupsV2.tryToEnsureUuidsForGroupMembers(for: addresses)
+ return self.tryToFillInMissingUuuids(for: addresses, isBlocking: isBlocking)
}.then(on: .global()) { _ -> Promise in
+ return self.tryToEnableGroupsV2Capability(for: addresses, isBlocking: isBlocking)
+ }
+ }
+
+ public static func tryToFillInMissingUuuids(for addresses: [SignalServiceAddress],
+ isBlocking: Bool) -> Promise {
+ guard FeatureFlags.useOnlyModernContactDiscovery ||
+ FeatureFlags.compareLegacyContactDiscoveryAgainstModern else {
+ // Can't fill in UUIDs using legacy contact intersections.
+ return Promise.value(())
+ }
+
+ let phoneNumbersWithoutUuids = addresses.filter { $0.uuid == nil }.compactMap { $0.phoneNumber }
+ guard phoneNumbersWithoutUuids.count > 0 else {
+ return Promise.value(())
+ }
+
+ if isBlocking {
+ // Block on the outcome.
+ return contactsUpdater.lookupIdentifiersPromise(phoneNumbers:
+ phoneNumbersWithoutUuids).asVoid()
+ } else {
+ // This will throttle, de-bounce, etc.
+ self.bulkUUIDLookup.lookupUuids(phoneNumbers: phoneNumbersWithoutUuids)
+ return Promise.value(())
+ }
+ }
+
+ private static func tryToEnableGroupsV2Capability(for addresses: [SignalServiceAddress],
+ isBlocking: Bool) -> Promise {
+ return firstly { () -> Promise<[SignalServiceAddress]> in
+ let validAddresses = addresses.filter { $0.isValid }
+ if validAddresses.count < addresses.count {
+ owsFailDebug("Invalid addresses.")
+ }
+ return Promise.value(validAddresses)
+ }.then(on: .global()) { (addresses: [SignalServiceAddress]) -> Promise in
// Try to ensure groups v2 capability.
var addressesWithoutCapability = [SignalServiceAddress]()
self.databaseStorage.read { transaction in
@@ -1233,11 +1283,18 @@ public class GroupManager: NSObject {
guard !addressesWithoutCapability.isEmpty else {
return Promise.value(())
}
- var promises = [Promise]()
- for address in addressesWithoutCapability {
- promises.append(self.profileManager.updateProfile(forAddressPromise: address).asVoid())
+ if isBlocking {
+ // Block on the outcome of the profile updates.
+ var promises = [Promise]()
+ for address in addressesWithoutCapability {
+ promises.append(self.profileManager.updateProfile(forAddressPromise: address).asVoid())
+ }
+ return when(fulfilled: promises)
+ } else {
+ // This will throttle, de-bounce, etc.
+ self.bulkProfileFetch.fetchAndUpdateProfiles(addresses: addressesWithoutCapability)
+ return Promise.value(())
}
- return when(fulfilled: promises)
}
}
diff --git a/SignalServiceKit/src/groups/GroupsV2.swift b/SignalServiceKit/src/groups/GroupsV2.swift
index 425afebba0..4bde2f5d29 100644
--- a/SignalServiceKit/src/groups/GroupsV2.swift
+++ b/SignalServiceKit/src/groups/GroupsV2.swift
@@ -66,8 +66,6 @@ public protocol GroupsV2Swift: GroupsV2 {
func tryToEnsureProfileKeyCredentials(for addresses: [SignalServiceAddress]) -> Promise
- func tryToEnsureUuidsForGroupMembers(for addresses: [SignalServiceAddress]) -> Promise
-
func fetchCurrentGroupV2Snapshot(groupModel: TSGroupModelV2) -> Promise
func fetchCurrentGroupV2Snapshot(groupSecretParamsData: Data) -> Promise
@@ -326,10 +324,6 @@ public class MockGroupsV2: NSObject, GroupsV2Swift {
owsFail("Not implemented.")
}
- public func tryToEnsureUuidsForGroupMembers(for addresses: [SignalServiceAddress]) -> Promise {
- owsFail("Not implemented.")
- }
-
public func fetchCurrentGroupV2Snapshot(groupModel: TSGroupModelV2) -> Promise {
owsFail("Not implemented.")
}
diff --git a/SignalShareExtension/Info.plist b/SignalShareExtension/Info.plist
index 275311161f..4c1031e4ca 100644
--- a/SignalShareExtension/Info.plist
+++ b/SignalShareExtension/Info.plist
@@ -19,7 +19,7 @@
CFBundleShortVersionString
3.8.0
CFBundleVersion
- 3.8.0.15
+ 3.8.0.19
ITSAppUsesNonExemptEncryption
NSAppTransportSecurity