From e12bd4773a52af9867fdc98585b04d221d06901d Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 15 Mar 2017 17:37:19 -0300 Subject: [PATCH 1/3] Fix non-contact lookup for non-US users. // FREEBIE --- src/Contacts/ContactsUpdater.h | 16 +++++++++- src/Contacts/ContactsUpdater.m | 27 +++++++++++++++- src/Contacts/PhoneNumber.h | 12 +++++++ src/Contacts/PhoneNumber.m | 58 ++++++++++++++++++++++++++++++++++ src/Contacts/PhoneNumberUtil.h | 5 +-- src/Contacts/PhoneNumberUtil.m | 15 +++++++-- 6 files changed, 127 insertions(+), 6 deletions(-) diff --git a/src/Contacts/ContactsUpdater.h b/src/Contacts/ContactsUpdater.h index 7bfab04c2..aba757422 100644 --- a/src/Contacts/ContactsUpdater.h +++ b/src/Contacts/ContactsUpdater.h @@ -14,11 +14,25 @@ NS_ASSUME_NONNULL_BEGIN - (nullable SignalRecipient *)synchronousLookup:(NSString *)identifier error:(NSError **)error; -// This asynchronously updates the SignalRecipient for a given contactId. +// This asynchronously tries to verify whether or not a contact id +// corresponds to a service account. +// +// The failure callback is invoked if the lookup fails _or_ if the +// contact id doesn't correspond to an account. - (void)lookupIdentifier:(NSString *)identifier success:(void (^)(SignalRecipient *recipient))success failure:(void (^)(NSError *error))failure; +// This asynchronously tries to verify whether or not group of possible +// contact ids correspond to service accounts. +// +// The failure callback is only invoked if the lookup fails. Otherwise, +// the success callback is invoked with the (possibly empty) set of contacts +// that were found. +- (void)lookupIdentifiers:(NSArray *)identifiers + success:(void (^)(NSArray *recipients))success + failure:(void (^)(NSError *error))failure; + - (void)updateSignalContactIntersectionWithABContacts:(NSArray *)abContacts success:(void (^)())success failure:(void (^)(NSError *error))failure; diff --git a/src/Contacts/ContactsUpdater.m b/src/Contacts/ContactsUpdater.m index 569fc3118..42b3d7b67 100644 --- a/src/Contacts/ContactsUpdater.m +++ b/src/Contacts/ContactsUpdater.m @@ -60,7 +60,7 @@ NS_ASSUME_NONNULL_BEGIN failure(OWSErrorWithCodeDescription(OWSErrorCodeInvalidMethodParameters, @"Cannot lookup nil identifier")); return; } - + [self contactIntersectionWithSet:[NSSet setWithObject:identifier] success:^(NSSet *_Nonnull matchedIds) { if (matchedIds.count == 1) { @@ -72,6 +72,31 @@ NS_ASSUME_NONNULL_BEGIN failure:failure]; } +- (void)lookupIdentifiers:(NSArray *)identifiers + success:(void (^)(NSArray *recipients))success + failure:(void (^)(NSError *error))failure +{ + if (identifiers.count < 1) { + OWSAssert(NO); + failure(OWSErrorWithCodeDescription(OWSErrorCodeInvalidMethodParameters, @"Cannot lookup zero identifiers")); + return; + } + + [self contactIntersectionWithSet:[NSSet setWithArray:identifiers] + success:^(NSSet *_Nonnull matchedIds) { + if (matchedIds.count == 1) { + NSMutableArray *recipients = [NSMutableArray new]; + for (NSString *identifier in matchedIds) { + [recipients addObject:[SignalRecipient recipientWithTextSecureIdentifier:identifier]]; + } + success(recipients); + } else { + failure(OWSErrorMakeNoSuchSignalRecipientError()); + } + } + failure:failure]; +} + - (void)updateSignalContactIntersectionWithABContacts:(NSArray *)abContacts success:(void (^)())success failure:(void (^)(NSError *error))failure { diff --git a/src/Contacts/PhoneNumber.h b/src/Contacts/PhoneNumber.h index a1effb58c..527419311 100644 --- a/src/Contacts/PhoneNumber.h +++ b/src/Contacts/PhoneNumber.h @@ -1,3 +1,7 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + #import #import @@ -24,6 +28,14 @@ + (PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text; + (PhoneNumber *)tryParsePhoneNumberFromE164:(NSString *)text; +// This will try to parse the input text as a phone number using +// the default region and the country code for this client's phone +// number. +// +// Order matters; better results will appear first. ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber; + + (NSString *)removeFormattingCharacters:(NSString *)inputString; + (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input; + (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input diff --git a/src/Contacts/PhoneNumber.m b/src/Contacts/PhoneNumber.m index 1341bbb0a..f1043b466 100644 --- a/src/Contacts/PhoneNumber.m +++ b/src/Contacts/PhoneNumber.m @@ -1,3 +1,7 @@ +// +// Copyright (c) 2017 Open Whisper Systems. All rights reserved. +// + #import "NBAsYouTypeFormatter.h" #import "NBPhoneNumber.h" #import "PhoneNumber.h" @@ -115,6 +119,60 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return [self phoneNumberFromUserSpecifiedText:sanitizedString]; } ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber { + assert(text != nil); + + text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; + if ([text isEqualToString:@""]) { + return nil; + } + + NSString *sanitizedString = [self removeFormattingCharacters:text]; + assert(sanitizedString != nil); + + NSMutableArray *result = [NSMutableArray new]; + NSMutableSet *phoneNumberSet = [NSMutableSet new]; + void (^tryParsingWithCountryCode)(NSString *, NSString *) = ^(NSString *text, + NSString *countryCode) { + PhoneNumber *phoneNumber = [PhoneNumber phoneNumberFromText:text + andRegion:countryCode]; + if (phoneNumber && + ![phoneNumberSet containsObject:[phoneNumber toE164]]) { + [result addObject:phoneNumber]; + [phoneNumberSet addObject:[phoneNumber toE164]]; + } + }; + + // Order matters; better results should appear first so + // prefer matches using this client's phone number. + if (clientPhoneNumber.length > 0) { + NSNumber *callingCodeForLocalNumber = [[PhoneNumber phoneNumberFromE164:clientPhoneNumber] getCountryCode]; + if (callingCodeForLocalNumber != nil) { + tryParsingWithCountryCode([NSString stringWithFormat:@"+%@%@", + callingCodeForLocalNumber, + sanitizedString], + [self defaultRegionCode]); + // It's gratuitous to try all country codes associated with a given + // calling code, but it can't hurt and this isn't a performance + // hotspot. + NSArray *possibleLocalCountryCodes = [PhoneNumberUtil countryCodesFromCallingCode:[NSString stringWithFormat:@"+%@", + callingCodeForLocalNumber]]; + for (NSString *countryCode in possibleLocalCountryCodes) { + tryParsingWithCountryCode([NSString stringWithFormat:@"+%@%@", + callingCodeForLocalNumber, + sanitizedString], + countryCode); + } + } + } + + // Also try matches using the phone's default region. + tryParsingWithCountryCode(sanitizedString, [self defaultRegionCode]); + + return result; +} + + (NSString *)removeFormattingCharacters:(NSString *)inputString { char outputString[inputString.length + 1]; diff --git a/src/Contacts/PhoneNumberUtil.h b/src/Contacts/PhoneNumberUtil.h index f0b4be643..ff0e24275 100644 --- a/src/Contacts/PhoneNumberUtil.h +++ b/src/Contacts/PhoneNumberUtil.h @@ -10,9 +10,10 @@ @property (nonatomic, retain) NBPhoneNumberUtil *nbPhoneNumberUtil; -+ (NSString *)callingCodeFromCountryCode:(NSString *)code; -+ (NSString *)countryNameFromCountryCode:(NSString *)code; ++ (NSString *)callingCodeFromCountryCode:(NSString *)countryCode; ++ (NSString *)countryNameFromCountryCode:(NSString *)countryCode; + (NSArray *)countryCodesForSearchTerm:(NSString *)searchTerm; ++ (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode; + (NSUInteger)translateCursorPosition:(NSUInteger)offset from:(NSString *)source diff --git a/src/Contacts/PhoneNumberUtil.m b/src/Contacts/PhoneNumberUtil.m index f8b0dfb4d..773c71fb2 100644 --- a/src/Contacts/PhoneNumberUtil.m +++ b/src/Contacts/PhoneNumberUtil.m @@ -30,8 +30,8 @@ } // country code -> country name -+ (NSString *)countryNameFromCountryCode:(NSString *)code { - NSDictionary *countryCodeComponent = @{NSLocaleCountryCode : code}; ++ (NSString *)countryNameFromCountryCode:(NSString *)countryCode { + NSDictionary *countryCodeComponent = @{NSLocaleCountryCode : countryCode}; NSString *identifier = [NSLocale localeIdentifierFromComponents:countryCodeComponent]; NSString *country = [NSLocale.currentLocale displayNameForKey:NSLocaleIdentifier value:identifier]; return country; @@ -86,6 +86,17 @@ return callingCode; } ++ (NSArray *)countryCodesFromCallingCode:(NSString *)callingCode { + NSMutableArray *countryCodes = [NSMutableArray new]; + for (NSString *countryCode in NSLocale.ISOCountryCodes) { + NSString *callingCodeForCountryCode = [self callingCodeFromCountryCode:countryCode]; + if ([callingCode isEqualToString:callingCodeForCountryCode]) { + [countryCodes addObject:countryCode]; + } + } + return countryCodes; +} + // search term -> country codes + (NSArray *)countryCodesForSearchTerm:(NSString *)searchTerm { searchTerm = [searchTerm stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; From 60e0ddfb92e4f7245e04c97e747fd0979cbe5cc2 Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Wed, 15 Mar 2017 17:55:36 -0300 Subject: [PATCH 2/3] Fix non-contact lookup for non-US users. // FREEBIE --- src/Contacts/PhoneNumber.m | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/Contacts/PhoneNumber.m b/src/Contacts/PhoneNumber.m index f1043b466..9d1f473f4 100644 --- a/src/Contacts/PhoneNumber.m +++ b/src/Contacts/PhoneNumber.m @@ -143,7 +143,14 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN [phoneNumberSet addObject:[phoneNumber toE164]]; } }; - + + if ([sanitizedString hasPrefix:@"+"]) { + // If the text starts with "+", just parse it. + tryParsingWithCountryCode(sanitizedString, [self defaultRegionCode]); + + return result; + } + // Order matters; better results should appear first so // prefer matches using this client's phone number. if (clientPhoneNumber.length > 0) { @@ -153,6 +160,14 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN callingCodeForLocalNumber, sanitizedString], [self defaultRegionCode]); + + if ([sanitizedString hasPrefix:callingCodeForLocalNumber.description]) { + // If the text starts with the calling code for the local number, try + // just adding "+" and parsing it. + tryParsingWithCountryCode( + [NSString stringWithFormat:@"+%@", sanitizedString], [self defaultRegionCode]); + } + // It's gratuitous to try all country codes associated with a given // calling code, but it can't hurt and this isn't a performance // hotspot. From 332833da7b136923282ad6749e5a2fe53a67422b Mon Sep 17 00:00:00 2001 From: Matthew Chen Date: Thu, 16 Mar 2017 13:53:06 -0300 Subject: [PATCH 3/3] Respond to CR. // FREEBIE --- src/Contacts/ContactsUpdater.h | 2 +- src/Contacts/ContactsUpdater.m | 2 +- src/Contacts/PhoneNumber.h | 4 ++-- src/Contacts/PhoneNumber.m | 43 +++++++++++++++++----------------- 4 files changed, 26 insertions(+), 25 deletions(-) diff --git a/src/Contacts/ContactsUpdater.h b/src/Contacts/ContactsUpdater.h index aba757422..bb04863be 100644 --- a/src/Contacts/ContactsUpdater.h +++ b/src/Contacts/ContactsUpdater.h @@ -23,7 +23,7 @@ NS_ASSUME_NONNULL_BEGIN success:(void (^)(SignalRecipient *recipient))success failure:(void (^)(NSError *error))failure; -// This asynchronously tries to verify whether or not group of possible +// This asynchronously tries to verify whether or not a group of possible // contact ids correspond to service accounts. // // The failure callback is only invoked if the lookup fails. Otherwise, diff --git a/src/Contacts/ContactsUpdater.m b/src/Contacts/ContactsUpdater.m index 42b3d7b67..c2c22ce7f 100644 --- a/src/Contacts/ContactsUpdater.m +++ b/src/Contacts/ContactsUpdater.m @@ -89,7 +89,7 @@ NS_ASSUME_NONNULL_BEGIN for (NSString *identifier in matchedIds) { [recipients addObject:[SignalRecipient recipientWithTextSecureIdentifier:identifier]]; } - success(recipients); + success([recipients copy]); } else { failure(OWSErrorMakeNoSuchSignalRecipientError()); } diff --git a/src/Contacts/PhoneNumber.h b/src/Contacts/PhoneNumber.h index 527419311..e0b08c1f7 100644 --- a/src/Contacts/PhoneNumber.h +++ b/src/Contacts/PhoneNumber.h @@ -33,8 +33,8 @@ // number. // // Order matters; better results will appear first. -+ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text - clientPhoneNumber:(NSString *)clientPhoneNumber; ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber; + (NSString *)removeFormattingCharacters:(NSString *)inputString; + (NSString *)bestEffortFormatPartialUserSpecifiedTextToLookLikeAPhoneNumber:(NSString *)input; diff --git a/src/Contacts/PhoneNumber.m b/src/Contacts/PhoneNumber.m index 9d1f473f4..40034d4ff 100644 --- a/src/Contacts/PhoneNumber.m +++ b/src/Contacts/PhoneNumber.m @@ -13,8 +13,8 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN @implementation PhoneNumber + (PhoneNumber *)phoneNumberFromText:(NSString *)text andRegion:(NSString *)regionCode { - assert(text != nil); - assert(regionCode != nil); + OWSAssert(text != nil); + OWSAssert(regionCode != nil); NBPhoneNumberUtil *phoneUtil = [PhoneNumberUtil sharedUtil].nbPhoneNumberUtil; @@ -40,7 +40,7 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN } + (PhoneNumber *)phoneNumberFromUserSpecifiedText:(NSString *)text { - assert(text != nil); + OWSAssert(text != nil); return [PhoneNumber phoneNumberFromText:text andRegion:[self defaultRegionCode]]; } @@ -60,11 +60,11 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN } + (PhoneNumber *)phoneNumberFromE164:(NSString *)text { - assert(text != nil); - assert([text hasPrefix:COUNTRY_CODE_PREFIX]); + OWSAssert(text != nil); + OWSAssert([text hasPrefix:COUNTRY_CODE_PREFIX]); PhoneNumber *number = [PhoneNumber phoneNumberFromText:text andRegion:@"ZZ"]; - assert(number != nil); + OWSAssert(number != nil); return number; } @@ -102,14 +102,14 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN + (PhoneNumber *)tryParsePhoneNumberFromText:(NSString *)text fromRegion:(NSString *)regionCode { - assert(text != nil); - assert(regionCode != nil); + OWSAssert(text != nil); + OWSAssert(regionCode != nil); return [self phoneNumberFromText:text andRegion:regionCode]; } + (PhoneNumber *)tryParsePhoneNumberFromUserSpecifiedText:(NSString *)text { - assert(text != nil); + OWSAssert(text != nil); if ([text isEqualToString:@""]) { return nil; @@ -119,26 +119,26 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return [self phoneNumberFromUserSpecifiedText:sanitizedString]; } -+ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text - clientPhoneNumber:(NSString *)clientPhoneNumber { - assert(text != nil); - ++ (NSArray *)tryParsePhoneNumbersFromsUserSpecifiedText:(NSString *)text + clientPhoneNumber:(NSString *)clientPhoneNumber +{ + OWSAssert(text != nil); + text = [text stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]]; if ([text isEqualToString:@""]) { return nil; } NSString *sanitizedString = [self removeFormattingCharacters:text]; - assert(sanitizedString != nil); - + OWSAssert(sanitizedString != nil); + NSMutableArray *result = [NSMutableArray new]; NSMutableSet *phoneNumberSet = [NSMutableSet new]; void (^tryParsingWithCountryCode)(NSString *, NSString *) = ^(NSString *text, NSString *countryCode) { PhoneNumber *phoneNumber = [PhoneNumber phoneNumberFromText:text andRegion:countryCode]; - if (phoneNumber && - ![phoneNumberSet containsObject:[phoneNumber toE164]]) { + if (phoneNumber && [phoneNumber toE164] && ![phoneNumberSet containsObject:[phoneNumber toE164]]) { [result addObject:phoneNumber]; [phoneNumberSet addObject:[phoneNumber toE164]]; } @@ -151,8 +151,9 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN return result; } - // Order matters; better results should appear first so - // prefer matches using this client's phone number. + // Order matters; better results should appear first so prefer + // matches with the same country code as this client's phone number. + OWSAssert(clientPhoneNumber.length > 0); if (clientPhoneNumber.length > 0) { NSNumber *callingCodeForLocalNumber = [[PhoneNumber phoneNumberFromE164:clientPhoneNumber] getCountryCode]; if (callingCodeForLocalNumber != nil) { @@ -161,7 +162,7 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN sanitizedString], [self defaultRegionCode]); - if ([sanitizedString hasPrefix:callingCodeForLocalNumber.description]) { + if ([sanitizedString hasPrefix:[NSString stringWithFormat:@"%d", callingCodeForLocalNumber.intValue]]) { // If the text starts with the calling code for the local number, try // just adding "+" and parsing it. tryParsingWithCountryCode( @@ -204,7 +205,7 @@ static NSString *const RPDefaultsKeyPhoneNumberCanonical = @"RPDefaultsKeyPhoneN } + (PhoneNumber *)tryParsePhoneNumberFromE164:(NSString *)text { - assert(text != nil); + OWSAssert(text != nil); return [self phoneNumberFromE164:text]; }