ContactStore.m 69 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455
  1. // _____ _
  2. // |_ _| |_ _ _ ___ ___ _ __ __ _
  3. // | | | ' \| '_/ -_) -_) ' \/ _` |_
  4. // |_| |_||_|_| \___\___|_|_|_\__,_(_)
  5. //
  6. // Threema iOS Client
  7. // Copyright (c) 2012-2020 Threema GmbH
  8. //
  9. // This program is free software: you can redistribute it and/or modify
  10. // it under the terms of the GNU Affero General Public License, version 3,
  11. // as published by the Free Software Foundation.
  12. //
  13. // This program is distributed in the hope that it will be useful,
  14. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  15. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  16. // GNU Affero General Public License for more details.
  17. //
  18. // You should have received a copy of the GNU Affero General Public License
  19. // along with this program. If not, see <https://www.gnu.org/licenses/>.
  20. #import <AddressBook/AddressBook.h>
  21. #import <Contacts/Contacts.h>
  22. #import "PhoneNumberNormalizer.h"
  23. #import "ContactStore.h"
  24. #import "NSString+Hex.h"
  25. #import "Contact.h"
  26. #import "ServerAPIConnector.h"
  27. #import "MyIdentityStore.h"
  28. #import "Utils.h"
  29. #import "UserSettings.h"
  30. #import "ProtocolDefines.h"
  31. #import "EntityManager.h"
  32. #import "ThreemaError.h"
  33. #import "AppGroup.h"
  34. #import "WorkDataFetcher.h"
  35. #import "ValidationLogger.h"
  36. #import "IdentityInfoFetcher.h"
  37. #import "CryptoUtils.h"
  38. #import "TrustedContacts.h"
  39. #define MIN_CHECK_INTERVAL 5*60
  40. #ifdef DEBUG
  41. static const DDLogLevel ddLogLevel = DDLogLevelInfo;
  42. #else
  43. static const DDLogLevel ddLogLevel = DDLogLevelWarning;
  44. #endif
  45. 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};
  46. 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};
  47. static const NSTimeInterval minimumSyncInterval = 30; /* avoid multiple concurrent syncs, e.g. triggered by interval timer + incoming message from unknown user */
  48. @implementation ContactStore {
  49. NSDate *lastMaxModificationDate;
  50. NSDate *lastFullSyncDate;
  51. NSTimer *checkStatusTimer;
  52. dispatch_queue_t syncQueue;
  53. EntityManager *entityManager;
  54. }
  55. + (ContactStore*)sharedContactStore {
  56. static ContactStore *instance;
  57. @synchronized (self) {
  58. if (!instance)
  59. instance = [[ContactStore alloc] init];
  60. }
  61. return instance;
  62. }
  63. - (id)init
  64. {
  65. self = [super init];
  66. if (self) {
  67. syncQueue = dispatch_queue_create("ch.threema.contactsync", DISPATCH_QUEUE_SERIAL);
  68. dispatch_set_target_queue(syncQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0));
  69. /* register a callback to get information about address book changes */
  70. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(addressBookChangeDetected:) name:CNContactStoreDidChangeNotification object:nil];
  71. [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(orderChanged:) name:@"ThreemaContactsOrderChanged" object:nil];
  72. entityManager = [[EntityManager alloc] init];
  73. /* update display/sort order prefs to match system */
  74. if (@available(iOS 11.0, *)) {
  75. BOOL sortOrder = [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName;
  76. [[UserSettings sharedUserSettings] setSortOrderFirstName:sortOrder];
  77. } else {
  78. #pragma clang diagnostic push
  79. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  80. [[UserSettings sharedUserSettings] setSortOrderFirstName:(ABPersonGetSortOrdering() == kABPersonSortByFirstName)];
  81. #pragma clang diagnostic pop
  82. }
  83. }
  84. return self;
  85. }
  86. - (void)dealloc {
  87. [checkStatusTimer invalidate];
  88. }
  89. - (void)addressBookChangeDetected:(NSNotification *)notification {
  90. DDLogInfo(@"Address book change detected");
  91. [[ValidationLogger sharedValidationLogger] logString:@"Address book change detected"];
  92. [self synchronizeAddressBookForceFullSync:NO onCompletion:^(BOOL addressBookAccessGranted) {
  93. [self updateAllContacts];
  94. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
  95. } onError:^(NSError *error) {
  96. [self updateAllContacts];
  97. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
  98. }];
  99. }
  100. - (Contact*)contactForIdentity:(NSString *)identity {
  101. /* check in local DB first */
  102. EntityManager *entityManager = [[EntityManager alloc] init];
  103. Contact *contact = [entityManager.entityFetcher contactForId: identity];
  104. return contact;
  105. }
  106. - (void)addContactWithIdentity:(NSString *)identity verificationLevel:(int32_t)verificationLevel onCompletion:(void(^)(Contact *contact, BOOL alreadyExists))onCompletion onError:(void(^)(NSError *error))onError {
  107. /* check in local DB first */
  108. EntityManager *entityManager = [[EntityManager alloc] init];
  109. NSError *error;
  110. Contact *contact = [entityManager.entityFetcher contactForId:identity error:&error];
  111. if (contact) {
  112. onCompletion(contact, YES);
  113. return;
  114. }
  115. if (error != nil) {
  116. onError(error);
  117. }
  118. /* not found - request from server */
  119. ServerAPIConnector *apiConnector = [[ServerAPIConnector alloc] init];
  120. [apiConnector fetchIdentityInfo:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
  121. /* save new contact */
  122. dispatch_async(dispatch_get_main_queue(), ^{
  123. /* save new contact */
  124. Contact *contact = [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:verificationLevel state:state type:type featureMask:featureMask alerts:YES];
  125. /* force synchronisation */
  126. [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
  127. [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil];
  128. [[NSNotificationCenter defaultCenter] postNotificationName:kSafeBackupTrigger object:nil];
  129. onCompletion(contact, NO);
  130. });
  131. } onError:^(NSError *error) {
  132. onError(error);
  133. }];
  134. }
  135. - (Contact*)addContactWithIdentity:(NSString*)identity publicKey:(NSData*)publicKey cnContactId:(NSString *)cnContactId verificationLevel:(int32_t)verificationLevel featureMask:(NSNumber *)featureMask alerts:(BOOL)alerts {
  136. return [self addContactWithIdentity:identity publicKey:publicKey cnContactId:cnContactId verificationLevel:verificationLevel state:nil type:nil featureMask:featureMask alerts:alerts];
  137. }
  138. - (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 {
  139. /* Make sure this is not our own identity */
  140. if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
  141. DDLogInfo(@"Ignoring attempt to add own identity");
  142. return nil;
  143. }
  144. /* Check if we already have a contact with this identity */
  145. __block BOOL added = NO;
  146. __block Contact *contact;
  147. [entityManager performSyncBlockAndSafe:^{
  148. contact = [entityManager.entityFetcher contactForId: identity];
  149. if (contact) {
  150. DDLogInfo(@"Found existing contact with identity %@", identity);
  151. if (![publicKey isEqualToData:contact.publicKey]) {
  152. DDLogError(@"Public key doesn't match for existing identity %@!", identity);
  153. if (alerts) {
  154. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationErrorPublicKeyMismatch object:nil userInfo:nil];
  155. }
  156. return;
  157. }
  158. } else {
  159. added = YES;
  160. contact = [entityManager.entityCreator contact];
  161. contact.identity = identity;
  162. contact.publicKey = publicKey;
  163. contact.featureMask = featureMask;
  164. if (state != nil) {
  165. contact.state = state;
  166. }
  167. if (type != nil) {
  168. if ([type isEqualToNumber:@1]) {
  169. NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities];
  170. if (![workIdentities containsObject:contact.identity])
  171. [workIdentities addObject:contact.identity];
  172. [UserSettings sharedUserSettings].workIdentities = workIdentities;
  173. }
  174. }
  175. }
  176. if (contact.verificationLevel == nil || (contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified)
  177. contact.verificationLevel = [NSNumber numberWithInt:verificationLevel];
  178. if (contact.workContact == nil) {
  179. if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
  180. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
  181. contact.workContact = [NSNumber numberWithBool:YES];
  182. } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
  183. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
  184. contact.workContact = [NSNumber numberWithBool:YES];
  185. } else {
  186. contact.workContact = [NSNumber numberWithBool:NO];
  187. }
  188. }
  189. if ([contact.workContact isEqualToNumber:[NSNumber numberWithBool:YES]] && (contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified)) {
  190. if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
  191. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
  192. } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
  193. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
  194. }
  195. }
  196. // check if this is a trusted contact (like *THREEMA)
  197. if ([TrustedContacts isTrustedContactWithIdentity:identity publicKey:publicKey]) {
  198. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
  199. }
  200. if (cnContactId) {
  201. if (contact.cnContactId != nil) {
  202. if (![contact.cnContactId isEqualToString:cnContactId]) {
  203. /* contact is already linked to a different CNContactID - check if the name matches;
  204. if so, the CNContactID may have changed and we need to re-link */
  205. CNContactStore *cnAddressBook = [CNContactStore new];
  206. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  207. if (granted == YES) {
  208. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactId]];
  209. NSError *error;
  210. NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  211. if (error) {
  212. NSLog(@"error fetching contacts %@", error);
  213. } else {
  214. if (cnContacts.count == 1) {
  215. CNContact *foundContact = cnContacts.firstObject;
  216. NSString *firstName = foundContact.givenName;
  217. NSString *lastName = foundContact.familyName;
  218. if (contact.firstName != nil && contact.firstName.length > 0 && contact.lastName != nil && contact.lastName.length > 0) {
  219. if ([firstName isEqualToString:contact.firstName] && [lastName isEqualToString:contact.lastName]) {
  220. DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
  221. [self linkContact:contact toCnContactId:cnContactId];
  222. }
  223. }
  224. else if (contact.firstName != nil && contact.firstName.length > 0) {
  225. if ([firstName isEqualToString:contact.firstName]) {
  226. DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
  227. [self linkContact:contact toCnContactId:cnContactId];
  228. }
  229. }
  230. else if (contact.lastName != nil && contact.lastName.length > 0) {
  231. if ([lastName isEqualToString:contact.lastName]) {
  232. DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
  233. [self linkContact:contact toCnContactId:cnContactId];
  234. }
  235. }
  236. else {
  237. // No name for the contact to compare, replace the cncontactid
  238. DDLogInfo(@"Address book record ID has changed for %@ %@ (%@ -> %@) - relinking", firstName, lastName, contact.cnContactId, cnContactId);
  239. [self linkContact:contact toCnContactId:cnContactId];
  240. }
  241. }
  242. }
  243. }
  244. }];
  245. }
  246. } else {
  247. [self linkContact:contact toCnContactId:cnContactId];
  248. }
  249. }
  250. }];
  251. if (added)
  252. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact];
  253. return contact;
  254. }
  255. - (Contact *)addWorkContactWithIdentity:(NSString *)identity publicKey:(NSData*)publicKey firstname:(NSString *)firstname lastname:(NSString *)lastname {
  256. /* Make sure this is not our own identity */
  257. if ([MyIdentityStore sharedMyIdentityStore].isProvisioned && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
  258. DDLogInfo(@"Ignoring attempt to add own identity");
  259. return nil;
  260. }
  261. /* Check if we already have a contact with this identity */
  262. __block BOOL added = NO;
  263. __block Contact *contact;
  264. [entityManager performSyncBlockAndSafe:^{
  265. contact = [entityManager.entityFetcher contactForId: identity];
  266. if (contact) {
  267. DDLogInfo(@"Found existing contact with identity %@", identity);
  268. if (![publicKey isEqualToData:contact.publicKey]) {
  269. DDLogError(@"Public key doesn't match for existing identity %@!", identity);
  270. return;
  271. }
  272. } else {
  273. added = YES;
  274. contact = [entityManager.entityCreator contact];
  275. contact.identity = identity;
  276. contact.publicKey = publicKey;
  277. contact.firstName = firstname;
  278. contact.lastName = lastname;
  279. NSMutableOrderedSet *workIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[UserSettings sharedUserSettings].workIdentities];
  280. if (![workIdentities containsObject:contact.identity])
  281. [workIdentities addObject:contact.identity];
  282. [UserSettings sharedUserSettings].workIdentities = workIdentities;
  283. }
  284. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
  285. contact.workContact = [NSNumber numberWithBool:YES];
  286. }];
  287. if (added)
  288. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddedContact object:contact];
  289. [self updateFeatureMasksForContacts:@[contact] onCompletion:^{
  290. } onError:^(NSError *error) {
  291. }];
  292. return contact;
  293. }
  294. - (void)updateContact:(Contact*)contact {
  295. if (contact.cnContactId == nil) {
  296. return;
  297. }
  298. CNContactStore *cnAddressBook = [CNContactStore new];
  299. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  300. if (granted == YES) {
  301. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  302. NSError *error;
  303. NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  304. if (error) {
  305. NSLog(@"error fetching contacts %@", error);
  306. } else {
  307. CNContact *foundContact = cnContacts.firstObject;
  308. [self _updateContact:contact withCnContact:foundContact];
  309. }
  310. }
  311. }];
  312. }
  313. - (void)_updateContact:(Contact *)contact withCnContact:(CNContact *)cnContact {
  314. NSString *newFirstName = cnContact.givenName;
  315. NSString *newLastName = cnContact.familyName;
  316. /* no name? try company name and e-mail address (Outlook auto-generated contacts etc.) */
  317. if (newFirstName.length == 0 && newLastName.length == 0) {
  318. NSString *companyName = cnContact.organizationName;
  319. if (companyName.length > 0) {
  320. newLastName = companyName;
  321. } else {
  322. /* no name? try e-mail address (Outlook auto-generated contacts etc.) */
  323. if (cnContact.emailAddresses.count > 0) {
  324. newLastName = ((CNLabeledValue *)cnContact.emailAddresses.firstObject).value;
  325. }
  326. }
  327. }
  328. if (newFirstName != contact.firstName && ![newFirstName isEqual:contact.firstName])
  329. contact.firstName = newFirstName;
  330. if (newLastName != contact.lastName && ![newLastName isEqual:contact.lastName])
  331. contact.lastName = newLastName;
  332. /* get image, if any */
  333. NSData *newImageData = nil;
  334. if (cnContact.imageDataAvailable) {
  335. newImageData = cnContact.thumbnailImageData;
  336. }
  337. if (newImageData != contact.imageData && ![newImageData isEqualToData:contact.imageData])
  338. contact.imageData = newImageData;
  339. DDLogVerbose(@"Updated contact %@ %@", contact.firstName, contact.lastName);
  340. }
  341. - (void)updateAllContacts {
  342. NSArray *allContacts = [entityManager.entityFetcher allContacts];
  343. if (allContacts == nil || allContacts.count == 0) {
  344. return;
  345. }
  346. // Migration of verfication level kVerificationLevelWorkVerified and kVerificationLevelWorkFullyVerified to flag workContact
  347. [entityManager performAsyncBlockAndSafe:^{
  348. for (Contact *contact in allContacts) {
  349. if (contact.workContact == nil || contact.verificationLevel.intValue == kVerificationLevelWorkVerified || contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
  350. if (contact.verificationLevel.intValue == kVerificationLevelWorkVerified) {
  351. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelServerVerified];
  352. contact.workContact = [NSNumber numberWithBool:YES];
  353. } else if (contact.verificationLevel.intValue == kVerificationLevelWorkFullyVerified) {
  354. contact.verificationLevel = [NSNumber numberWithInt:kVerificationLevelFullyVerified];
  355. contact.workContact = [NSNumber numberWithBool:YES];
  356. } else {
  357. contact.workContact = [NSNumber numberWithBool:NO];
  358. }
  359. }
  360. }
  361. }];
  362. NSArray *linkedContacts = [allContacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
  363. Contact *contact = (Contact *)evaluatedObject;
  364. return contact.cnContactId != nil;
  365. }]];
  366. if (linkedContacts == nil || linkedContacts.count == 0) {
  367. return;
  368. }
  369. CNContactStore *cnAddressBook = [CNContactStore new];
  370. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  371. if (granted == YES) {
  372. [entityManager performAsyncBlockAndSafe:^{
  373. /* go through all contacts and resync with address book; only create
  374. address book ref when encountering the first contact that is linked */
  375. int nupdated = 0;
  376. for (Contact *contact in linkedContacts) {
  377. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  378. NSError *error;
  379. NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  380. if (error) {
  381. NSLog(@"error fetching contacts %@", error);
  382. } else {
  383. if (cnContacts != nil && cnContacts.count > 0) {
  384. CNContact *foundContact = cnContacts.firstObject;
  385. [self _updateContact:contact withCnContact:foundContact];
  386. nupdated++;
  387. }
  388. }
  389. }
  390. DDLogInfo(@"Updated %d contacts", nupdated);
  391. }];
  392. [self updateStatusForAllContacts];
  393. }
  394. }];
  395. }
  396. - (void)linkContact:(Contact*)contact toCnContactId:(NSString *)cnContactId {
  397. /* obtain first/last name from address book */
  398. [entityManager performSyncBlockAndSafe:^{
  399. contact.cnContactId = cnContactId;
  400. [self updateContact:contact];
  401. }];
  402. }
  403. - (void)unlinkContact:(Contact*)contact {
  404. [entityManager performSyncBlockAndSafe:^{
  405. contact.abRecordId = [NSNumber numberWithInt:0];
  406. contact.cnContactId = nil;
  407. contact.firstName = nil;
  408. contact.lastName = nil;
  409. contact.imageData = nil;
  410. }];
  411. }
  412. - (void)fetchPublicKeyForIdentity:(NSString*)identity onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
  413. [entityManager performBlock:^{
  414. // check in local DB first
  415. Contact *contact = [entityManager.entityFetcher contactForId:identity];
  416. if (contact.publicKey) {
  417. onCompletion(contact.publicKey);
  418. } else {
  419. // not found - request from server
  420. if ([UserSettings sharedUserSettings].blockUnknown) {
  421. DDLogVerbose(@"Block unknown contacts is on - discarding message");
  422. onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:kBlockUnknownContactErrorCode]);
  423. return;
  424. }
  425. [[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchIdentityInfoFor:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
  426. dispatch_async(dispatch_get_main_queue(), ^{
  427. // First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
  428. Contact *contact = [entityManager.entityFetcher contactForId:identity];
  429. if (contact.publicKey) {
  430. onCompletion(contact.publicKey);
  431. return;
  432. }
  433. // Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
  434. [self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:kVerificationLevelUnverified state:state type:type featureMask:featureMask alerts:NO];
  435. [self synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
  436. [WorkDataFetcher checkUpdateWorkDataForce:YES onCompletion:nil onError:nil];
  437. onCompletion(publicKey);
  438. });
  439. } onError:^(NSError * _Nonnull error) {
  440. onError(error);
  441. }];
  442. }
  443. }];
  444. }
  445. - (void)prefetchIdentityInfo:(NSSet*)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
  446. NSMutableSet *identitiesToFetch = [NSMutableSet set];
  447. // Skip identities that we already have a contact for
  448. for (NSString *identity in identities) {
  449. Contact *contact = [entityManager.entityFetcher contactForId:identity];
  450. if (!contact) {
  451. [identitiesToFetch addObject:identity];
  452. }
  453. }
  454. if ([identitiesToFetch count] == 0) {
  455. onCompletion();
  456. return;
  457. }
  458. [[IdentityInfoFetcher sharedIdentityInfoFetcher] prefetchIdentityInfo:identitiesToFetch onCompletion:onCompletion onError:onError];
  459. }
  460. - (void)upgradeContact:(Contact*)contact toVerificationLevel:(int)verificationLevel {
  461. if ((contact.verificationLevel.intValue < verificationLevel && contact.verificationLevel.intValue != kVerificationLevelFullyVerified) || verificationLevel == kVerificationLevelFullyVerified) {
  462. [entityManager performSyncBlockAndSafe:^{
  463. contact.verificationLevel = [NSNumber numberWithInt:verificationLevel];
  464. }];
  465. }
  466. }
  467. - (void)setWorkContact:(Contact *)contact workContact:(BOOL)workContact {
  468. [entityManager performSyncBlockAndSafe:^{
  469. contact.workContact = [NSNumber numberWithBool:workContact];
  470. }];
  471. }
  472. - (void)updateProfilePicture:(Contact *)contact imageData:(NSData *)imageData didFailWithError:(NSError **)error {
  473. UIImage *image = [UIImage imageWithData:imageData];
  474. if (image == nil) {
  475. *error = [ThreemaError threemaError:@"Image decoding failed"];
  476. return;
  477. }
  478. [entityManager performSyncBlockAndSafe:^{
  479. ImageData *dbImage = [entityManager.entityCreator imageData];
  480. dbImage.data = imageData;
  481. dbImage.width = [NSNumber numberWithInt:image.size.width];
  482. dbImage.height = [NSNumber numberWithInt:image.size.height];
  483. contact.contactImage = dbImage;
  484. }];
  485. [self removeProfilePictureRequest:contact.identity];
  486. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity];
  487. }
  488. - (void)deleteProfilePicture:(Contact *)contact {
  489. [entityManager performSyncBlockAndSafe:^{
  490. contact.contactImage = nil;
  491. }];
  492. [self removeProfilePictureRequest:contact.identity];
  493. [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationIdentityAvatarChanged object:contact.identity];
  494. }
  495. - (void)removeProfilePictureFlagForAllContacts {
  496. [entityManager performAsyncBlockAndSafe:^{
  497. NSArray *allContacts = [entityManager.entityFetcher allContacts];
  498. if (allContacts != nil) {
  499. for (Contact *contact in allContacts) {
  500. contact.profilePictureSended = NO;
  501. }
  502. }
  503. }];
  504. }
  505. - (void)removeProfilePictureFlagForContact:(NSString *)identity {
  506. [entityManager performAsyncBlockAndSafe:^{
  507. Contact *contact = [entityManager.entityFetcher contactForId:identity];
  508. if (contact != nil) {
  509. contact.profilePictureSended = NO;
  510. }
  511. }];
  512. }
  513. - (BOOL)existsProfilePictureRequest:(NSString *)identity {
  514. @synchronized (self) {
  515. return [[[UserSettings sharedUserSettings] profilePictureRequestList] containsObject:identity];
  516. }
  517. }
  518. - (void)removeProfilePictureRequest:(NSString *)identity {
  519. @synchronized (self) {
  520. if ([self existsProfilePictureRequest:identity]) {
  521. NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:[UserSettings sharedUserSettings].profilePictureRequestList];
  522. [profilePictureRequestList removeObject:identity];
  523. [UserSettings sharedUserSettings].profilePictureRequestList = profilePictureRequestList.allObjects;
  524. }
  525. }
  526. }
  527. - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
  528. [self synchronizeAddressBookForceFullSync:forceFullSync ignoreMinimumInterval:NO onCompletion:onCompletion onError:onError];
  529. }
  530. - (void)synchronizeAddressBookForceFullSync:(BOOL)forceFullSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
  531. if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
  532. return;
  533. }
  534. /* Get all entries from the user's address book, hash the e-mail addresses
  535. and phone numbers and send to the server. */
  536. if (![UserSettings sharedUserSettings].syncContacts) {
  537. // trigger updating of status for identities
  538. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
  539. [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
  540. if (onCompletion != nil) {
  541. dispatch_async(dispatch_get_main_queue(), ^{
  542. onCompletion(NO);
  543. });
  544. }
  545. });
  546. return;
  547. }
  548. CNContactStore *cnAddressBook = [CNContactStore new];
  549. if ([CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts] != CNAuthorizationStatusAuthorized) {
  550. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  551. if (granted == YES) {
  552. [self synchronizeAddressBookForceFullSync:forceFullSync onCompletion:onCompletion onError:onError];
  553. } else {
  554. DDLogInfo(@"Address book access has NOT been granted: %@", error);
  555. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
  556. [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
  557. });
  558. if (onCompletion != nil)
  559. dispatch_async(dispatch_get_main_queue(), ^{
  560. onCompletion(NO);
  561. });
  562. }
  563. }];
  564. return;
  565. }
  566. if (cnAddressBook == nil) {
  567. DDLogInfo(@"Address book is nil");
  568. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
  569. [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
  570. });
  571. return;
  572. }
  573. dispatch_async(syncQueue, ^{
  574. NSUserDefaults *defaults = [AppGroup userDefaults];
  575. NSDate *lastServerCheck = [defaults objectForKey:@"ContactsSyncLastCheck"];
  576. NSInteger lastServerCheckInterval = [defaults integerForKey:@"ContactsSyncLastCheckInterval"];
  577. BOOL fullServerSync = YES;
  578. /* calculate earliest date for next server check */
  579. if (lastServerCheck != nil) {
  580. if (-[lastServerCheck timeIntervalSinceNow] < lastServerCheckInterval) {
  581. DDLogInfo(@"Last server contacts sync less than %ld seconds ago", (long)lastServerCheckInterval);
  582. if (forceFullSync) {
  583. DDLogInfo(@"Forcing full sync");
  584. } else {
  585. fullServerSync = NO;
  586. }
  587. }
  588. }
  589. /* check if we are within the minimum interval */
  590. if (fullServerSync) {
  591. if (!ignoreMinimumInterval && lastFullSyncDate != nil && -[lastFullSyncDate timeIntervalSinceNow] < minimumSyncInterval) {
  592. DDLogInfo(@"Still within minimum interval - not syncing");
  593. if (onCompletion != nil)
  594. dispatch_async(dispatch_get_main_queue(), ^{
  595. onCompletion(YES);
  596. });
  597. return;
  598. }
  599. lastFullSyncDate = [NSDate date];
  600. }
  601. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: build all e-mail and phone number hashes"];
  602. /* extract all e-mail and phone number hashes from the user's address book */
  603. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  604. if (granted == YES) {
  605. NSError *error;
  606. NSMutableArray *allCNContacts = [NSMutableArray new];
  607. NSArray *containers = [cnAddressBook containersMatchingPredicate:nil error:&error];
  608. for (CNContainer *container in containers) {
  609. NSPredicate *predicate = [CNContact predicateForContactsInContainerWithIdentifier:container.identifier];
  610. NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  611. [allCNContacts addObjectsFromArray:cnContacts];
  612. }
  613. [self processAddressBookContacts:allCNContacts fullServerSync:fullServerSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
  614. }
  615. }];
  616. });
  617. }
  618. - (void)processAddressBookContacts:(NSArray*)contacts fullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
  619. NSUserDefaults *defaults = [AppGroup userDefaults];
  620. NSSet *emailLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
  621. NSSet *mobileNoLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
  622. PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance];
  623. NSString *countryCode = [PhoneNumberNormalizer userRegion];
  624. DDLogInfo(@"Current country code: %@", countryCode);
  625. NSMutableSet *emailHashesBase64 = [NSMutableSet set];
  626. NSMutableSet *mobileNoHashesBase64 = [NSMutableSet set];
  627. NSMutableDictionary *emailHashToCnContactId = [NSMutableDictionary dictionary];
  628. NSMutableDictionary *mobileNoHashToCnContactId = [NSMutableDictionary dictionary];
  629. for (CNContact *person in contacts) {
  630. NSString *cnContactId = person.identifier;
  631. NSString *name = [CNContactFormatter stringFromContact:person style:CNContactFormatterStyleFullName];
  632. for (CNLabeledValue *label in person.emailAddresses) {
  633. NSString *email = label.value;
  634. if (email.length > 0) {
  635. NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  636. NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized];
  637. [emailHashToCnContactId setObject:cnContactId forKey:emailHashBase64];
  638. [emailHashesBase64 addObject:emailHashBase64];
  639. /* Gmail address? If so, hash with the other domain as well */
  640. NSString *emailNormalizedAlt = nil;
  641. if ([emailNormalized hasSuffix:@"@gmail.com"])
  642. emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@gmail.com" withString:@"@googlemail.com"];
  643. else if ([emailNormalized hasSuffix:@"@googlemail.com"])
  644. emailNormalizedAlt = [emailNormalized stringByReplacingOccurrencesOfString:@"@googlemail.com" withString:@"@gmail.com"];
  645. if (emailNormalizedAlt != nil) {
  646. NSString *emailHashAltBase64 = [self hashEmailBase64:emailNormalizedAlt];
  647. [emailHashToCnContactId setObject:cnContactId forKey:emailHashAltBase64];
  648. [emailHashesBase64 addObject:emailHashAltBase64];
  649. }
  650. DDLogVerbose(@"%@ (%@): %@", name, cnContactId, emailNormalized);
  651. }
  652. }
  653. for (CNLabeledValue *label in person.phoneNumbers) {
  654. NSString *phone = [label.value stringValue];
  655. if (phone.length > 0) {
  656. /* normalize phone number first */
  657. NSString *mobileNoNormalized = [normalizer phoneNumberToE164:phone withDefaultRegion:countryCode prettyFormat:nil];
  658. if (mobileNoNormalized == nil)
  659. continue;
  660. NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized];
  661. [mobileNoHashToCnContactId setObject:cnContactId forKey:mobileNoHashBase64];
  662. [mobileNoHashesBase64 addObject:mobileNoHashBase64];
  663. DDLogVerbose(@"%@ (%@): %@", name, cnContactId, mobileNoNormalized);
  664. }
  665. }
  666. }
  667. if (!fullServerSync) {
  668. /* a full server sync is not scheduled right now, so remove any hashes that we checked last time from the list */
  669. for (NSString *emailHash in emailLastCheck) {
  670. [emailHashesBase64 removeObject:emailHash];
  671. }
  672. for (NSString *mobileNoHash in mobileNoLastCheck) {
  673. [mobileNoHashesBase64 removeObject:mobileNoHash];
  674. }
  675. }
  676. if (emailHashesBase64.count == 0 && mobileNoHashesBase64.count == 0) {
  677. DDLogInfo(@"No new contacts to synchronize");
  678. if (onCompletion != nil)
  679. dispatch_async(dispatch_get_main_queue(), ^{
  680. onCompletion(YES);
  681. });
  682. return;
  683. }
  684. [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"ContactSync: Start request %lu emails, %lu phonenumbers", (unsigned long)emailHashesBase64.count, (unsigned long)mobileNoHashesBase64.count]];
  685. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  686. [conn matchIdentitiesWithEmailHashes:[emailHashesBase64 allObjects] mobileNoHashes:[mobileNoHashesBase64 allObjects] includeInactive:NO onCompletion:^(NSArray *identities, int checkInterval) {
  687. if (fullServerSync) {
  688. [defaults setObject:[emailHashesBase64 allObjects] forKey:@"ContactsSyncLastEmailHashes"];
  689. [defaults setObject:[mobileNoHashesBase64 allObjects] forKey:@"ContactsSyncLastMobileNoHashes"];
  690. [defaults setObject:[NSDate date] forKey:@"ContactsSyncLastCheck"];
  691. } else {
  692. /* add the hashes we just checked to the full list */
  693. NSMutableArray *prevEmailHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
  694. NSMutableArray *prevMobileNoHashes = [NSMutableArray arrayWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
  695. [prevEmailHashes addObjectsFromArray:[emailHashesBase64 allObjects]];
  696. [prevMobileNoHashes addObjectsFromArray:[mobileNoHashesBase64 allObjects]];
  697. [defaults setObject:prevEmailHashes forKey:@"ContactsSyncLastEmailHashes"];
  698. [defaults setObject:prevMobileNoHashes forKey:@"ContactsSyncLastMobileNoHashes"];
  699. }
  700. [defaults setInteger:checkInterval forKey:@"ContactsSyncLastCheckInterval"];
  701. [defaults synchronize];
  702. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: Start core data stuff"];
  703. /* Core data stuff on main thread */
  704. dispatch_async(dispatch_get_main_queue(), ^{
  705. NSSet *excludedIds = [NSSet setWithArray:[UserSettings sharedUserSettings].syncExclusionList];
  706. NSMutableArray *allIdentities = [NSMutableArray new];
  707. for (NSDictionary *identityData in identities) {
  708. NSString *identity = [identityData objectForKey:@"identity"];
  709. /* ignore this ID? */
  710. if ([excludedIds containsObject:identity])
  711. continue;
  712. NSString *cnContactId = [emailHashToCnContactId objectForKey:[identityData objectForKey:@"emailHash"]];
  713. if (cnContactId == nil) {
  714. cnContactId = [mobileNoHashToCnContactId objectForKey:[identityData objectForKey:@"mobileNoHash"]];
  715. }
  716. if (cnContactId == nil) {
  717. continue;
  718. }
  719. DDLogVerbose(@"Adding identity %@ to contacts", identity);
  720. [allIdentities addObject:identity];
  721. [self addContactWithIdentity:identity publicKey:[[NSData alloc] initWithBase64EncodedString:[identityData objectForKey:@"publicKey"] options:0] cnContactId:cnContactId verificationLevel:kVerificationLevelServerVerified featureMask:nil alerts:NO];
  722. }
  723. DDLogNotice(@"ContactSync: Found %lu contacts", (unsigned long)allIdentities.count);
  724. // trigger updating of status for identities
  725. dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
  726. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremask for all contacts"];
  727. [self updateStatusForAllContactsIgnoreInterval: ignoreMinimumInterval];
  728. });
  729. if (onCompletion != nil) {
  730. dispatch_async(dispatch_get_main_queue(), ^{
  731. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: onCompletion"];
  732. onCompletion(YES);
  733. });
  734. } else {
  735. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: finished"];
  736. }
  737. });
  738. } onError:^(NSError *error) {
  739. DDLogError(@"Synchronize address book failed: %@", error);
  740. [[ValidationLogger sharedValidationLogger] logString:[NSString stringWithFormat:@"Synchronize address book failed: %@", error]];
  741. if (onError != nil) {
  742. dispatch_async(dispatch_get_main_queue(), ^{
  743. onError(error);
  744. });
  745. }
  746. }];
  747. }
  748. - (void)linkedIdentities:(NSString*)email mobileNo:(NSString*)mobileNo onCompletion:(void(^)(NSArray *identities))onCompletion {
  749. NSArray *emailHashesBase64 = [NSArray array];
  750. NSArray *mobileNoHashesBase64 = [NSArray array];
  751. if (email.length > 0) {
  752. NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
  753. NSString *emailHashBase64 = [self hashEmailBase64:emailNormalized];
  754. emailHashesBase64 = @[emailHashBase64];
  755. }
  756. if (mobileNo.length > 0) {
  757. /* normalize phone number first */
  758. PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer sharedInstance];
  759. NSString *countryCode = [PhoneNumberNormalizer userRegion];
  760. NSString *mobileNoNormalized = [normalizer phoneNumberToE164:mobileNo withDefaultRegion:countryCode prettyFormat:nil];
  761. if (mobileNoNormalized != nil) {
  762. NSString *mobileNoHashBase64 = [self hashMobileNoBase64:mobileNoNormalized];
  763. mobileNoHashesBase64 = @[mobileNoHashBase64];
  764. }
  765. }
  766. if (emailHashesBase64.count > 0 || mobileNoHashesBase64.count > 0) {
  767. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  768. [conn matchIdentitiesWithEmailHashes:emailHashesBase64 mobileNoHashes:mobileNoHashesBase64 includeInactive:YES onCompletion:^(NSArray *identities, int checkInterval) {
  769. onCompletion(identities);
  770. } onError:^(NSError *error) {
  771. DDLogError(@"linked identities failed: %@", error);
  772. NSArray *emptyArray = [NSArray array];
  773. onCompletion(emptyArray);
  774. }];
  775. } else {
  776. NSArray *emptyArray = [NSArray array];
  777. onCompletion(emptyArray);
  778. }
  779. }
  780. - (NSArray *)allIdentities {
  781. NSFetchRequest *fetchRequest = [entityManager.entityFetcher fetchRequestForEntity:@"Contact"];
  782. fetchRequest.propertiesToFetch = @[@"identity"];
  783. NSArray *result = [entityManager.entityFetcher executeFetchRequest:fetchRequest];
  784. if (result != nil) {
  785. return [self identitiesForContacts:result];
  786. } else {
  787. DDLogError(@"Cannot get identities");
  788. return nil;
  789. }
  790. }
  791. - (NSArray *)contactsWithVerificationLevel:(NSInteger)verificationLevel {
  792. return [entityManager.entityFetcher contactsWithVerificationLevel:verificationLevel];
  793. }
  794. - (NSArray *)contactsWithFeatureMaskNil {
  795. return [entityManager.entityFetcher contactsWithFeatureMaskNil];
  796. }
  797. - (NSArray *)allContacts {
  798. return [entityManager.entityFetcher allContacts];
  799. }
  800. - (void)orderChanged:(NSNotification*)notification {
  801. [entityManager performAsyncBlockAndSafe:^{
  802. /* update display name and sort index of all contacts */
  803. NSArray *allContacts = [entityManager.entityFetcher allContacts];
  804. if (allContacts != nil) {
  805. for (Contact *contact in allContacts) {
  806. /* set last name again to trigger update of display name and sort index */
  807. contact.lastName = contact.lastName;
  808. }
  809. }
  810. }];
  811. }
  812. - (NSArray *)identitiesForContacts:(NSArray *)contacts {
  813. NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
  814. for (Contact *contact in contacts) {
  815. [identities addObject:contact.identity];
  816. }
  817. return identities;
  818. }
  819. - (NSArray *)validIdentitiesForContacts:(NSArray *)contacts {
  820. NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
  821. for (Contact *contact in contacts) {
  822. if (contact.state.intValue != kStateInvalid) {
  823. [identities addObject:contact.identity];
  824. }
  825. }
  826. return identities;
  827. }
  828. - (void)updateFeatureMasksForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
  829. NSArray *identities = [self identitiesForContacts: contacts];
  830. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  831. [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) {
  832. [entityManager performSyncBlockAndSafe:^{
  833. for (NSInteger i=0; i<[identities count]; i++) {
  834. NSNumber *featureMask = [featureMasks objectAtIndex: i];
  835. if (featureMask.integerValue >= 0) {
  836. NSString *identityString = [identities objectAtIndex:i];
  837. Contact *contact = [entityManager.entityFetcher contactForId: identityString];
  838. contact.featureMask = featureMask;
  839. }
  840. }
  841. }];
  842. onCompletion();
  843. } onError:^(NSError *error) {
  844. onError(error);
  845. }];
  846. }
  847. - (void)updateFeatureMasksForIdentities:(NSArray *)identities onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
  848. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  849. [conn getFeatureMasksForIdentities:identities onCompletion:^(NSArray *featureMasks) {
  850. [entityManager performSyncBlockAndSafe:^{
  851. for (NSInteger i=0; i<[identities count]; i++) {
  852. NSNumber *featureMask = [featureMasks objectAtIndex: i];
  853. if (featureMask.integerValue >= 0) {
  854. NSString *identityString = [identities objectAtIndex:i];
  855. Contact *contact = [entityManager.entityFetcher contactForId: identityString];
  856. contact.featureMask = featureMask;
  857. }
  858. }
  859. }];
  860. onCompletion();
  861. } onError:^(NSError *error) {
  862. onError(error);
  863. }];
  864. }
  865. - (BOOL)needCheckStatus:(BOOL)ignoreInterval {
  866. if (ignoreInterval) {
  867. return YES;
  868. }
  869. NSUserDefaults *defaults = [AppGroup userDefaults];
  870. NSDate *dateLastCheck = [defaults objectForKey:@"DateLastCheckStatus"];
  871. if (dateLastCheck == nil) {
  872. return true;
  873. }
  874. NSInteger checkInterval = [self getCheckStatusInterval];
  875. NSDate *dateOfNextCheck = [dateLastCheck dateByAddingTimeInterval:checkInterval];
  876. NSDate *now = [NSDate date];
  877. return [now timeIntervalSinceDate:dateOfNextCheck] > 0;
  878. }
  879. - (void)setupCheckStatusTimer {
  880. NSUserDefaults *defaults = [AppGroup userDefaults];
  881. NSDate *now = [NSDate date];
  882. [defaults setObject:now forKey:@"DateLastCheckStatus"];
  883. [defaults synchronize];
  884. NSInteger checkInterval = [self getCheckStatusInterval];
  885. checkStatusTimer = [NSTimer scheduledTimerWithTimeInterval:checkInterval target:self selector:@selector(updateStatusForAllContacts) userInfo:nil repeats:NO];
  886. }
  887. - (NSInteger) getCheckStatusInterval {
  888. NSUserDefaults *defaults = [AppGroup userDefaults];
  889. NSInteger checkInterval = [defaults integerForKey:@"CheckStatusInterval"];
  890. return MAX(checkInterval, MIN_CHECK_INTERVAL);
  891. }
  892. - (void)updateStatusForAllContacts {
  893. [self updateStatusForAllContactsIgnoreInterval:NO];
  894. }
  895. - (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval {
  896. if ([[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
  897. NSArray *contacts = [entityManager.entityFetcher allContacts];
  898. [self updateStatusForContacts:contacts onCompletion:^() {
  899. [self setupCheckStatusTimer];
  900. } onError:^(){
  901. [self setupCheckStatusTimer];
  902. }];
  903. } else {
  904. if ([self needCheckStatus:ignoreInterval] == NO) {
  905. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: do not update status and featuremasks"];
  906. return;
  907. }
  908. NSArray *contacts = [entityManager.entityFetcher allContacts];
  909. [self updateStatusForContacts:contacts onCompletion:^() {
  910. [self setupCheckStatusTimer];
  911. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status and featuremasks finished"];
  912. } onError:^(){
  913. [[ValidationLogger sharedValidationLogger] logString:@"ContactSync: update status featuremasks finished with error"];
  914. [self setupCheckStatusTimer];
  915. }];
  916. }
  917. }
  918. - (void)updateStatusForContacts:(NSArray *)contacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(void))onError {
  919. NSArray *identities = [self validIdentitiesForContacts: contacts];
  920. ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
  921. [conn checkStatusOfIdentities:identities onCompletion:^(NSArray *states, NSArray *types, NSArray *featureMasks, int checkInterval) {
  922. [entityManager performSyncBlockAndSafe:^{
  923. NSMutableOrderedSet *workIdentities = [NSMutableOrderedSet new];
  924. for (NSInteger i=0; i<[identities count]; i++) {
  925. NSNumber *state = [states objectAtIndex: i];
  926. NSNumber *type = [types objectAtIndex:i];
  927. NSNumber *featureMask = [featureMasks objectAtIndex:i];
  928. NSString *identityString = [identities objectAtIndex:i];
  929. Contact *contact = [entityManager.entityFetcher contactForId: identityString];
  930. if (![contact.state isEqualToNumber:state])
  931. contact.state = state;
  932. if ([type isEqualToNumber:@1]) {
  933. [workIdentities addObject:contact.identity];
  934. }
  935. if (![featureMask isEqual:[NSNull null]]) {
  936. if (![contact.featureMask isEqualToNumber:featureMask]) {
  937. contact.featureMask = featureMask;
  938. }
  939. }
  940. }
  941. if (![[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
  942. [UserSettings sharedUserSettings].workIdentities = workIdentities;
  943. }
  944. }];
  945. NSUserDefaults *defaults = [AppGroup userDefaults];
  946. [defaults setInteger:checkInterval forKey:@"CheckStatusInterval"];
  947. [defaults synchronize];
  948. onCompletion();
  949. } onError:^(NSError *error) {
  950. DDLogError(@"Status update failed: %@", error);
  951. onError();
  952. }];
  953. }
  954. - (void)updateAllContactsToCNContact {
  955. #pragma clang diagnostic push
  956. #pragma clang diagnostic ignored "-Wdeprecated-declarations"
  957. NSUserDefaults *defaults = [AppGroup userDefaults];
  958. if ([defaults boolForKey:@"AlreadyUpdatedToCNContacts"]) {
  959. return;
  960. }
  961. NSArray *linkedContacts = [[entityManager.entityFetcher allContacts] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
  962. Contact *contact = (Contact *)evaluatedObject;
  963. return contact.abRecordId != nil && contact.abRecordId.intValue != 0;
  964. }]];
  965. if (linkedContacts == nil || linkedContacts.count == 0) {
  966. NSUserDefaults *defaults = [AppGroup userDefaults];
  967. [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"];
  968. [defaults synchronize];
  969. return;
  970. }
  971. CNContactStore *cnAddressBook = [CNContactStore new];
  972. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  973. if (granted == YES) {
  974. ABAddressBookRef addressBook = nil;
  975. int nupdated = 0;
  976. for (Contact *contact in linkedContacts) {
  977. if (addressBook == nil) {
  978. addressBook = ABAddressBookCreate();
  979. if (addressBook == nil)
  980. return;
  981. }
  982. ABRecordRef abPerson = ABAddressBookGetPersonWithRecordID(addressBook, contact.abRecordId.intValue);
  983. if (abPerson != nil) {
  984. NSString *firstName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonFirstNameProperty));
  985. NSString *lastName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonLastNameProperty));
  986. NSString *middleName = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonMiddleNameProperty));
  987. NSString *company = CFBridgingRelease(ABRecordCopyValue(abPerson, kABPersonOrganizationProperty));
  988. NSString *fullName = [NSString stringWithFormat:@"%@ %@ %@", firstName, middleName, lastName];
  989. ABMutableMultiValueRef multiPhone = ABRecordCopyValue(abPerson, kABPersonPhoneProperty);
  990. NSMutableArray *personPhones = [NSMutableArray new];
  991. if (ABMultiValueGetCount(multiPhone) > 0) {
  992. for (CFIndex i = 0; i < ABMultiValueGetCount(multiPhone); i++) {
  993. CFStringRef phoneRef = ABMultiValueCopyValueAtIndex(multiPhone, i);
  994. [personPhones addObject:(__bridge NSString *)phoneRef];
  995. CFRelease(phoneRef);
  996. }
  997. }
  998. CFRelease(multiPhone);
  999. ABMutableMultiValueRef multiEmail = ABRecordCopyValue(abPerson, kABPersonEmailProperty);
  1000. NSMutableArray *personEmails = [NSMutableArray new];
  1001. if (ABMultiValueGetCount(multiEmail) > 0) {
  1002. for (CFIndex i = 0; i < ABMultiValueGetCount(multiEmail); i++) {
  1003. CFStringRef emailRef = ABMultiValueCopyValueAtIndex(multiEmail, i);
  1004. [personEmails addObject:(__bridge NSString *)emailRef];
  1005. CFRelease(emailRef);
  1006. }
  1007. }
  1008. CFRelease(multiEmail);
  1009. // Check is there a CNContact for the ABPerson
  1010. NSPredicate *predicate = [CNContact predicateForContactsMatchingName:fullName];
  1011. NSError *error;
  1012. NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  1013. if (error) {
  1014. NSLog(@"error fetching contacts %@", error);
  1015. } else {
  1016. if (cnContacts.count == 1) {
  1017. NSLog(@"Found the CNContact for ABPerson; Identifier: %@", [((CNContact *)cnContacts.firstObject) identifier]);
  1018. [entityManager performSyncBlockAndSafe:^{
  1019. contact.cnContactId = [((CNContact *)cnContacts.firstObject) identifier];
  1020. }];
  1021. }
  1022. else if (cnContacts.count > 1) {
  1023. // Find correct contact in array
  1024. NSMutableArray *phoneEmailMatch = [NSMutableArray new];
  1025. NSMutableArray *phoneMatch = [NSMutableArray new];
  1026. NSMutableArray *emailMatch = [NSMutableArray new];
  1027. for (CNContact *contact in cnContacts) {
  1028. if ([company isEqualToString:contact.organizationName]) {
  1029. // compare ABPerson numbers with CNContact numbers
  1030. BOOL foundPhone = NO;
  1031. for (NSString *abPhone in personPhones) {
  1032. for (CNLabeledValue *label in contact.phoneNumbers) {
  1033. NSString *phoneNumber = [label.value stringValue];
  1034. if (phoneNumber.length > 0) {
  1035. if ([phoneNumber isEqualToString:abPhone]) {
  1036. foundPhone = YES;
  1037. } else {
  1038. foundPhone = NO;
  1039. }
  1040. }
  1041. }
  1042. }
  1043. // compare ABPerson emails with CNContact emails
  1044. BOOL foundEmail = NO;
  1045. for (NSString *abEmail in personEmails) {
  1046. for (CNLabeledValue *label in contact.emailAddresses) {
  1047. NSString *email = label.value;
  1048. if (email.length > 0) {
  1049. if ([email isEqualToString:abEmail]) {
  1050. foundEmail = YES;
  1051. } else {
  1052. foundEmail = NO;
  1053. }
  1054. }
  1055. }
  1056. }
  1057. if (foundEmail && foundPhone) {
  1058. [phoneEmailMatch addObject:contact];
  1059. } else {
  1060. if (foundEmail) {
  1061. [emailMatch addObject:contact];
  1062. }
  1063. if (foundPhone) {
  1064. [phoneMatch addObject:contact];
  1065. }
  1066. }
  1067. }
  1068. }
  1069. // compare is only one contact with mail and phone match
  1070. if (phoneEmailMatch.count == 1) {
  1071. [entityManager performSyncBlockAndSafe:^{
  1072. NSLog(@"Found phone and email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneEmailMatch.firstObject) identifier]);
  1073. contact.cnContactId = [((CNContact *)phoneEmailMatch.firstObject) identifier];
  1074. }];
  1075. }
  1076. else if (phoneMatch.count == 1 && emailMatch.count == 0) {
  1077. [entityManager performSyncBlockAndSafe:^{
  1078. NSLog(@"Found phone of the CNContact for ABPerson; Identifier: %@", [((CNContact *)phoneMatch.firstObject) identifier]);
  1079. contact.cnContactId = [((CNContact *)phoneMatch.firstObject) identifier];
  1080. }];
  1081. }
  1082. else if (emailMatch.count == 1 && phoneMatch.count == 0) {
  1083. [entityManager performSyncBlockAndSafe:^{
  1084. NSLog(@"Found email of the CNContact for ABPerson; Identifier: %@", [((CNContact *)emailMatch.firstObject) identifier]);
  1085. contact.cnContactId = [((CNContact *)emailMatch.firstObject) identifier];
  1086. }];
  1087. } else {
  1088. NSLog(@"Found %lu contacts that could match", phoneEmailMatch.count + phoneMatch.count + emailMatch.count);
  1089. }
  1090. }
  1091. else {
  1092. NSLog(@"Found no CNContact for ABPerson");
  1093. // skip
  1094. }
  1095. }
  1096. nupdated++;
  1097. }
  1098. }
  1099. if (addressBook != nil)
  1100. CFRelease(addressBook);
  1101. DDLogInfo(@"Updated %d contacts to CNContact", nupdated);
  1102. NSUserDefaults *defaults = [AppGroup userDefaults];
  1103. [defaults setBool:YES forKey:@"AlreadyUpdatedToCNContacts"];
  1104. [defaults synchronize];
  1105. }
  1106. }];
  1107. #pragma clang diagnostic pop
  1108. }
  1109. - (void)cnContactAskAccessEmailsForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler {
  1110. if (contact.cnContactId == nil)
  1111. completionHandler(YES, nil);
  1112. __block NSArray *cnContacts;
  1113. CNContactStore *cnAddressBook = [CNContactStore new];
  1114. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  1115. if (granted == YES) {
  1116. NSError *error;
  1117. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  1118. NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  1119. if (error) {
  1120. NSLog(@"error fetching contacts %@", error);
  1121. completionHandler(YES, nil);
  1122. } else {
  1123. cnContacts = tmpCnContacts;
  1124. NSMutableArray *emails = [NSMutableArray new];
  1125. if (cnContacts.count == 1) {
  1126. for (CNContact *person in cnContacts) {
  1127. for (CNLabeledValue *label in person.emailAddresses) {
  1128. NSMutableDictionary *dict = [NSMutableDictionary new];
  1129. NSString *emailLabel = label.label;
  1130. NSString *email = label.value;
  1131. if (email.length > 0) {
  1132. [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"];
  1133. [dict setValue:email forKey:@"address"];
  1134. [emails addObject:dict];
  1135. }
  1136. }
  1137. }
  1138. }
  1139. completionHandler(YES, emails);
  1140. }
  1141. } else {
  1142. completionHandler(NO, nil);
  1143. }
  1144. }];
  1145. }
  1146. - (void)cnContactAskAccessPhoneNumbersForContact:(Contact *)contact completionHandler:(void (^)(BOOL granted, NSArray *array))completionHandler {
  1147. if (contact.cnContactId == nil)
  1148. completionHandler(YES, nil);
  1149. __block NSArray *cnContacts;
  1150. CNContactStore *cnAddressBook = [CNContactStore new];
  1151. [cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
  1152. if (granted == YES) {
  1153. NSError *error;
  1154. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  1155. NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  1156. if (error) {
  1157. NSLog(@"error fetching contacts %@", error);
  1158. completionHandler(YES, nil);
  1159. } else {
  1160. cnContacts = tmpCnContacts;
  1161. NSMutableArray *phoneNumbers = [NSMutableArray new];
  1162. if (cnContacts.count == 1) {
  1163. for (CNContact *person in cnContacts) {
  1164. for (CNLabeledValue *label in person.phoneNumbers) {
  1165. NSMutableDictionary *dict = [NSMutableDictionary new];
  1166. NSString *phoneLabel = label.label;
  1167. NSString *phone = [label.value stringValue];
  1168. if (phone.length > 0) {
  1169. [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"];
  1170. [dict setValue:phone forKey:@"number"];
  1171. [phoneNumbers addObject:dict];
  1172. }
  1173. }
  1174. }
  1175. }
  1176. completionHandler(YES, phoneNumbers);
  1177. }
  1178. } else {
  1179. completionHandler(NO, nil);
  1180. }
  1181. }];
  1182. }
  1183. - (NSArray *)cnContactEmailsForContact:(Contact *)contact {
  1184. if (contact.cnContactId == nil)
  1185. return nil;
  1186. __block NSArray *cnContacts;
  1187. CNContactStore *cnAddressBook = [CNContactStore new];
  1188. NSError *error;
  1189. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  1190. NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  1191. if (error) {
  1192. NSLog(@"error fetching contacts %@", error);
  1193. return nil;
  1194. } else {
  1195. cnContacts = tmpCnContacts;
  1196. NSMutableArray *emails = [NSMutableArray new];
  1197. if (cnContacts.count == 1) {
  1198. for (CNContact *person in cnContacts) {
  1199. for (CNLabeledValue *label in person.emailAddresses) {
  1200. NSMutableDictionary *dict = [NSMutableDictionary new];
  1201. NSString *emailLabel = label.label;
  1202. NSString *email = label.value;
  1203. if (email.length > 0) {
  1204. [dict setValue:[CNLabeledValue localizedStringForLabel:emailLabel] forKey:@"label"];
  1205. [dict setValue:email forKey:@"address"];
  1206. [emails addObject:dict];
  1207. }
  1208. }
  1209. }
  1210. }
  1211. return emails;
  1212. }
  1213. }
  1214. - (NSArray *)cnContactPhoneNumbersForContact:(Contact *)contact {
  1215. if (contact.cnContactId == nil)
  1216. return nil;
  1217. __block NSArray *cnContacts;
  1218. CNContactStore *cnAddressBook = [CNContactStore new];
  1219. NSError *error;
  1220. NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[contact.cnContactId]];
  1221. NSArray *tmpCnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
  1222. if (error) {
  1223. NSLog(@"error fetching contacts %@", error);
  1224. return nil;
  1225. } else {
  1226. cnContacts = tmpCnContacts;
  1227. NSMutableArray *phoneNumbers = [NSMutableArray new];
  1228. if (cnContacts.count == 1) {
  1229. for (CNContact *person in cnContacts) {
  1230. for (CNLabeledValue *label in person.phoneNumbers) {
  1231. NSMutableDictionary *dict = [NSMutableDictionary new];
  1232. NSString *phoneLabel = label.label;
  1233. NSString *phone = [label.value stringValue];
  1234. if (phone.length > 0) {
  1235. [dict setValue:[CNLabeledValue localizedStringForLabel:phoneLabel] forKey:@"label"];
  1236. [dict setValue:phone forKey:@"number"];
  1237. [phoneNumbers addObject:dict];
  1238. }
  1239. }
  1240. }
  1241. }
  1242. return phoneNumbers;
  1243. }
  1244. }
  1245. - (NSString*)hashEmailBase64:(NSString*)email {
  1246. NSData *emailHashKeyData = [NSData dataWithBytes:emailHashKey length:sizeof(emailHashKey)];
  1247. return [[CryptoUtils hmacSha256ForData:[email dataUsingEncoding:NSASCIIStringEncoding] key:emailHashKeyData] base64EncodedStringWithOptions:0];
  1248. }
  1249. - (NSString*)hashMobileNoBase64:(NSString*)mobileNo {
  1250. NSData *mobileNoHashKeyData = [NSData dataWithBytes:mobileNoHashKey length:sizeof(mobileNoHashKey)];
  1251. return [[CryptoUtils hmacSha256ForData:[mobileNo dataUsingEncoding:NSASCIIStringEncoding] key:mobileNoHashKeyData] base64EncodedStringWithOptions:0];
  1252. }
  1253. @end