123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455 |
- // _____ _
- // |_ _| |_ _ _ ___ ___ _ __ __ _
- // | | | ' \| '_/ -_) -_) ' \/ _` |_
- // |_| |_||_|_| \___\___|_|_|_\__,_(_)
- //
- // Threema iOS Client
- // Copyright (c) 2012-2020 Threema GmbH
- //
- // This program is free software: you can redistribute it and/or modify
- // it under the terms of the GNU Affero General Public License, version 3,
- // as published by the Free Software Foundation.
- //
- // This program is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU Affero General Public License for more details.
- //
- // You should have received a copy of the GNU Affero General Public License
- // along with this program. If not, see <https://www.gnu.org/licenses/>.
- #import <AddressBook/AddressBook.h>
- #import <Contacts/Contacts.h>
- #import "PhoneNumberNormalizer.h"
- #import "ContactStore.h"
- #import "NSString+Hex.h"
- #import "Contact.h"
- #import "ServerAPIConnector.h"
- #import "MyIdentityStore.h"
- #import "Utils.h"
- #import "UserSettings.h"
- #import "ProtocolDefines.h"
- #import "EntityManager.h"
- #import "ThreemaError.h"
- #import "AppGroup.h"
- #import "WorkDataFetcher.h"
- #import "ValidationLogger.h"
- #import "IdentityInfoFetcher.h"
- #import "CryptoUtils.h"
- #import "TrustedContacts.h"
- #define MIN_CHECK_INTERVAL 5*60
- #ifdef DEBUG
- static const DDLogLevel ddLogLevel = DDLogLevelInfo;
- #else
- static const DDLogLevel ddLogLevel = DDLogLevelWarning;
- #endif
- static const uint8_t emailHashKey[] = {0x30,0xa5,0x50,0x0f,0xed,0x97,0x01,0xfa,0x6d,0xef,0xdb,0x61,0x08,0x41,0x90,0x0f,0xeb,0xb8,0xe4,0x30,0x88,0x1f,0x7a,0xd8,0x16,0x82,0x62,0x64,0xec,0x09,0xba,0xd7};
- static const uint8_t mobileNoHashKey[] = {0x85,0xad,0xf8,0x22,0x69,0x53,0xf3,0xd9,0x6c,0xfd,0x5d,0x09,0xbf,0x29,0x55,0x5e,0xb9,0x55,0xfc,0xd8,0xaa,0x5e,0xc4,0xf9,0xfc,0xd8,0x69,0xe2,0x58,0x37,0x07,0x23};
- static const NSTimeInterval minimumSyncInterval = 30; /* avoid multiple concurrent syncs, e.g. triggered by interval timer + incoming message from unknown user */
- @implementation ContactStore {
- NSDate *lastMaxModificationDate;
- NSDate *lastFullSyncDate;
- NSTimer *checkStatusTimer;
- dispatch_queue_t syncQueue;
- EntityManager *entityManager;
- }
- + (ContactStore*)sharedContactStore {
- static ContactStore *instance;
-
- @synchronized (self) {
- if (!instance)
- instance = [[ContactStore alloc] init];
- }
-
- return instance;
- }
- - (id)init
- {
- self = [super init];
- if (self) {
- syncQueue = dispatch_queue_create("ch.threema.contactsync", DISPATCH_QUEUE_SERIAL);
- dispatch_set_target_queue(syncQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
-
- /* register a callback to get information about address book changes */
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChangeDetected:) name:CNContactStoreDidChangeNotification object:nil];
- [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orderChanged:) name:@"ThreemaContactsOrderChanged" object:nil];
-
- entityManager = [[EntityManager alloc] init];
-
- /* update display/sort order prefs to match system */
- if (@available(iOS 11.0, *)) {
- BOOL sortOrder = [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName;
- [[UserSettings sharedUserSettings] setSortOrderFirstName:sortOrder];
- } else {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- [[UserSettings sharedUserSettings] setSortOrderFirstName:(ABPersonGetSortOrdering() == kABPersonSortByFirstName)];
- #pragma clang diagnostic pop
- }
- }
- return self;
- }
- - (void)dealloc {
- [checkStatusTimer invalidate];
- }
- - (void)addressBookChangeDetected:(NSNotification *)notification {
- DDLogInfo(@"Address book change detected");
- [[ValidationLogger sharedValidationLogger] logString:@"Address book change detected"];
- [self synchronizeAddressBookForceFullSync:NO onCompletion:^(BOOL addressBookAccessGranted) {
- [self updateAllContacts];
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
- } onError:^(NSError *error) {
- [self updateAllContacts];
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
- }];
- }
- - (Contact*)contactForIdentity:(NSString *)identity {
- /* check in local DB first */
- EntityManager *entityManager = [[EntityManager alloc] init];
- Contact *contact = [entityManager.entityFetcher contactForId: identity];
- return contact;
- }
- - (void)addContactWithIdentity:(NSString *)identity verificationLevel:(int32_t)verificationLevel onCompletion:(void(^)(Contact *contact, BOOL alreadyExists))onCompletion onError:(void(^)(NSError *error))onError {
-
- /* check in local DB first */
- EntityManager *entityManager = [[EntityManager alloc] init];
- NSError *error;
- Contact *contact = [entityManager.entityFetcher contactForId:identity error:&error];
- if (contact) {
- onCompletion(contact, YES);
- return;
- }
- if (error != nil) {
- onError(error);
- }
-
- /* not found - request from server */
- ServerAPIConnector *apiConnector = [[ServerAPIConnector alloc] init];
- [apiConnector fetchIdentityInfo:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
-
- /* save new contact */
- dispatch_async(dispatch_get_main_queue(), ^{
- /* save new contact */
- Contact *contact = [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:verificationLevel state:state type:type featureMask:featureMask alerts:YES];
- /* force synchronisation */
- [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
- [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:kSafeBackupTrigger object:nil];
-
- onCompletion(contact, NO);
- });
- } onError:^(NSError *error) {
- onError(error);
- }];
- }
- - (Contact*)addContactWithIdentity:(NSString*)identity publicKey:(NSData*)publicKey cnContactId:(NSString *)cnContactId verificationLevel:(int32_t)verificationLevel featureMask:(NSNumber *)featureMask alerts:(BOOL)alerts {
- return [self addContactWithIdentity:identity publicKey:publicKey cnContactId:cnContactId verificationLevel:verificationLevel state:nil type:nil featureMask:featureMask alerts:alerts];
- }
- - (Contact*)addContactWithIdentity:(NSString*)identity publicKey:(NSData*)publicKey cnContactId:(NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(NSNumber *)state type:(NSNumber *)type featureMask:(NSNumber *)featureMask alerts:(BOOL)alerts {
- /* Make sure this is not our own identity */
- if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
- DDLogInfo(@"Ignoring attempt to add own identity");
- return nil;
- }
-
- /* Check if we already have a contact with this identity */
- __block BOOL added = NO;
- __block Contact *contact;
- [entityManager performSyncBlockAndSafe:^{
- contact = [entityManager.entityFetcher contactForId: identity];
- if (contact) {
- DDLogInfo(@"Found existing contact with identity %@", identity);
- if (![publicKey isEqualToData:contact.publicKey]) {
- DDLogError(@"Public key doesn't match for existing identity %@!", identity);
-
- if (alerts) {
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorPublicKeyMismatch object:nil userInfo:nil];
- }
-
- return;
- }
- } else {
- added = YES;
- contact = [entityManager.entityCreator contact];
- contact.identity = identity;
- contact.publicKey = publicKey;
- contact.featureMask = featureMask;
- if (state != nil) {
- contact.state = state;
- }
- if (type != nil) {
- if ([type isEqualToNumber:@1]) {
- NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities];
- if (![workIdentities containsObject:contact.identity])
- [workIdentities addObject:contact.identity];
- [UserSettings sharedUserSettings].workIdentities = workIdentities;
- }
- }
- }
-
- if (contact.verificationLevel == nil || (contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified)
- contact.verificationLevel = [NSNumber numberWithInt:verificationLevel];
-
- if (contact.workContact == nil) {
- if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
- contact.workContact = [NSNumber numberWithBool:YES];
- } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
- contact.workContact = [NSNumber numberWithBool:YES];
- } else {
- contact.workContact = [NSNumber numberWithBool:NO];
- }
- }
- if ([contact.workContact isEqualToNumber:[NSNumber numberWithBool:YES]] && (contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified)) {
- if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
- } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
- }
- }
-
- // check if this is a trusted contact (like *THREEMA)
- if ([TrustedContacts isTrustedContactWithIdentity:identity publicKey:publicKey]) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
- }
-
- if (cnContactId) {
- if (contact.cnContactId != nil) {
- if (![contact.cnContactId isEqualToString:cnContactId]) {
- /* contact is already linked to a different CNContactID - check if the name matches;
- if so, the CNContactID may have changed and we need to re-link */
- CNContactStore *cnAddressBook = [CNContactStore new];
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactId]];
- NSError *error;
- NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- } else {
- if (cnContacts.count == 1) {
- CNContact *foundContact = cnContacts.firstObject;
- NSString *firstName = foundContact.givenName;
- NSString *lastName = foundContact.familyName;
-
- if (contact.firstName != nil && contact.firstName.length > 0 && contact.lastName != nil && contact.lastName.length > 0) {
- if ([firstName isEqualToString:contact.firstName] && [lastName isEqualToString:contact.lastName]) {
- DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
- [self linkContact:contact toCnContactId:cnContactId];
- }
- }
- else if (contact.firstName != nil && contact.firstName.length > 0) {
- if ([firstName isEqualToString:contact.firstName]) {
- DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
- [self linkContact:contact toCnContactId:cnContactId];
- }
- }
- else if (contact.lastName != nil && contact.lastName.length > 0) {
- if ([lastName isEqualToString:contact.lastName]) {
- DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
- [self linkContact:contact toCnContactId:cnContactId];
- }
- }
- else {
- // No name for the contact to compare, replace the cncontactid
- DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
- [self linkContact:contact toCnContactId:cnContactId];
- }
- }
- }
- }
- }];
- }
- } else {
- [self linkContact:contact toCnContactId:cnContactId];
- }
- }
- }];
-
- if (added)
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact];
-
- return contact;
- }
- - (Contact *)addWorkContactWithIdentity:(NSString *)identity publicKey:(NSData*)publicKey firstname:(NSString *)firstname lastname:(NSString *)lastname {
- /* Make sure this is not our own identity */
- if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
- DDLogInfo(@"Ignoring attempt to add own identity");
- return nil;
- }
-
- /* Check if we already have a contact with this identity */
- __block BOOL added = NO;
- __block Contact *contact;
-
- [entityManager performSyncBlockAndSafe:^{
- contact = [entityManager.entityFetcher contactForId: identity];
- if (contact) {
- DDLogInfo(@"Found existing contact with identity %@", identity);
- if (![publicKey isEqualToData:contact.publicKey]) {
- DDLogError(@"Public key doesn't match for existing identity %@!", identity);
- return;
- }
- } else {
- added = YES;
- contact = [entityManager.entityCreator contact];
- contact.identity = identity;
- contact.publicKey = publicKey;
- contact.firstName = firstname;
- contact.lastName = lastname;
- NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities];
- if (![workIdentities containsObject:contact.identity])
- [workIdentities addObject:contact.identity];
- [UserSettings sharedUserSettings].workIdentities = workIdentities;
- }
-
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
- contact.workContact = [NSNumber numberWithBool:YES];
- }];
-
- if (added)
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact];
-
- [self updateFeatureMasksForContacts:@[contact] onCompletion:^{
- } onError:^(NSError *error) {
- }];
- return contact;
- }
- - (void)updateContact:(Contact*)contact {
- if (contact.cnContactId == nil) {
- return;
- }
-
- CNContactStore *cnAddressBook = [CNContactStore new];
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSError *error;
- NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- } else {
- CNContact *foundContact = cnContacts.firstObject;
- [self _updateContact:contact withCnContact:foundContact];
- }
- }
- }];
- }
- - (void)_updateContact:(Contact *)contact withCnContact:(CNContact *)cnContact {
-
- NSString *newFirstName = cnContact.givenName;
- NSString *newLastName = cnContact.familyName;
-
- /* no name? try company name and e-mail address (Outlook auto-generated contacts etc.) */
- if (newFirstName.length == 0 && newLastName.length == 0) {
- NSString *companyName = cnContact.organizationName;
- if (companyName.length > 0) {
- newLastName = companyName;
- } else {
- /* no name? try e-mail address (Outlook auto-generated contacts etc.) */
- if (cnContact.emailAddresses.count > 0) {
- newLastName = ((CNLabeledValue *)cnContact.emailAddresses.firstObject).value;
- }
- }
- }
-
- if (newFirstName != contact.firstName && ![newFirstName isEqual:contact.firstName])
- contact.firstName = newFirstName;
-
- if (newLastName != contact.lastName && ![newLastName isEqual:contact.lastName])
- contact.lastName = newLastName;
-
- /* get image, if any */
- NSData *newImageData = nil;
- if (cnContact.imageDataAvailable) {
- newImageData = cnContact.thumbnailImageData;
- }
-
- if (newImageData != contact.imageData && ![newImageData isEqualToData:contact.imageData])
- contact.imageData = newImageData;
-
- DDLogVerbose(@"Updated contact %@ %@", contact.firstName, contact.lastName);
- }
- - (void)updateAllContacts {
-
- NSArray *allContacts = [entityManager.entityFetcher allContacts];
- if (allContacts == nil || allContacts.count == 0) {
- return;
- }
-
- // Migration of verfication level kVerificationLevelWorkVerified and kVerificationLevelWorkFullyVerified to flag workContact
- [entityManager performAsyncBlockAndSafe:^{
- for (Contact *contact in allContacts) {
- if (contact.workContact == nil || contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
- if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
- contact.workContact = [NSNumber numberWithBool:YES];
- } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
- contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
- contact.workContact = [NSNumber numberWithBool:YES];
- } else {
- contact.workContact = [NSNumber numberWithBool:NO];
- }
- }
- }
- }];
-
- NSArray *linkedContacts = [allContacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
- Contact *contact = (Contact *)evaluatedObject;
- return contact.cnContactId != nil;
- }]];
- if (linkedContacts == nil || linkedContacts.count == 0) {
- return;
- }
-
- CNContactStore *cnAddressBook = [CNContactStore new];
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- [entityManager performAsyncBlockAndSafe:^{
- /* go through all contacts and resync with address book; only create
- address book ref when encountering the first contact that is linked */
- int nupdated = 0;
-
- for (Contact *contact in linkedContacts) {
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSError *error;
- NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- } else {
- if (cnContacts != nil && cnContacts.count > 0) {
- CNContact *foundContact = cnContacts.firstObject;
- [self _updateContact:contact withCnContact:foundContact];
- nupdated++;
- }
- }
- }
-
- DDLogInfo(@"Updated %d contacts", nupdated);
- }];
-
- [self updateStatusForAllContacts];
- }
- }];
- }
- - (void)linkContact:(Contact*)contact toCnContactId:(NSString *)cnContactId {
- /* obtain first/last name from address book */
- [entityManager performSyncBlockAndSafe:^{
- contact.cnContactId = cnContactId;
- [self updateContact:contact];
- }];
- }
- - (void)unlinkContact:(Contact*)contact {
- [entityManager performSyncBlockAndSafe:^{
- contact.abRecordId = [NSNumber numberWithInt:0];
- contact.cnContactId = nil;
- contact.firstName = nil;
- contact.lastName = nil;
- contact.imageData = nil;
- }];
- }
- - (void)fetchPublicKeyForIdentity:(NSString*)identity onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
-
- [entityManager performBlock:^{
- // check in local DB first
- Contact *contact = [entityManager.entityFetcher contactForId:identity];
- if (contact.publicKey) {
- onCompletion(contact.publicKey);
- } else {
- // not found - request from server
- if ([UserSettings sharedUserSettings].blockUnknown) {
- DDLogVerbose(@"Block unknown contacts is on - discarding message");
- onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:kBlockUnknownContactErrorCode]);
- return;
- }
-
- [[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchIdentityInfoFor:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
-
- dispatch_async(dispatch_get_main_queue(), ^{
- // First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
- Contact *contact = [entityManager.entityFetcher contactForId:identity];
- if (contact.publicKey) {
- onCompletion(contact.publicKey);
- return;
- }
- // Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
- [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:kVerificationLevelUnverified state:state type:type featureMask:featureMask alerts:NO];
- [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
- [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil];
-
- onCompletion(publicKey);
- });
- } onError:^(NSError * _Nonnull error) {
- onError(error);
- }];
- }
- }];
- }
- - (void)prefetchIdentityInfo:(NSSet*)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
- NSMutableSet *identitiesToFetch = [NSMutableSet set];
-
- // Skip identities that we already have a contact for
- for (NSString *identity in identities) {
- Contact *contact = [entityManager.entityFetcher contactForId:identity];
- if (!contact) {
- [identitiesToFetch addObject:identity];
- }
- }
-
- if ([identitiesToFetch count] == 0) {
- onCompletion();
- return;
- }
-
- [[IdentityInfoFetcher sharedIdentityInfoFetcher] prefetchIdentityInfo:identitiesToFetch onCompletion:onCompletion onError:onError];
- }
- - (void)upgradeContact:(Contact*)contact toVerificationLevel:(int)verificationLevel {
- if ((contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified) {
- [entityManager performSyncBlockAndSafe:^{
- contact.verificationLevel = [NSNumber numberWithInt:verificationLevel];
- }];
- }
- }
- - (void)setWorkContact:(Contact *)contact workContact:(BOOL)workContact {
- [entityManager performSyncBlockAndSafe:^{
- contact.workContact = [NSNumber numberWithBool:workContact];
- }];
- }
- - (void)updateProfilePicture:(Contact *)contact imageData:(NSData *)imageData didFailWithError:(NSError **)error {
- UIImage *image = [UIImage imageWithData:imageData];
- if (image == nil) {
- *error = [ThreemaError threemaError:@"Image decoding failed"];
- return;
- }
-
- [entityManager performSyncBlockAndSafe:^{
- ImageData *dbImage = [entityManager.entityCreator imageData];
- dbImage.data = imageData;
- dbImage.width = [NSNumber numberWithInt:image.size.width];
- dbImage.height = [NSNumber numberWithInt:image.size.height];
-
- contact.contactImage = dbImage;
- }];
-
- [self removeProfilePictureRequest:contact.identity];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity];
- }
- - (void)deleteProfilePicture:(Contact *)contact {
- [entityManager performSyncBlockAndSafe:^{
- contact.contactImage = nil;
- }];
-
- [self removeProfilePictureRequest:contact.identity];
-
- [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity];
- }
- - (void)removeProfilePictureFlagForAllContacts {
- [entityManager performAsyncBlockAndSafe:^{
- NSArray *allContacts = [entityManager.entityFetcher allContacts];
-
- if (allContacts != nil) {
- for (Contact *contact in allContacts) {
- contact.profilePictureSended = NO;
- }
- }
- }];
- }
- - (void)removeProfilePictureFlagForContact:(NSString *)identity {
- [entityManager performAsyncBlockAndSafe:^{
- Contact *contact = [entityManager.entityFetcher contactForId:identity];
-
- if (contact != nil) {
- contact.profilePictureSended = NO;
- }
- }];
- }
- - (BOOL)existsProfilePictureRequest:(NSString *)identity {
- @synchronized (self) {
- return [[[UserSettings sharedUserSettings] profilePictureRequestList] containsObject:identity];
- }
- }
- - (void)removeProfilePictureRequest:(NSString *)identity {
- @synchronized (self) {
- if ([self existsProfilePictureRequest:identity]) {
- NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:[UserSettings sharedUserSettings].profilePictureRequestList];
- [profilePictureRequestList removeObject:identity];
- [UserSettings sharedUserSettings].profilePictureRequestList = profilePictureRequestList.allObjects;
- }
- }
- }
- - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
- [self synchronizeAddressBookForceFullSync:forceFullSync ignoreMinimumInterval:NO onCompletion:onCompletion onError:onError];
- }
- - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
- if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
- return;
- }
-
- /* Get all entries from the user's address book, hash the e-mail addresses
- and phone numbers and send to the server. */
- if (![UserSettings sharedUserSettings].syncContacts) {
- // trigger updating of status for identities
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
- [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
- if (onCompletion != nil) {
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion(NO);
- });
- }
- });
- return;
- }
-
- CNContactStore *cnAddressBook = [CNContactStore new];
-
- if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusAuthorized) {
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- [self synchronizeAddressBookForceFullSync:forceFullSync onCompletion:onCompletion onError:onError];
- } else {
- DDLogInfo(@"Address book access has NOT been granted: %@", error);
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
- [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
- });
- if (onCompletion != nil)
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion(NO);
- });
- }
- }];
- return;
- }
-
- if (cnAddressBook == nil) {
- DDLogInfo(@"Address book is nil");
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
- [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
- });
- return;
- }
-
- dispatch_async(syncQueue, ^{
- NSUserDefaults *defaults = [AppGroup userDefaults];
- NSDate *lastServerCheck = [defaults objectForKey:@"ContactsSyncLastCheck"];
- NSInteger lastServerCheckInterval = [defaults integerForKey:@"ContactsSyncLastCheckInterval"];
- BOOL fullServerSync = YES;
-
- /* calculate earliest date for next server check */
- if (lastServerCheck != nil) {
- if (-[lastServerCheck timeIntervalSinceNow] < lastServerCheckInterval) {
- DDLogInfo(@"Last server contacts sync less than %ld seconds ago", (long)lastServerCheckInterval);
- if (forceFullSync) {
- DDLogInfo(@"Forcing full sync");
- } else {
- fullServerSync = NO;
- }
- }
- }
-
- /* check if we are within the minimum interval */
- if (fullServerSync) {
- if (!ignoreMinimumInterval && lastFullSyncDate != nil && -[lastFullSyncDate timeIntervalSinceNow] < minimumSyncInterval) {
- DDLogInfo(@"Still within minimum interval - not syncing");
- if (onCompletion != nil)
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion(YES);
- });
- return;
- }
- lastFullSyncDate = [NSDate date];
- }
-
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: build all e-mail and phone number hashes"];
- /* extract all e-mail and phone number hashes from the user's address book */
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- NSError *error;
- NSMutableArray *allCNContacts = [NSMutableArray new];
-
- NSArray *containers = [cnAddressBook containersMatchingPredicate:nil error:&error];
- for (CNContainer *container in containers) {
- NSPredicate *predicate = [CNContact predicateForContactsInContainerWithIdentifier:container.identifier];
- NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- [allCNContacts addObjectsFromArray:cnContacts];
- }
-
- [self processAddressBookContacts:allCNContacts fullServerSync:fullServerSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
- }
- }];
- });
- }
- - (void)processAddressBookContacts:(NSArray*)contacts fullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
-
- NSSet *emailLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
- NSSet *mobileNoLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
-
- PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance];
- NSString *countryCode = [PhoneNumberNormalizer userRegion];
- DDLogInfo(@"Current country code: %@", countryCode);
-
- NSMutableSet *emailHashesBase64 = [NSMutableSet set];
- NSMutableSet *mobileNoHashesBase64 = [NSMutableSet set];
-
- NSMutableDictionary *emailHashToCnContactId = [NSMutableDictionary dictionary];
- NSMutableDictionary *mobileNoHashToCnContactId = [NSMutableDictionary dictionary];
-
- for (CNContact *person in contacts) {
- NSString *cnContactId = person.identifier;
- NSString *name = [CNContactFormatter stringFromContact:person style:CNContactFormatterStyleFullName];
-
- for (CNLabeledValue *label in person.emailAddresses) {
- NSString *email = label.value;
- if (email.length > 0) {
- NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
- NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized];
- [emailHashToCnContactId setObject:cnContactId forKey:emailHashBase64];
- [emailHashesBase64 addObject:emailHashBase64];
-
- /* Gmail address? If so, hash with the other domain as well */
- NSString *emailNormalizedAlt = nil;
- if ([emailNormalized hasSuffix:@"@gmail.com"])
- emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@gmail.com" withString:@"@googlemail.com"];
- else if ([emailNormalized hasSuffix:@"@googlemail.com"])
- emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@googlemail.com" withString:@"@gmail.com"];
-
- if (emailNormalizedAlt != nil) {
- NSString *emailHashAltBase64 = [self hashEmailBase64:emailNormalizedAlt];
- [emailHashToCnContactId setObject:cnContactId forKey:emailHashAltBase64];
- [emailHashesBase64 addObject:emailHashAltBase64];
- }
-
- DDLogVerbose(@"%@ (%@): %@", name, cnContactId, emailNormalized);
- }
- }
-
- for (CNLabeledValue *label in person.phoneNumbers) {
- NSString *phone = [label.value stringValue];
- if (phone.length > 0) {
- /* normalize phone number first */
- NSString *mobileNoNormalized = [normalizer phoneNumberToE164:phone withDefaultRegion:countryCode prettyFormat:nil];
- if (mobileNoNormalized == nil)
- continue;
- NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized];
- [mobileNoHashToCnContactId setObject:cnContactId forKey:mobileNoHashBase64];
- [mobileNoHashesBase64 addObject:mobileNoHashBase64];
- DDLogVerbose(@"%@ (%@): %@", name, cnContactId, mobileNoNormalized);
- }
- }
- }
-
- if (!fullServerSync) {
- /* a full server sync is not scheduled right now, so remove any hashes that we checked last time from the list */
- for (NSString *emailHash in emailLastCheck) {
- [emailHashesBase64 removeObject:emailHash];
- }
- for (NSString *mobileNoHash in mobileNoLastCheck) {
- [mobileNoHashesBase64 removeObject:mobileNoHash];
- }
- }
-
- if (emailHashesBase64.count == 0 && mobileNoHashesBase64.count == 0) {
- DDLogInfo(@"No new contacts to synchronize");
- if (onCompletion != nil)
- dispatch_async(dispatch_get_main_queue(), ^{
- onCompletion(YES);
- });
- return;
- }
- [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"ContactSync: Start request %lu emails, %lu phonenumbers", (unsigned long)emailHashesBase64.count, (unsigned long)mobileNoHashesBase64.count]];
- ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
- [conn matchIdentitiesWithEmailHashes:[emailHashesBase64 allObjects] mobileNoHashes:[mobileNoHashesBase64 allObjects] includeInactive:NO onCompletion:^(NSArray *identities, int checkInterval) {
-
- if (fullServerSync) {
- [defaults setObject:[emailHashesBase64 allObjects] forKey:@"ContactsSyncLastEmailHashes"];
- [defaults setObject:[mobileNoHashesBase64 allObjects] forKey:@"ContactsSyncLastMobileNoHashes"];
- [defaults setObject:[NSDate date] forKey:@"ContactsSyncLastCheck"];
- } else {
- /* add the hashes we just checked to the full list */
- NSMutableArray *prevEmailHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
- NSMutableArray *prevMobileNoHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
- [prevEmailHashes addObjectsFromArray:[emailHashesBase64 allObjects]];
- [prevMobileNoHashes addObjectsFromArray:[mobileNoHashesBase64 allObjects]];
- [defaults setObject:prevEmailHashes forKey:@"ContactsSyncLastEmailHashes"];
- [defaults setObject:prevMobileNoHashes forKey:@"ContactsSyncLastMobileNoHashes"];
- }
- [defaults setInteger:checkInterval forKey:@"ContactsSyncLastCheckInterval"];
- [defaults synchronize];
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: Start core data stuff"];
- /* Core data stuff on main thread */
- dispatch_async(dispatch_get_main_queue(), ^{
- NSSet *excludedIds = [NSSet setWithArray:[UserSettings sharedUserSettings].syncExclusionList];
- NSMutableArray *allIdentities = [NSMutableArray new];
- for (NSDictionary *identityData in identities) {
- NSString *identity = [identityData objectForKey:@"identity"];
-
- /* ignore this ID? */
- if ([excludedIds containsObject:identity])
- continue;
-
- NSString *cnContactId = [emailHashToCnContactId objectForKey:[identityData objectForKey:@"emailHash"]];
- if (cnContactId == nil) {
- cnContactId = [mobileNoHashToCnContactId objectForKey:[identityData objectForKey:@"mobileNoHash"]];
- }
- if (cnContactId == nil) {
- continue;
- }
-
- DDLogVerbose(@"Adding identity %@ to contacts", identity);
- [allIdentities addObject:identity];
- [self addContactWithIdentity:identity publicKey:[[NSData alloc] initWithBase64EncodedString:[identityData objectForKey:@"publicKey"] options:0] cnContactId:cnContactId verificationLevel:kVerificationLevelServerVerified featureMask:nil alerts:NO];
- }
- DDLogNotice(@"ContactSync: Found %lu contacts", (unsigned long)allIdentities.count);
- // trigger updating of status for identities
- dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremask for all contacts"];
- [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
- });
-
- if (onCompletion != nil) {
- dispatch_async(dispatch_get_main_queue(), ^{
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: onCompletion"];
- onCompletion(YES);
- });
- } else {
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: finished"];
- }
- });
-
- } onError:^(NSError *error) {
- DDLogError(@"Synchronize address book failed: %@", error);
- [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Synchronize address book failed: %@", error]];
- if (onError != nil) {
- dispatch_async(dispatch_get_main_queue(), ^{
- onError(error);
- });
- }
- }];
- }
- - (void)linkedIdentities:(NSString*)email mobileNo:(NSString*)mobileNo onCompletion:(void(^)(NSArray *identities))onCompletion {
-
- NSArray *emailHashesBase64 = [NSArray array];
- NSArray *mobileNoHashesBase64 = [NSArray array];
-
- if (email.length > 0) {
- NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
- NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized];
- emailHashesBase64 = @[emailHashBase64];
- }
-
- if (mobileNo.length > 0) {
- /* normalize phone number first */
- PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance];
- NSString *countryCode = [PhoneNumberNormalizer userRegion];
- NSString *mobileNoNormalized = [normalizer phoneNumberToE164:mobileNo withDefaultRegion:countryCode prettyFormat:nil];
- if (mobileNoNormalized != nil) {
- NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized];
- mobileNoHashesBase64 = @[mobileNoHashBase64];
- }
- }
- if (emailHashesBase64.count > 0 || mobileNoHashesBase64.count > 0) {
- ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
- [conn matchIdentitiesWithEmailHashes:emailHashesBase64 mobileNoHashes:mobileNoHashesBase64 includeInactive:YES onCompletion:^(NSArray *identities, int checkInterval) {
- onCompletion(identities);
- } onError:^(NSError *error) {
- DDLogError(@"linked identities failed: %@", error);
- NSArray *emptyArray = [NSArray array];
- onCompletion(emptyArray);
- }];
- } else {
- NSArray *emptyArray = [NSArray array];
- onCompletion(emptyArray);
- }
- }
- - (NSArray *)allIdentities {
- NSFetchRequest *fetchRequest = [entityManager.entityFetcher fetchRequestForEntity:@"Contact"];
- fetchRequest.propertiesToFetch = @[@"identity"];
- NSArray *result = [entityManager.entityFetcher executeFetchRequest:fetchRequest];
- if (result != nil) {
- return [self identitiesForContacts:result];
- } else {
- DDLogError(@"Cannot get identities");
- return nil;
- }
- }
- - (NSArray *)contactsWithVerificationLevel:(NSInteger)verificationLevel {
- return [entityManager.entityFetcher contactsWithVerificationLevel:verificationLevel];
- }
- - (NSArray *)contactsWithFeatureMaskNil {
- return [entityManager.entityFetcher contactsWithFeatureMaskNil];
- }
- - (NSArray *)allContacts {
- return [entityManager.entityFetcher allContacts];
- }
- - (void)orderChanged:(NSNotification*)notification {
- [entityManager performAsyncBlockAndSafe:^{
- /* update display name and sort index of all contacts */
- NSArray *allContacts = [entityManager.entityFetcher allContacts];
-
- if (allContacts != nil) {
- for (Contact *contact in allContacts) {
- /* set last name again to trigger update of display name and sort index */
- contact.lastName = contact.lastName;
- }
- }
- }];
- }
- - (NSArray *)identitiesForContacts:(NSArray *)contacts {
- NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
- for (Contact *contact in contacts) {
- [identities addObject:contact.identity];
- }
-
- return identities;
- }
- - (NSArray *)validIdentitiesForContacts:(NSArray *)contacts {
- NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
- for (Contact *contact in contacts) {
- if (contact.state.intValue != kStateInvalid) {
- [identities addObject:contact.identity];
- }
- }
-
- return identities;
- }
- - (void)updateFeatureMasksForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
- NSArray *identities = [self identitiesForContacts: contacts];
-
- ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
- [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) {
- [entityManager performSyncBlockAndSafe:^{
- for (NSInteger i=0; i<[identities count]; i++) {
- NSNumber *featureMask = [featureMasks objectAtIndex: i];
-
- if (featureMask.integerValue >= 0) {
- NSString *identityString = [identities objectAtIndex:i];
- Contact *contact = [entityManager.entityFetcher contactForId: identityString];
- contact.featureMask = featureMask;
- }
- }
- }];
-
- onCompletion();
- } onError:^(NSError *error) {
- onError(error);
- }];
- }
- - (void)updateFeatureMasksForIdentities:(NSArray *)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
- ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
- [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) {
- [entityManager performSyncBlockAndSafe:^{
- for (NSInteger i=0; i<[identities count]; i++) {
- NSNumber *featureMask = [featureMasks objectAtIndex: i];
-
- if (featureMask.integerValue >= 0) {
- NSString *identityString = [identities objectAtIndex:i];
- Contact *contact = [entityManager.entityFetcher contactForId: identityString];
- contact.featureMask = featureMask;
- }
- }
- }];
-
- onCompletion();
- } onError:^(NSError *error) {
- onError(error);
- }];
- }
- - (BOOL)needCheckStatus:(BOOL)ignoreInterval {
- if (ignoreInterval) {
- return YES;
- }
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
- NSDate *dateLastCheck = [defaults objectForKey:@"DateLastCheckStatus"];
- if (dateLastCheck == nil) {
- return true;
- }
-
- NSInteger checkInterval = [self getCheckStatusInterval];
- NSDate *dateOfNextCheck = [dateLastCheck dateByAddingTimeInterval:checkInterval];
- NSDate *now = [NSDate date];
- return [now timeIntervalSinceDate:dateOfNextCheck] > 0;
- }
- - (void)setupCheckStatusTimer {
- NSUserDefaults *defaults = [AppGroup userDefaults];
-
- NSDate *now = [NSDate date];
- [defaults setObject:now forKey:@"DateLastCheckStatus"];
- [defaults synchronize];
-
- NSInteger checkInterval = [self getCheckStatusInterval];
- checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(updateStatusForAllContacts) userInfo:nil repeats:NO];
- }
- - (NSInteger) getCheckStatusInterval {
- NSUserDefaults *defaults = [AppGroup userDefaults];
- NSInteger checkInterval = [defaults integerForKey:@"CheckStatusInterval"];
-
- return MAX(checkInterval, MIN_CHECK_INTERVAL);
- }
- - (void)updateStatusForAllContacts {
- [self updateStatusForAllContactsIgnoreInterval:NO];
- }
- - (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval {
- if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
- NSArray *contacts = [entityManager.entityFetcher allContacts];
- [self updateStatusForContacts:contacts onCompletion:^() {
- [self setupCheckStatusTimer];
- } onError:^(){
- [self setupCheckStatusTimer];
- }];
- } else {
- if ([self needCheckStatus:ignoreInterval] == NO) {
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: do not update status and featuremasks"];
- return;
- }
-
- NSArray *contacts = [entityManager.entityFetcher allContacts];
- [self updateStatusForContacts:contacts onCompletion:^() {
- [self setupCheckStatusTimer];
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremasks finished"];
- } onError:^(){
- [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status featuremasks finished with error"];
- [self setupCheckStatusTimer];
- }];
- }
- }
- - (void)updateStatusForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(void))onError {
- NSArray *identities = [self validIdentitiesForContacts: contacts];
- ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
- [conn checkStatusOfIdentities:identities onCompletion:^(NSArray *states, NSArray *types, NSArray *featureMasks, int checkInterval) {
- [entityManager performSyncBlockAndSafe:^{
- NSMutableOrderedSet *workIdentities = [NSMutableOrderedSet new];
- for (NSInteger i=0; i<[identities count]; i++) {
- NSNumber *state = [states objectAtIndex: i];
- NSNumber *type = [types objectAtIndex:i];
- NSNumber *featureMask = [featureMasks objectAtIndex:i];
-
- NSString *identityString = [identities objectAtIndex:i];
- Contact *contact = [entityManager.entityFetcher contactForId: identityString];
- if (![contact.state isEqualToNumber:state])
- contact.state = state;
-
- if ([type isEqualToNumber:@1]) {
- [workIdentities addObject:contact.identity];
- }
-
- if (![featureMask isEqual:[NSNull null]]) {
- if (![contact.featureMask isEqualToNumber:featureMask]) {
- contact.featureMask = featureMask;
- }
- }
- }
- if (![[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
- [UserSettings sharedUserSettings].workIdentities = workIdentities;
- }
- }];
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults setInteger:checkInterval forKey:@"CheckStatusInterval"];
- [defaults synchronize];
-
- onCompletion();
- } onError:^(NSError *error) {
- DDLogError(@"Status update failed: %@", error);
- onError();
- }];
- }
- - (void)updateAllContactsToCNContact {
- #pragma clang diagnostic push
- #pragma clang diagnostic ignored "-Wdeprecated-declarations"
- NSUserDefaults *defaults = [AppGroup userDefaults];
- if ([defaults boolForKey:@"AlreadyUpdatedToCNContacts"]) {
- return;
- }
-
- NSArray *linkedContacts = [[entityManager.entityFetcher allContacts] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
- Contact *contact = (Contact *)evaluatedObject;
- return contact.abRecordId != nil && contact.abRecordId.intValue != 0;
- }]];
- if (linkedContacts == nil || linkedContacts.count == 0) {
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"];
- [defaults synchronize];
-
- return;
- }
- CNContactStore *cnAddressBook = [CNContactStore new];
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- ABAddressBookRef addressBook = nil;
-
- int nupdated = 0;
- for (Contact *contact in linkedContacts) {
-
- if (addressBook == nil) {
- addressBook = ABAddressBookCreate();
- if (addressBook == nil)
- return;
- }
-
- ABRecordRef abPerson = ABAddressBookGetPersonWithRecordID(addressBook, contact.abRecordId.intValue);
- if (abPerson != nil) {
- NSString *firstName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonFirstNameProperty));
- NSString *lastName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonLastNameProperty));
- NSString *middleName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonMiddleNameProperty));
- NSString *company = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonOrganizationProperty));
- NSString *fullName = [NSString stringWithFormat:@"%@ %@ %@", firstName, middleName, lastName];
-
- ABMutableMultiValueRef multiPhone = ABRecordCopyValue(abPerson, kABPersonPhoneProperty);
- NSMutableArray *personPhones = [NSMutableArray new];
- if (ABMultiValueGetCount(multiPhone) > 0) {
-
- for (CFIndex i = 0; i < ABMultiValueGetCount(multiPhone); i++) {
- CFStringRef phoneRef = ABMultiValueCopyValueAtIndex(multiPhone, i);
- [personPhones addObject:(__bridge NSString *)phoneRef];
- CFRelease(phoneRef);
- }
- }
- CFRelease(multiPhone);
-
- ABMutableMultiValueRef multiEmail = ABRecordCopyValue(abPerson, kABPersonEmailProperty);
- NSMutableArray *personEmails = [NSMutableArray new];
- if (ABMultiValueGetCount(multiEmail) > 0) {
-
- for (CFIndex i = 0; i < ABMultiValueGetCount(multiEmail); i++) {
- CFStringRef emailRef = ABMultiValueCopyValueAtIndex(multiEmail, i);
- [personEmails addObject:(__bridge NSString *)emailRef];
- CFRelease(emailRef);
- }
- }
- CFRelease(multiEmail);
-
- // Check is there a CNContact for the ABPerson
- NSPredicate *predicate = [CNContact predicateForContactsMatchingName:fullName];
- NSError *error;
- NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- } else {
- if (cnContacts.count == 1) {
- NSLog(@"Found the CNContact for ABPerson; Identifier: %@", [((CNContact *)cnContacts.firstObject) identifier]);
- [entityManager performSyncBlockAndSafe:^{
- contact.cnContactId = [((CNContact *)cnContacts.firstObject) identifier];
- }];
- }
- else if (cnContacts.count > 1) {
- // Find correct contact in array
- NSMutableArray *phoneEmailMatch = [NSMutableArray new];
- NSMutableArray *phoneMatch = [NSMutableArray new];
- NSMutableArray *emailMatch = [NSMutableArray new];
-
- for (CNContact *contact in cnContacts) {
- if ([company isEqualToString:contact.organizationName]) {
- // compare ABPerson numbers with CNContact numbers
- BOOL foundPhone = NO;
- for (NSString *abPhone in personPhones) {
- for (CNLabeledValue *label in contact.phoneNumbers) {
- NSString *phoneNumber = [label.value stringValue];
- if (phoneNumber.length > 0) {
- if ([phoneNumber isEqualToString:abPhone]) {
- foundPhone = YES;
- } else {
- foundPhone = NO;
- }
- }
- }
- }
-
- // compare ABPerson emails with CNContact emails
- BOOL foundEmail = NO;
- for (NSString *abEmail in personEmails) {
- for (CNLabeledValue *label in contact.emailAddresses) {
- NSString *email = label.value;
- if (email.length > 0) {
- if ([email isEqualToString:abEmail]) {
- foundEmail = YES;
- } else {
- foundEmail = NO;
- }
- }
- }
- }
-
- if (foundEmail && foundPhone) {
- [phoneEmailMatch addObject:contact];
- } else {
- if (foundEmail) {
- [emailMatch addObject:contact];
- }
- if (foundPhone) {
- [phoneMatch addObject:contact];
- }
- }
- }
- }
-
- // compare is only one contact with mail and phone match
- if (phoneEmailMatch.count == 1) {
- [entityManager performSyncBlockAndSafe:^{
- NSLog(@"Found phone and email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneEmailMatch.firstObject) identifier]);
- contact.cnContactId = [((CNContact *)phoneEmailMatch.firstObject) identifier];
- }];
- }
- else if (phoneMatch.count == 1 && emailMatch.count == 0) {
- [entityManager performSyncBlockAndSafe:^{
- NSLog(@"Found phone of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneMatch.firstObject) identifier]);
- contact.cnContactId = [((CNContact *)phoneMatch.firstObject) identifier];
- }];
- }
- else if (emailMatch.count == 1 && phoneMatch.count == 0) {
- [entityManager performSyncBlockAndSafe:^{
- NSLog(@"Found email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)emailMatch.firstObject) identifier]);
- contact.cnContactId = [((CNContact *)emailMatch.firstObject) identifier];
- }];
- } else {
- NSLog(@"Found %lu contacts that could match", phoneEmailMatch.count + phoneMatch.count + emailMatch.count);
- }
- }
- else {
- NSLog(@"Found no CNContact for ABPerson");
- // skip
- }
- }
- nupdated++;
- }
- }
-
- if (addressBook != nil)
- CFRelease(addressBook);
- DDLogInfo(@"Updated %d contacts to CNContact", nupdated);
-
- NSUserDefaults *defaults = [AppGroup userDefaults];
- [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"];
- [defaults synchronize];
- }
- }];
- #pragma clang diagnostic pop
- }
- - (void)cnContactAskAccessEmailsForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler {
- if (contact.cnContactId == nil)
- completionHandler(YES, nil);
-
- __block NSArray *cnContacts;
- CNContactStore *cnAddressBook = [CNContactStore new];
-
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- NSError *error;
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- completionHandler(YES, nil);
- } else {
- cnContacts = tmpCnContacts;
-
- NSMutableArray *emails = [NSMutableArray new];
- if (cnContacts.count == 1) {
- for (CNContact *person in cnContacts) {
- for (CNLabeledValue *label in person.emailAddresses) {
- NSMutableDictionary *dict = [NSMutableDictionary new];
- NSString *emailLabel = label.label;
- NSString *email = label.value;
- if (email.length > 0) {
- [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"];
- [dict setValue:email forKey:@"address"];
- [emails addObject:dict];
- }
- }
- }
- }
- completionHandler(YES, emails);
- }
- } else {
- completionHandler(NO, nil);
- }
- }];
- }
- - (void)cnContactAskAccessPhoneNumbersForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler {
- if (contact.cnContactId == nil)
- completionHandler(YES, nil);
-
- __block NSArray *cnContacts;
- CNContactStore *cnAddressBook = [CNContactStore new];
-
- [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
- if (granted == YES) {
- NSError *error;
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- completionHandler(YES, nil);
- } else {
- cnContacts = tmpCnContacts;
-
- NSMutableArray *phoneNumbers = [NSMutableArray new];
- if (cnContacts.count == 1) {
- for (CNContact *person in cnContacts) {
- for (CNLabeledValue *label in person.phoneNumbers) {
- NSMutableDictionary *dict = [NSMutableDictionary new];
- NSString *phoneLabel = label.label;
- NSString *phone = [label.value stringValue];
- if (phone.length > 0) {
- [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"];
- [dict setValue:phone forKey:@"number"];
- [phoneNumbers addObject:dict];
- }
- }
- }
- }
- completionHandler(YES, phoneNumbers);
- }
- } else {
- completionHandler(NO, nil);
- }
- }];
- }
- - (NSArray *)cnContactEmailsForContact:(Contact *)contact {
- if (contact.cnContactId == nil)
- return nil;
-
- __block NSArray *cnContacts;
- CNContactStore *cnAddressBook = [CNContactStore new];
-
- NSError *error;
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- return nil;
- } else {
- cnContacts = tmpCnContacts;
-
- NSMutableArray *emails = [NSMutableArray new];
- if (cnContacts.count == 1) {
- for (CNContact *person in cnContacts) {
- for (CNLabeledValue *label in person.emailAddresses) {
- NSMutableDictionary *dict = [NSMutableDictionary new];
- NSString *emailLabel = label.label;
- NSString *email = label.value;
- if (email.length > 0) {
- [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"];
- [dict setValue:email forKey:@"address"];
- [emails addObject:dict];
- }
- }
- }
- }
- return emails;
- }
- }
- - (NSArray *)cnContactPhoneNumbersForContact:(Contact *)contact {
- if (contact.cnContactId == nil)
- return nil;
-
- __block NSArray *cnContacts;
- CNContactStore *cnAddressBook = [CNContactStore new];
-
- NSError *error;
- NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
- NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
- if (error) {
- NSLog(@"error fetching contacts %@", error);
- return nil;
- } else {
- cnContacts = tmpCnContacts;
-
- NSMutableArray *phoneNumbers = [NSMutableArray new];
- if (cnContacts.count == 1) {
- for (CNContact *person in cnContacts) {
- for (CNLabeledValue *label in person.phoneNumbers) {
- NSMutableDictionary *dict = [NSMutableDictionary new];
- NSString *phoneLabel = label.label;
- NSString *phone = [label.value stringValue];
- if (phone.length > 0) {
- [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"];
- [dict setValue:phone forKey:@"number"];
- [phoneNumbers addObject:dict];
- }
- }
- }
- }
- return phoneNumbers;
- }
- }
- - (NSString*)hashEmailBase64:(NSString*)email {
- NSData *emailHashKeyData = [NSData dataWithBytes:emailHashKey length:sizeof(emailHashKey)];
- return [[CryptoUtils hmacSha256ForData:[email dataUsingEncoding:NSASCIIStringEncoding] key:emailHashKeyData] base64EncodedStringWithOptions:0];
- }
- - (NSString*)hashMobileNoBase64:(NSString*)mobileNo {
- NSData *mobileNoHashKeyData = [NSData dataWithBytes:mobileNoHashKey length:sizeof(mobileNoHashKey)];
- return [[CryptoUtils hmacSha256ForData:[mobileNo dataUsingEncoding:NSASCIIStringEncoding] key:mobileNoHashKeyData] base64EncodedStringWithOptions:0];
- }
- @end
|