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]];