|
|
|
|
Quelle ContactStore.m
Sprache: Unknown
|
|
Spracherkennung für: .m vermutete Sprache: Abap {ABAP[36] Lisp[34] Masm[34]} [Methode: maximale Elemente, drei Dimensionen]
#import <AddressBook/AddressBook.h>
#import <Contacts/Contacts.h>
#import "ContactStore.h"
#import "NSString+Hex.h"
#import "ServerAPIConnector.h"
#import "ServerConnector.h"
#import "MyIdentityStore.h"
#import "ProtocolDefines.h"
#import "ThreemaFramework/ThreemaFramework-Swift.h"
#import "ThreemaError.h"
#import "AppGroup.h"
#import "IdentityInfoFetcher.h"
#import "CryptoUtils.h"
#import "LicenseStore.h"
#define MIN_CHECK_INTERVAL 5*60
#ifdef DEBUG
static const DDLogLevel ddLogLevel = DDLogLevelAll;
#else
static const DDLogLevel ddLogLevel = DDLogLevelNotice;
#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;
id<UserSettingsProtocol> userSettings;
EntityManager *entityManager;
// Workaround to prevent too many active synchronizations at the same time (IOS-4791)
BOOL addressBookChangeSynchronizationActive;
}
+ (ContactStore*)sharedContactStore {
static ContactStore *instance;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [ContactStore new];
});
return instance;
}
- (instancetype)init
{
return [self initWithUserSettings:[UserSettings sharedUserSettings] entityManager:[self entityManager]];
}
- (instancetype)initWithUserSettings:(id<UserSettingsProtocol>)userSettingsProtocol entityManager:(NSObject *)entityManagerObject {
NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
self = [super init];
if (self) {
_syncContactsQueue = dispatch_queue_create("ch.threema.ContactStore.syncContactsQueue", DISPATCH_QUEUE_SERIAL);
dispatch_set_target_queue(_syncContactsQueue, 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];
userSettings = userSettingsProtocol;
entityManager = (EntityManager *)entityManagerObject;
/* update display/sort order prefs to match system */
BOOL sortOrder = [[CNContactsUserDefaults sharedDefaults] sortOrder] == CNContactSortOrderGivenName;
[userSettings setSortOrderFirstName:sortOrder];
addressBookChangeSynchronizationActive = false;
}
return self;
}
- (void)dealloc {
[checkStatusTimer invalidate];
}
- (void)resetEntityManager {
self->entityManager = [self entityManager];
}
- (void)addressBookChangeDetected:(NSNotification *)notification {
DDLogNotice(@"Address book change detected");
if (addressBookChangeSynchronizationActive) {
DDLogNotice(@"Skip address book change synchronization, because one is already running...");
return;
}
addressBookChangeSynchronizationActive = true;
[self synchronizeAddressBookForceFullSync:NO onCompletion:^(BOOL addressBookAccessGranted) {
[self updateAllContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
addressBookChangeSynchronizationActive = false;
} onError:^(NSError *error) {
[self updateAllContacts];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationAddressbookSyncronized object:self userInfo:nil];
addressBookChangeSynchronizationActive = false;
}];
}
- (NSObject *)contactForIdentity:(NSString *)identity {
/* check in local DB first */
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: identity];
return contact;
}
- (void)addContactWithIdentity:(NSString *)identity verificationLevel:(int32_t)verificationLevel onCompletion:(nonnull void(^)(NSObject * _Nullable, BOOL))onCompletion onError:(void(^)(NSError *error))onError {
/* check in local DB first */
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
if (contact) {
[entityManager performAndWaitSave:^{
if (contact.isHidden) {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
contact.isHidden = NO;
[mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:ContactAcquaintanceLevelDirect]];
// Check if this is a trusted contact (like *THREEMA)
if ([PredefinedContactsObjC isPredefinedContactWithIdentity:contact.identity publicKey:contact.publicKey]) {
contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
[mediatorSyncableContacts updateVerificationLevelWithIdentity:identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
}
[mediatorSyncableContacts syncAsync];
}
}];
onCompletion(contact, YES);
return;
}
/* 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 */
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
__weak typeof(self) weakSelf = self;
[self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:verificationLevel state:state type:type featureMask:featureMask acquaintanceLevel:ContactAcquaintanceLevelDirect alerts:YES contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject *contactObject){
NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter senderObject must be type of ContactEntity");
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
/* force synchronisation */
[weakSelf synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
WorkDataFetcherObjCBridge* fetcher = [[WorkDataFetcherObjCBridge alloc] initWithEntityManager:entityManager contactStore:self];
[fetcher updateWorkDataWithForce:YES completionHandler:^(NSError * _Nullable error) {
if (error == nil) {
[[NSNotificationCenter defaultCenter] postNotificationName:kSafeBackupTrigger object:nil];
onCompletion(contactObject, NO);
}
else {
DDLogError(@"WorkDataFetch failed: %@", [error localizedDescription]);
onCompletion(nil, NO);
}
}];
}
else {
DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
onCompletion(nil, NO);
}
}];
}];
});
} onError:^(NSError *error) {
if (onError != nil) {
onError(error);
}
}];
}
- (void)addContactWithIdentity:(nullable NSString *)identity publicKey:(nullable NSData *)publicKey cnContactId:(nullable NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(nullable NSNumber *)state type:(nullable NSNumber *)type featureMask:(nullable NSNumber *)featureMask acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel alerts:(BOOL)alerts onCompletion:(nonnull void(^)(NSObject * nullable))onCompletion {
MediatorSyncableContacts *mediatorSyncableContacts = [MediatorSyncableContacts new];
[self addContactWithIdentity:identity publicKey:publicKey cnContactId:cnContactId verificationLevel:verificationLevel state:state type:type featureMask:featureMask acquaintanceLevel:acquaintanceLevel alerts:alerts contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject * contactObject) {
NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter senderObject must be type of ContactEntity");
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
onCompletion(contactObject);
}
else {
onCompletion(nil);
}
}];
}];
}
/**
Add or update contact.
@param identity Identity of the contact (will be not validated)
@param publicKey Public key it corresponds with the identity (will be not validated)
@param cnContactId Is not null contact will be linked to this address book contact
@param verificationLevel Verification level of the identity
@param state State of the identity, will only be changed if is not null
@param type Type of the identity, will only be changed if is not null
@param featureMask Feature mask of the identity
@param acquaintanceLevel Is `group` contact will be marked as hidden
@param alerts Is `YES` an notification will be displayed, if public key of already existing contact differs to given public key
@param mediatorSyncableContacts Contact syncer for multi device
@param onCompletion With parameter of type `ContactEntity` as result of added/updated contact
*/
- (void)addContactWithIdentity:(nonnull NSString*)identity publicKey:(nonnull NSData*)publicKey cnContactId:(nullable NSString *)cnContactId verificationLevel:(int32_t)verificationLevel state:(nullable NSNumber *)state type:(nullable NSNumber *)type featureMask:(nullable NSNumber *)featureMask acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel alerts:(BOOL)alerts contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(nonnull void(^)(NSObject * nullable))onCompletion {
/* Make sure this is not our own identity */
if ([MyIdentityStore sharedMyIdentityStore].isValidIdentity && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
DDLogInfo(@"Ignoring attempt to add own identity");
onCompletion(nil);
return;
}
/* Check if we already have a contact with this identity */
[entityManager performAndWaitSave:^{
__block BOOL added = NO;
void (^linkingFinished)(ContactEntity *) = ^(ContactEntity *contact){
if (added) {
__block NSString *identity = contact.identity;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[mediatorSyncableContacts updateAllWithIdentity:identity added:added];
onCompletion(contact);
});
return;
}
onCompletion(contact);
};
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: 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];
}
onCompletion(contact);
return;
}
if (contact.isHidden && acquaintanceLevel == ContactAcquaintanceLevelDirect) {
contact.isHidden = NO;
[mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:acquaintanceLevel]];
}
} else {
added = YES;
NSError *createContactError;
contact = [entityManager getOrCreateContactWithIdentity:identity publicKey:publicKey sortOrderFirstName:userSettings.sortOrderFirstName error:&createContactError];
if (createContactError) {
DDLogError(@"New contact could not be created: %@", createContactError);
onCompletion(nil);
return;
}
[contact setFeatureMaskTo: featureMask.intValue];
if (state != nil) {
contact.contactState = (ContactState)state.integerValue;
}
if (type != nil) {
if ([type isEqualToNumber:@1]) {
[self addAsWorkWithIdentities:[[NSOrderedSet alloc] initWithArray:@[contact.identity]] contactSyncer:mediatorSyncableContacts];
}
}
contact.isHidden = acquaintanceLevel == ContactAcquaintanceLevelGroupOrDeleted ? YES : NO;
[self addProfilePictureRequest:identity];
}
// Update verification level
ContactVerificationLevel currentVerificationLevel = contact.contactVerificationLevel;
// Check if this is a trusted contact (like *THREEMA) and verification level is not fully verified
if ([PredefinedContactsObjC isPredefinedContactWithIdentity:identity publicKey:publicKey] && contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) {
contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
}
else {
// Check if verification level increases to server or fully verified
if (contact.contactVerificationLevel < verificationLevel) {
contact.contactVerificationLevel = verificationLevel;
}
}
[self fetchGatewayProfilePictureFor:identity];
if (contact.contactVerificationLevel != currentVerificationLevel) {
[mediatorSyncableContacts updateVerificationLevelWithIdentity:identity value: [NSNumber numberWithInteger: contact.contactVerificationLevel]];
}
// Set contact as none work contact
if (contact.workContact == nil) {
contact.workContact = @NO;
[mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
}
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) {
DDLogError(@"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 contactSyncer:mediatorSyncableContacts onCompletion:^{
linkingFinished(contact);
}];
return;
}
}
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 contactSyncer:mediatorSyncableContacts onCompletion:^{
linkingFinished(contact);
}];
return;
}
}
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 contactSyncer:mediatorSyncableContacts onCompletion:^{
linkingFinished(contact);
}];
return;
}
}
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 contactSyncer:mediatorSyncableContacts onCompletion:^{
linkingFinished(contact);
}];
return;
}
} // if (cnContacts.count == 1)
}
} // if (granted == YES)
linkingFinished(contact);
}];
return;
} // if (![contact.cnContactId isEqualToString:cnContactId])
} // if (contact.cnContactId != nil)
else {
[self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
linkingFinished(contact);
}];
return;
}
} // if (cnContactId)
linkingFinished(contact);
}];
}
/**
Add or update all linked address book contacts.
@param identities: Identities to add and update
@param emailHashes: Email hashes with contact id of address book
@param mobileHashes: Mobile hashes with contact id of address book
@param contactSyncer: Contact syncer for multi device
@param onCompletion: Completion handler
*/
- (void)addContactsWithIdentities:(NSArray * _Nonnull)identities emailHashes:(NSDictionary * _Nonnull)emailHashToCnContactId mobileNoHashes:(NSDictionary * _Nonnull)mobileNoHashToCnContactId contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(nonnull void(^)(void))onCompletion
{
NSSet *excludedIds = [NSSet setWithArray:userSettings.syncExclusionList];
NSMutableArray *allIdentities = [NSMutableArray new];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
dispatch_group_t dispatchGroup = dispatch_group_create();
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];
dispatch_group_enter(dispatchGroup);
dispatch_async(dispatch_get_main_queue(), ^{
[self addContactWithIdentity:identity publicKey:[[NSData alloc] initWithBase64EncodedString:[identityData objectForKey:@"publicKey"] options:0] cnContactId:cnContactId verificationLevel:ContactVerificationLevelServerVerified state:nil type:nil featureMask:nil acquaintanceLevel:ContactAcquaintanceLevelDirect alerts:NO contactSyncer:mediatorSyncableContacts onCompletion:^(NSObject * __unused contactObject) {
dispatch_group_leave(dispatchGroup);
}];
});
}
DDLogNotice(@"[ContactSync] Found %lu new address book contacts", (unsigned long)allIdentities.count);
dispatch_group_wait(dispatchGroup, DISPATCH_TIME_FOREVER);
onCompletion();
});
}
- (void)resetImportedStatus {
[entityManager performAndWaitSave:^{
NSArray *contacts = [entityManager.entityFetcher contactEntities];
for (ContactEntity *contact in contacts) {
contact.contactImportStatus = ContactImportStatusInitial;
}
}];
}
- (void)addWorkContactAndUpdateFeatureMaskWithIdentity:(nonnull NSString *)identity
publicKey:(nonnull NSData *)publicKey
firstname:(nullable NSString *)firstname
lastname:(nullable NSString *)lastname
csi:(nullable NSString *)csi
jobTitle:(nullable NSString *)jobTitle
department:(nullable NSString *)department
acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel
workAvailabilityStatus:(nullable WorkAvailabilityStatus *) workAvailabilityStatus
onCompletion:(nonnull void(^)(NSObject * _Nonnull))onCompletion
onError:(nonnull void(^)(NSError * _Nonnull))onError {
[FeatureMask getFeatureMaskFor:@[identity] completionHandler:^(NSDictionary<NSString *,NSNumber *> * _Nullable featureMasks, NSError * _Nullable error) {
if (error) {
onError(error);
return;
}
NSNumber *featureMask = featureMasks[identity];
// Sync create of the contact first for MD, if it successful than store it and sync update of the contact
MediatorSyncableContacts *createContactSync = [MediatorSyncableContacts new];
[createContactSync createWithDefaultValuesWithIdentity:identity publicKey:publicKey featureMask:featureMask.intValue isWork:YES];
[createContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
onError(error);
return;
}
MediatorSyncableContacts *updateContactSync = [MediatorSyncableContacts new];
__block NSString *contactIdentity = [self addWorkContactWithIdentity:identity
publicKey:publicKey
firstname:firstname
lastname:lastname
csi:csi
jobTitle:jobTitle
department:department
featureMask:featureMasks[identity]
acquaintanceLevel:acquaintanceLevel
workAvailabilityStatus: workAvailabilityStatus
entityManager:entityManager
contactSyncer:updateContactSync];
if (contactIdentity) {
// Set sync action to update, because sync action create was used above before contact is stored in DB
[updateContactSync setSyncActionToUpdateWithIdentity:identity];
[updateContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
__block ContactEntity *contactEntity;
[entityManager performAndWait:^{
contactEntity = [[entityManager entityFetcher] contactEntityFor:contactIdentity];
}];
onCompletion(contactEntity);
}
else {
onError(error);
}
}];
}
else {
onError([ThreemaError threemaError:[NSString stringWithFormat:@"Add or update work contact for %@ failed", identity]]);
}
}];
}];
}
/**
Add or update work contact, if multi device activated contact will be synced.
@param identity: Identity of the contact (will be not validated)
@param publicKey: Public key it corresponds with the identity (will be not validated)
@param firstname: First name of the contact
@param lastname: Last name of the contact
@param csi: CSI of the contact
@param jobTitle: Job title of the contact
@param department: Department of the contact
@param featureMask: Feature mask of contact (will only updated if not nil)
@param acquaintanceLevel: Is `group` contact will be marked as hidden
@param workAvailabilityStatus: Optional `WorkAvailabilityStatus` of the contact
@param entityManagerObject: Used `EntityManager`
@param contactSyncer: Used `MediatorSyncableContacts` to sync changes when MD is enabled
@returns: Added/updated identity of contact or nil if public key of already existing contact differs to given public key
*/
- (nullable NSString *)addWorkContactWithIdentity:(nonnull NSString *)identity
publicKey:(nonnull NSData *)publicKey
firstname:(NSString *)firstname
lastname:(NSString *)lastname
csi:(NSString *)csi
jobTitle:(NSString *)jobTitle
department:(NSString *)department
featureMask:(nullable NSNumber *)featureMask
acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel
workAvailabilityStatus:(nullable WorkAvailabilityStatus *)workAvailabilityStatus
entityManager:(NSObject * _Nonnull)entityManagerObject
contactSyncer:(MediatorSyncableContacts*)mediatorSyncableContacts {
NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
EntityManager *em = (EntityManager *)entityManagerObject;
/* Make sure this is not our own identity */
if ([MyIdentityStore sharedMyIdentityStore].isValidIdentity && [identity isEqualToString:[MyIdentityStore sharedMyIdentityStore].identity]) {
DDLogInfo(@"Ignoring attempt to add own identity");
return nil;
}
__block NSString *contactIdentity;
__block BOOL added = NO;
[em performAndWaitSave:^{
/* Check if we already have a contact with this identity */
ContactEntity *contact;
contact = [em.entityFetcher contactEntityFor: 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);
contact = nil;
return;
}
else {
if (contact.isHidden && acquaintanceLevel == ContactAcquaintanceLevelDirect) {
contact.isHidden = NO;
[mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:acquaintanceLevel]];
}
}
} else {
DDLogNotice(@"New work contact added");
added = YES;
NSError *createContactError;
contact = [em getOrCreateContactWithIdentity:identity publicKey:publicKey sortOrderFirstName:userSettings.sortOrderFirstName error:&createContactError];
if (createContactError) {
DDLogError(@"New contact could not be created: %@", createContactError);
return;
}
contact.isHidden = acquaintanceLevel == ContactAcquaintanceLevelGroupOrDeleted ? YES : NO;
[self addAsWorkWithIdentities:[[NSOrderedSet alloc] initWithArray:@[contact.identity]] contactSyncer:mediatorSyncableContacts];
[self addProfilePictureRequest:identity];
}
if (firstname != nil) {
if (firstname.length > 0) {
if (![contact.firstName isEqualToString:firstname]) {
[contact setFirstNameTo:firstname sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:contact.firstName];
}
}
}
if (lastname != nil) {
if (lastname.length > 0) {
if (![contact.lastName isEqualToString:lastname]) {
[contact setLastNameTo:lastname sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value:contact.lastName];
}
}
}
// TODO: (IOS-4895) MD support Job Title and Department
if ((csi || contact.csi) && ![csi isEqualToString:contact.csi]) {
contact.csi = csi;
}
if ((jobTitle || contact.jobTitle) && ![jobTitle isEqualToString:contact.jobTitle]) {
contact.jobTitle = jobTitle;
}
if ((department || contact.department) && ![department isEqualToString:contact.department]) {
contact.department = department;
}
if (contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) {
if(contact.contactVerificationLevel != ContactVerificationLevelServerVerified) {
contact.contactVerificationLevel = ContactVerificationLevelServerVerified;
[mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
}
}
if (![contact.workContact isEqualToNumber:@YES]) {
contact.workContact = @YES;
[mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
}
if ((featureMask || contact.featureMask) && ![featureMask isEqual:contact.featureMask]) {
[contact setFeatureMaskTo: featureMask.intValue];
}
if(workAvailabilityStatus) {
// Check if contact currently has status
if (contact.workAvailabilityStatus) {
// Only update if it differs from current
if ([self workAvailabilityStatusChangedWithCurrent:contact.workAvailabilityStatus new:workAvailabilityStatus]) {
[em.entityDestroyer deleteWithWorkAvailabilityStatus:contact.workAvailabilityStatus];
[em.entityCreator workAvailabilityStatusEntityWithValue:workAvailabilityStatus.categoryRawValue text:workAvailabilityStatus.text contact:contact];
}
}
// Contact has no status, we create a new one
else {
[em.entityCreator workAvailabilityStatusEntityWithValue:workAvailabilityStatus.categoryRawValue text:workAvailabilityStatus.text contact:contact];
}
}
// We did not receive a status, if contact has one, we remove it
else if (contact.workAvailabilityStatus) {
[em.entityDestroyer deleteWithWorkAvailabilityStatus:contact.workAvailabilityStatus];
}
// We always sync the status, even if is not set
[mediatorSyncableContacts updateWorkAvailabilityStatusWithIdentity:contact.identity status:workAvailabilityStatus != nil ? workAvailabilityStatus : [[WorkAvailabilityStatus alloc] initWithValue:0 text:nil]];
contactIdentity = contact.identity;
}];
if (added) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
[mediatorSyncableContacts updateAllWithIdentity:contactIdentity added:added];
});
}
return contactIdentity;
}
- (void)updateFromAddressBookWithContact:(nonnull ContactEntity*)contact contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts forceImport:(BOOL)forceImport onCompletion:(nullable void(^)(void))onCompletion {
if ([contact contactImportStatus] != ContactImportStatusInitial && !forceImport) {
DDLogInfo(@"Contact already imported. Do not import again.");
if (onCompletion) {
onCompletion();
}
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;
if (foundContact != nil) {
[self _updateContact:contact withCnContact:foundContact forceImport:forceImport contactSyncer:mediatorSyncableContacts];
}
}
}
if (onCompletion) onCompletion();
}];
}
- (void)_updateContact:(nonnull ContactEntity *)contact withCnContact:(nonnull CNContact *)cnContact forceImport:(BOOL)forceImport contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts {
if (cnContact == nil) {
DDLogError(@"Cannot update contact from nil cnContact");
return;
}
// Refrain from updating the contact information if the multi-device feature is enabled and the import status is not set to `.initial`
if (userSettings.enableMultiDevice && [contact contactImportStatus] != ContactImportStatusInitial && !forceImport) {
DDLogInfo(@"Contact already imported. Do not import again.");
return;
}
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 setFirstNameTo:newFirstName sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:newFirstName];
}
if (newLastName != contact.lastName && ![newLastName isEqual:contact.lastName]) {
[contact setLastNameTo:newLastName sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value: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;
[mediatorSyncableContacts setProfileUpdateTypeWithIdentity:contact.identity value:contact.imageData != nil ? DeltaUpdateTypeUpdated : DeltaUpdateTypeRemoved];
}
ContactImportStatus importStatus = userSettings.enableMultiDevice ? ContactImportStatusImported : ContactImportStatusInitial;
if (contact.contactImportStatus != importStatus) {
contact.contactImportStatus = importStatus;
}
DDLogVerbose(@"Updated contact %@ to %@ %@", contact.identity, contact.firstName, contact.lastName);
}
- (void)updateAllContacts {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
NSArray *allContacts = [entityManager.entityFetcher contactEntities];
if (allContacts == nil || allContacts.count == 0) {
return;
}
// Migration of deprecated and removed verification level kVerificationLevelWorkVerified (3) and kVerificationLevelWorkFullyVerified (4) to flag workContact
[entityManager performAndWaitSave:^{
for (ContactEntity *contact in allContacts) {
if (contact.workContact == nil) {
if ([contact getVerificationLevelForMigration].intValue == 3) {
contact.contactVerificationLevel = ContactVerificationLevelServerVerified;
[mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
contact.workContact = @YES;
} else if ([contact getVerificationLevelForMigration].intValue == 4) {
contact.contactVerificationLevel = ContactVerificationLevelFullyVerified;
[mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
contact.workContact = @YES;
} else {
contact.workContact = @NO;
}
[mediatorSyncableContacts updateWorkVerificationLevelWithIdentity:contact.identity value:contact.workContact];
}
}
}];
__block NSArray *linkedContacts;
[entityManager performAndWait:^{
linkedContacts = [allContacts filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(id _Nullable evaluatedObject, NSDictionary<NSString *,id> * _Nullable bindings) {
ContactEntity *contact = (ContactEntity *)evaluatedObject;
return contact.cnContactId != nil;
}]];
}];
if (linkedContacts == nil || linkedContacts.count == 0) {
[mediatorSyncableContacts syncAsync];
return;
}
CNContactStore *cnAddressBook = [CNContactStore new];
[cnAddressBook requestAccessForEntityType:CNEntityTypeContacts completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (granted == YES) {
// Go through all contacts and resync with address book; only create
// address book ref when encountering the first contact that is linked
__block int updated = 0;
for (ContactEntity *contact in linkedContacts) {
[entityManager performAndWaitSave:^{
NSString *cnContactID = [contact.cnContactId copy];
if (cnContactID == nil) {
DDLogNotice(@"CNContactID of linked contact was nil.");
return;
}
NSPredicate *predicate = [CNContact predicateForContactsWithIdentifiers:@[cnContactID]];
NSError *error;
NSArray *cnContacts = [cnAddressBook unifiedContactsMatchingPredicate:predicate keysToFetch:kCNContactKeys error:&error];
if (error) {
DDLogError(@"Fetching address book contacts failed %@", error);
} else {
if (cnContacts != nil && cnContacts.count > 0) {
CNContact *foundContact = cnContacts.firstObject;
[self _updateContact:contact withCnContact:foundContact forceImport:NO contactSyncer:mediatorSyncableContacts];
updated++;
}
}
}];
}
DDLogInfo(@"Updated %d contacts", updated);
[self updateStatusForAllContactsIgnoreInterval:NO contactSyncer:mediatorSyncableContacts onCompletion:^{
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
DDLogError(@"Contact sync failed: %@", [error localizedDescription]);
}
}];
} onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
}
}];
}
/**
Update contact if attributes has changed and sync is multi device activated.
@param identity: Identity of the contact to changed
@param avatar: Image data of the contact avatar
@param firstName: First name of the contact
@param lastName: Last name of the contact
*/
- (void)updateContactWithIdentity:(NSString * _Nonnull)identity avatar:(NSData * _Nullable)avatar firstName:(NSString * _Nullable)firstName lastName:(NSString * _Nullable)lastName {
[entityManager performAndWaitSave:^{
ContactEntity *contact = [[entityManager entityFetcher] contactEntityFor:identity];
if (contact) {
MediatorSyncableContacts *mediatorSyncableContacts = [MediatorSyncableContacts new];
if (contact.imageData != avatar) {
contact.imageData = avatar;
[mediatorSyncableContacts setProfileUpdateTypeWithIdentity:identity value:contact.imageData ? 2 : 1];
}
if (contact.firstName != firstName) {
[contact setFirstNameTo:firstName sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateFirstNameWithIdentity:identity value:contact.firstName];
}
if (contact.lastName != lastName) {
[contact setLastNameTo:lastName sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateLastNameWithIdentity:identity value:contact.lastName];
}
[mediatorSyncableContacts syncAsync];
}
else {
DDLogError(@"Missing contact %@ to update", identity);
}
}];
}
/**
Mark contact as deleted if no 1:1-conversation exists.
If Multi Device is activated the contact will be reflected.
@param identity: Identity of the contact to mark as deleted
@param entityManagerObject: EntityManager on which the deletion will de executed
*/
- (void)markContactAsDeletedWithIdentity:(nonnull NSString *)identity entityManagerObject:(nonnull NSObject *)entityManagerObject {
NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
EntityManager *em = (EntityManager *)entityManagerObject;
[em performAndWait:^{
ContactEntity *contact = [[em entityFetcher] contactEntityFor:identity];
if (contact) {
if (!contact.isHidden) {
// Prevent deletion if has contact a 1:1 conversation
ConversationEntity *conversation = [[em entityFetcher] conversationEntityFor:contact.identity];
if (conversation) {
DDLogWarn(@"Contact %@ can't be deleted because has a 1:1 conversation", contact.identity);
return;
}
// Mark contact as deleted
[em performAndWaitSave:^{
contact.isHidden = YES;
}];
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[[NSNumber alloc] initWithInt:ContactAcquaintanceLevelGroupOrDeleted]];
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
DDLogError(@"Sync contact %@ mark as deleted failed: %@", contact.identity, [error localizedDescription]);
}
}];
}
}
}];
}
- (void)linkContact:(nonnull NSObject *)contactObject toCnContactId:(NSString *)cnContactId {
NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
ContactEntity *contact = (ContactEntity*)contactObject;
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts forceImport:YES onCompletion:^{
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
}
}];
}];
}
- (void)linkContact:(ContactEntity*)contact toCnContactId:(NSString *)cnContactId contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion {
[self linkContact:contact toCnContactId:cnContactId contactSyncer:mediatorSyncableContacts forceImport:NO onCompletion:onCompletion];
}
- (void)linkContact:(ContactEntity*)contact toCnContactId:(NSString *)cnContactId contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts forceImport:(BOOL)forceImport onCompletion:(void(^)(void))onCompletion {
/* obtain first/last name from address book */
[entityManager performAndWaitSave:^{
contact.cnContactId = cnContactId;
[self updateFromAddressBookWithContact:contact contactSyncer:mediatorSyncableContacts forceImport:forceImport onCompletion:onCompletion];
}];
}
- (void)unlinkContact:(nonnull NSObject*)contactObject {
__block MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[entityManager performAndWaitSave:^{
NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
ContactEntity *contact = (ContactEntity*)contactObject;
contact.cnContactId = nil;
if (contact.firstName) {
[contact setFirstNameTo:nil sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateFirstNameWithIdentity:contact.identity value:@""];
}
if (contact.lastName) {
[contact setLastNameTo:nil sortOrderFirstName:userSettings.sortOrderFirstName];
[mediatorSyncableContacts updateLastNameWithIdentity:contact.identity value:@""];
}
if (contact.imageData) {
contact.imageData = nil;
[mediatorSyncableContacts setProfileUpdateTypeWithIdentity:contact.identity value:DeltaUpdateTypeRemoved];
}
}];
[mediatorSyncableContacts syncAsync];
}
#pragma mark - Fetch contact
/**
Fetch public key for identity, the completion handler will be executed in background thread.
Warning: Please note that a newly added contact is synchronized as subtask (i.e. executed immediately)
@param identity: Contact identity
@param acquaintanceLevel: If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
@param entityManagerObject: Must be type of `EntityManager`, is needed to run DB on main or background context
@param onCompletion: Executed on background thread
@param onError: Executed on arbitrary thread
*/
- (void)fetchPublicKeyForIdentity:(NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(NSObject*)entityManagerObject ignoreBlockUnknown:(BOOL)ignoreBlockUnknown onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
// Don't store special contacts, return builtin public key
if ([PredefinedContactsObjC isSpecialContactWithIdentity:identity]) {
onCompletion([PredefinedContactsObjC publicKeyFor:identity]);
return;
}
NSAssert([entityManagerObject isKindOfClass:[EntityManager class]], @"Parameter entityManagerObject should be type of EntityManager");
EntityManager *em = (EntityManager *)entityManagerObject;
BusinessInjector *businessInjector = [[BusinessInjector alloc] initWithEntityManager:em];
GroupManager *groupManager = [businessInjector groupManagerObjC];
[em perform:^{
// check in local DB first
ContactEntity *contact = [em.entityFetcher contactEntityFor:identity];
if (contact.publicKey) {
if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
if (contact.isHidden == false) {
onCompletion(contact.publicKey);
return;
}
else if (contact.groupConversations.count > 0) {
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"identity == %@", contact.identity];
for (ConversationEntity *groupConversation in contact.groupConversations) {
Group *group = [groupManager getGroupWithConversation:groupConversation];
if (group.isSelfMember && [group.members filteredSetUsingPredicate:predicate].count > 0) {
onCompletion(contact.publicKey);
return;
}
}
}
DDLogVerbose(@"Block unknown contacts is on - discarding message");
onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
} else {
onCompletion(contact.publicKey);
}
} else {
if (TargetManagerObjC.isBusinessApp) {
[self fetchWorkPublicKeyForIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em ignoreBlockUnknown:ignoreBlockUnknown onCompletion:onCompletion onError:onError];
} else {
// Block message if user is not in our subscription
if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
DDLogVerbose(@"Block unknown contacts is on - discarding message");
onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
} else {
[self fetchAddContactAndSyncWithIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em onCompletion:onCompletion onError:onError];
}
}
}
}];
}
/// Fetch public key for identity in the work subscription to know if this identity should be blocked or not.
/// If identity was not found and block unknown is disabled, contact will be fetched and added in fetchAddContactAndSyncWithIdentity.
/// The completion handler will be executed in background thread
/// @param identity Contact identity
/// @param acquaintanceLevel If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
/// @param entityManagerObject Must be type of `EntityManager`, is needed to run DB on main or background context
/// @param onCompletion Executed on background thread
/// @param onError Executed on arbitrary thread
- (void)fetchWorkPublicKeyForIdentity:(nonnull NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(nonnull NSObject*)entityManagerObject ignoreBlockUnknown:(BOOL)ignoreBlockUnknown onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
EntityManager *em = (EntityManager *)entityManagerObject;
[self fetchWorkIdentities:@[identity] onCompletion:^(NSArray *foundIdentities) {
// First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
if ([em existingContactWith:identity]) {
__block NSData *publicKey;
[em performAndWait:^{
publicKey = [em.entityFetcher contactEntityFor:identity].publicKey;
}];
if (publicKey != nil) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
onCompletion(publicKey);
});
return;
}
}
dispatch_async(dispatch_get_main_queue(), ^{
if (foundIdentities.count > 0) {
for (NSDictionary *foundIdentity in foundIdentities) {
if ([foundIdentity[@"id"] isEqualToString:identity]) {
// Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
NSData *publicKey = [[NSData alloc] initWithBase64EncodedString:foundIdentity[@"pk"] options:0];
if (!publicKey) {
continue;
}
NSString *firstName = nil;
NSString *lastName = nil;
NSString *csi = nil;
NSString *jobTitle = nil;
NSString *department = nil;
WorkAvailabilityStatus *workAvailabilityStatus = nil;
if (![foundIdentity[@"first"] isEqual:[NSNull null]]) {
firstName = foundIdentity[@"first"];
}
if (![foundIdentity[@"last"] isEqual:[NSNull null]]) {
lastName = foundIdentity[@"last"];
}
if (![foundIdentity[@"csi"] isEqual:[NSNull null]]) {
csi = foundIdentity[@"csi"];
}
if (![foundIdentity[@"jobTitle"] isEqual:[NSNull null]]) {
jobTitle = foundIdentity[@"jobTitle"];
}
if (![foundIdentity[@"department"] isEqual:[NSNull null]]) {
department = foundIdentity[@"department"];
}
if (![foundIdentity[@"availability"] isEqual:[NSNull null]]) {
workAvailabilityStatus = [WorkAvailabilityStatus fromEncodedString:foundIdentity[@"availability"]];
}
[self addWorkContactAndUpdateFeatureMaskWithIdentity:identity
publicKey:publicKey
firstname:firstName
lastname:lastName csi:csi
jobTitle:jobTitle
department:department
acquaintanceLevel:acquaintanceLevel
workAvailabilityStatus:workAvailabilityStatus
onCompletion:^(NSObject * _Nonnull __unused contactObject) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
onCompletion(publicKey);
});
} onError:^(NSError * _Nonnull error) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
DDLogError(@"Add work contact failed: %@", error.localizedDescription);
onError(error);
});
}];
return;
}
}
}
// Block message if user is not in our subscription
if (userSettings.blockUnknown && ignoreBlockUnknown == false) {
DDLogVerbose(@"Block unknown contacts is on and contact not found in work list - discarding message");
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
onError([ThreemaError threemaError:@"Message received from unknown contact and block contacts is on" withCode:ThreemaProtocolErrorBlockUnknownContact]);
});
} else {
[self fetchAddContactAndSyncWithIdentity:identity acquaintanceLevel:acquaintanceLevel entityManager:em onCompletion:onCompletion onError:onError];
}
});
} onError:^(NSError *error) {
onError(error);
}];
}
/// Fetch public key for identity and add it to the contactlist. The completion handler will be executed in background thread.
/// @param identity Contact identity
/// @param acquaintanceLevel If contact new and acquaintance level is `ContactAcquaintanceLevelGroup`, than the created contact is marked as hidden
/// @param entityManagerObject Must be type of `EntityManager`, is needed to run DB on main or background context
/// @param onCompletion Executed on background thread
/// @param onError Executed on arbitrary thread
- (void)fetchAddContactAndSyncWithIdentity:(nonnull NSString*)identity acquaintanceLevel:(ContactAcquaintanceLevel)acquaintanceLevel entityManager:(nonnull NSObject*)entityManagerObject onCompletion:(void(^)(NSData *publicKey))onCompletion onError:(void(^)(NSError *error))onError {
EntityManager *em = (EntityManager *)entityManagerObject;
[[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchIdentityInfoFor:identity onCompletion:^(NSData *publicKey, NSNumber *state, NSNumber *type, NSNumber *featureMask) {
// First, check in local DB again, as it may have already been saved in the meantime (in case of parallel requests)
ContactEntity *contact = [em.entityFetcher contactEntityFor:identity];
if (contact.publicKey) {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
onCompletion(contact.publicKey);
});
return;
}
// Sync create of the contact first for MD, if it successful than store it and sync update of the contact
MediatorSyncableContacts *createContactSync = [[MediatorSyncableContacts alloc] init];
[createContactSync createWithDefaultValuesWithIdentity:identity publicKey:publicKey featureMask:featureMask.intValue isWork:NO];
[createContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
// Save new contact. Do it on main queue to ensure that it's done by the time we signal completion.
dispatch_async(dispatch_get_main_queue(), ^{
__weak typeof(self) weakSelf = self;
MediatorSyncableContacts *updateContactSync = [[MediatorSyncableContacts alloc] init];
[self addContactWithIdentity:identity publicKey:publicKey cnContactId:nil verificationLevel:ContactVerificationLevelUnverified state:state type:type featureMask:featureMask acquaintanceLevel:acquaintanceLevel alerts:NO contactSyncer:updateContactSync onCompletion:^(NSObject * __unused contactObject) {
// Set sync action to update, because sync action create was used above before contact is stored in DB
[updateContactSync setSyncActionToUpdateWithIdentity:identity];
[updateContactSync syncAsSubTaskWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
[weakSelf synchronizeAddressBookForceFullSync:YES onCompletion:nil onError:nil];
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0), ^{
onCompletion(publicKey);
});
}
else {
onError(error);
}
}];
}];
});
}
else {
onError(error);
}
}];
} 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) {
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
if (!contact) {
[identitiesToFetch addObject:identity];
}
}
if ([identitiesToFetch count] == 0) {
onCompletion();
return;
}
[[IdentityInfoFetcher sharedIdentityInfoFetcher] prefetchIdentityInfo:identitiesToFetch onCompletion:onCompletion onError:onError];
}
- (void)fetchWorkIdentities:(NSArray *)identities onCompletion:(void(^)(NSArray *foundIdentities))onCompletion onError:(void(^)(NSError *error))onError {
[[IdentityInfoFetcher sharedIdentityInfoFetcher] fetchWorkIdentitiesInfo:identities onCompletion:onCompletion onError:onError];
}
- (void)upgradeContact:(nonnull NSObject*)contactObject toVerificationLevel:(int32_t)verificationLevel {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[entityManager performAndWaitSave:^{
NSAssert([contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
ContactEntity *contact = (ContactEntity*)contactObject;
if ((contact.contactVerificationLevel < verificationLevel && contact.contactVerificationLevel != ContactVerificationLevelFullyVerified) || verificationLevel == ContactVerificationLevelFullyVerified) {
contact.contactVerificationLevel = verificationLevel;
contact.isHidden = NO;
[mediatorSyncableContacts updateVerificationLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactVerificationLevel]];
[mediatorSyncableContacts updateAcquaintanceLevelWithIdentity:contact.identity value:[NSNumber numberWithInteger:ContactAcquaintanceLevelDirect]];
}
}];
[mediatorSyncableContacts syncAsync];
}
- (void)addAsWorkWithIdentities:(NSOrderedSet *)identities contactSyncer:(nullable MediatorSyncableContacts *)mediatorSyncableContacts {
BOOL hasChanged = NO;
NSMutableOrderedSet *currentWorkIdentities = [[NSMutableOrderedSet alloc] initWithOrderedSet:[userSettings workIdentities]];
for (NSString *identity in identities) {
if (![currentWorkIdentities containsObject:identity]) {
[currentWorkIdentities addObject:identity];
[mediatorSyncableContacts updateIdentityTypeWithIdentity:identity value:@YES];
hasChanged = YES;
}
}
if (hasChanged) {
userSettings.workIdentities = currentWorkIdentities;
}
}
#pragma mark - Nickname
/**
Update nickname if is necessary.
@param identity: Identity of contact to change nickname
@param nickname: Nickname to update
*/
- (void)updateNickname:(nonnull NSString *)identity nickname:(NSString *)nickname {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[entityManager performAndWaitSave:^{
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
if (contact) {
BOOL hasChanged = NO;
if (nickname && nickname.length > 0 && ![contact.publicNickname isEqualToString:nickname]) {
if ([nickname isEqualToString:identity]) {
DDLogNotice(@"[Nickname] Set new nickname (ID) for %@", identity);
}
else {
DDLogNotice(@"[Nickname] Set new nickname for %@", identity);
}
contact.publicNickname = nickname;
hasChanged = YES;
}
else if (nickname == nil && ![contact.publicNickname isEqualToString:contact.identity]) {
DDLogNotice(@"[Nickname] Set ID as nickname for %@", identity);
contact.publicNickname = contact.identity;
hasChanged = YES;
}
if (hasChanged) {
[mediatorSyncableContacts updateNicknameWithIdentity:contact.identity value:contact.publicNickname];
}
}
}];
if ([userSettings enableMultiDevice]) {
[mediatorSyncableContacts syncAsync];
}
}
#pragma mark - Profile Picture
- (void)updateProfilePicture:(nullable NSString *)identity imageData:(NSData *)imageData shouldReflect:(BOOL)shouldReflect blobID:(nullable NSData *)blobID encryptionKey:(nullable NSData *)encryptionKey didFailWithError:(NSError * _Nullable __autoreleasing * _Nullable)error {
UIImage *image = [UIImage imageWithData:imageData];
if (image == nil) {
*error = [ThreemaError threemaError:@"Image decoding failed"];
return;
}
__block ContactEntity *contact;
[entityManager performAndWaitSave:^{
contact = [entityManager.entityFetcher contactEntityFor:identity];
if (contact) {
ImageDataEntity *dbImage = [entityManager.entityCreator imageDataEntityWithData:imageData size:image.size message:nil];
contact.contactImage = dbImage;
}
}];
if (!contact) {
*error = [ThreemaError threemaError:@"Contact not found"];
return;
}
[self removeProfilePictureRequest:identity];
if ([userSettings enableMultiDevice] && shouldReflect) {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[mediatorSyncableContacts setContactProfileUpdateTypeWithIdentity:identity value:DeltaUpdateTypeUpdated blobID:blobID encryptionKey:encryptionKey];
[mediatorSyncableContacts syncAsync];
}
}
- (void)deleteProfilePicture:(nullable NSString *)identity shouldReflect:(BOOL)shouldReflect {
__block ContactEntity *contact;
[entityManager performAndWaitSave:^{
contact = [entityManager.entityFetcher contactEntityFor:identity];
if (contact) {
contact.contactImage = nil;
}
}];
if (!contact) {
return;
}
[self removeProfilePictureRequest:identity];
if ([userSettings enableMultiDevice] && shouldReflect) {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[mediatorSyncableContacts setContactProfileUpdateTypeWithIdentity:identity value:DeltaUpdateTypeRemoved blobID:nil encryptionKey:nil];
[mediatorSyncableContacts syncAsync];
}
}
- (void)removeProfilePictureFlagForAllContacts {
[entityManager performSave:^{
NSArray *allContacts = [entityManager.entityFetcher contactEntities];
if (allContacts != nil) {
for (ContactEntity *contact in allContacts) {
contact.profilePictureSent = NO;
contact.profilePictureBlobID = nil;
}
}
}];
}
- (void)removeProfilePictureFlagForIdentity:(NSString *)identity {
[entityManager performAndWaitSave:^{
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor:identity];
if (contact) {
contact.profilePictureSent = NO;
contact.profilePictureBlobID = nil;
}
}];
}
- (BOOL)existsProfilePictureRequestForIdentity:(NSString *)identity {
@synchronized (self) {
return [[userSettings profilePictureRequestList] containsObject:identity];
}
}
- (void)removeProfilePictureRequest:(NSString *)identity {
@synchronized (self) {
if ([self existsProfilePictureRequestForIdentity:identity]) {
NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:userSettings.profilePictureRequestList];
[profilePictureRequestList removeObject:identity];
userSettings.profilePictureRequestList = profilePictureRequestList.allObjects;
}
}
}
- (void)addProfilePictureRequest:(NSString *)identity {
@synchronized (self) {
if (![self existsProfilePictureRequestForIdentity:identity]) {
NSMutableSet *profilePictureRequestList = [NSMutableSet setWithArray:userSettings.profilePictureRequestList];
[profilePictureRequestList addObject:identity];
userSettings.profilePictureRequestList = profilePictureRequestList.allObjects;
}
}
}
#pragma mark - Sync Address Book
- (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 (ProcessInfoHelper.isRunningForScreenshots) {
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.syncContacts) {
DDLogInfo(@"Contact sync is disabled");
[self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
return;
}
CNContactStore *cnAddressBook = [CNContactStore new];
if (cnAddressBook == nil) {
DDLogInfo(@"Address book not found");
[self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
return;
}
CNAuthorizationStatus currentStatus = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts];
if (currentStatus != CNAuthorizationStatusAuthorized && currentStatus != CNAuthorizationStatusLimited) {
[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);
[self processStatusUpdateOnlyWithFullServerSync:forceFullSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
}
}];
return;
}
dispatch_async(_syncContactsQueue, ^{
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;
}
}
DDLogNotice(@"[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];
if (cnContacts != nil) {
[allCNContacts addObjectsFromArray:cnContacts];
}
}
[self processAddressBookContacts:allCNContacts fullServerSync:fullServerSync ignoreMinimumInterval:ignoreMinimumInterval onCompletion:onCompletion onError:onError];
}
}];
});
}
/**
Process status request/update to all contacts.
@param fullServerSync: If true do MD sync of all contacts
@param ignoreMinimumInterval: True contact status request/update will be called anyway
@param onCompletion: Completion handler
@param onError: Error handler
*/
- (void)processStatusUpdateOnlyWithFullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError * _Nonnull))onError {
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[self updateStatusForAllContactsIgnoreInterval:ignoreMinimumInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
if (onCompletion != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
onCompletion(NO);
});
}
}
else {
DDLogError(@"[ContactSync] Contact multi device sync failed: %@", [error localizedDescription]);
if (onError != nil) {
onError(error);
}
}
}];
});
} onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
});
}
/**
Process address book contacts and status request/update to all contacts.
@param contacts: Address book contacts to add or update as Threema contact
@param fullServerSync: True sync all address book contacts otherwise just the new ones (If true do MD sync of all contacts)
@param ignoreMinimumInterval: True contact status request/update will be called anyway
@param onCompletion: Completion handler
@param onError: Error handler
*/
- (void)processAddressBookContacts:(NSArray*)contacts fullServerSync:(BOOL)fullServerSync ignoreMinimumInterval:(BOOL)ignoreMinimumInterval onCompletion:(void(^)(BOOL addressBookAccessGranted))onCompletion onError:(void(^)(NSError *error))onError {
NSUserDefaults *defaults = [AppGroup userDefaults];
PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer new];
NSSet *emailLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastEmailHashes"]];
NSSet *mobileNoLastCheck = [NSSet setWithArray:[defaults objectForKey:@"ContactsSyncLastMobileNoHashes"]];
NSString *countryCode = [normalizer 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 = [ContactStore 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 = [ContactStore 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 e164FormatFrom:phone defaultRegion:countryCode];
if (mobileNoNormalized == nil)
continue;
NSString *mobileNoHashBase64 = [ContactStore 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;
}
DDLogNotice(@"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];
DDLogNotice(@"[ContactSync] Start Core Data stuff");
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[self addContactsWithIdentities:identities emailHashes:emailHashToCnContactId mobileNoHashes:mobileNoHashToCnContactId contactSyncer:mediatorSyncableContacts onCompletion:^{
// trigger updating of status for identities
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
DDLogNotice(@"[ContactSync] Update status and featuremask for all contacts");
[self updateStatusForAllContactsIgnoreInterval:ignoreMinimumInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, (unsigned long)NULL), ^{
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error == nil) {
if (fullServerSync) {
lastFullSyncDate = [NSDate date];
}
DDLogNotice(@"[ContactSync] Address book sync finished");
if (onCompletion != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
onCompletion(YES);
});
}
}
else {
DDLogError(@"[ContactSync] Contact multi device sync failed: %@", [error localizedDescription]);
if (onCompletion) onCompletion(YES);
}
}];
});
} onError:nil]; // To keep the existing behavior we silently discard errors. // TODO: (IOS-4425) This should probably be changed
});
}];
} onError:^(NSError *error) {
DDLogError(@"[ContactSync] Synchronize address book failed: %@", error);
if (onError != nil) {
dispatch_async(dispatch_get_main_queue(), ^{
onError(error);
});
}
}];
}
+ (void)linkedIdentitiesForEmail:(NSString *)email AndMobileNo:(NSString *)mobileNo onCompletion:(void(^)(NSArray *identities))onCompletion {
NSArray<NSString *> *emailHashesBase64 = [NSArray array];
NSArray<NSString *> *mobileNoHashesBase64 = [NSArray array];
if (email.length > 0) {
NSString *emailNormalized = [[email lowercaseString] stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSString *emailHashBase64 = [ContactStore hashEmailBase64:emailNormalized];
emailHashesBase64 = @[emailHashBase64];
}
if (mobileNo.length > 0) {
/* normalize phone number first */
PhoneNumberNormalizer *normalizer = [PhoneNumberNormalizer new];
NSString *countryCode = [normalizer userRegion];
NSString *mobileNoNormalized = [normalizer e164FormatFrom:mobileNo defaultRegion:countryCode];
if (mobileNoNormalized != nil) {
NSString *mobileNoHashBase64 = [ContactStore 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) {
if (identities == nil) {
NSArray *emptyArray = [NSArray array];
onCompletion(emptyArray);
} else {
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 {
NSSet *identitiesSet = [entityManager.entityFetcher contactIdentities];
return [identitiesSet allObjects];
}
- (NSArray<NSString *> *)contactsWithFeatureMaskNil {
__block NSArray<NSString *> *identities;
[entityManager performAndWait:^{
identities = [self identitiesForContacts:[entityManager.entityFetcher contactEntitiesWithFeatureMaskNil]];
}];
return identities;
}
- (NSArray *)allContacts {
return [entityManager.entityFetcher contactEntities];
}
- (void)orderChanged:(NSNotification*)notification {
[entityManager performSave:^{
/* update display name and sort index of all contacts */
NSArray *allContacts = [entityManager.entityFetcher contactEntities];
if (allContacts != nil) {
for (ContactEntity *contact in allContacts) {
/* set last name again to trigger update of display name and sort index */
[contact setLastNameTo:contact.lastName sortOrderFirstName:userSettings.sortOrderFirstName];
}
}
}];
}
- (NSArray<NSString *> *)identitiesForContacts:(NSArray<ContactEntity *> *)contacts {
NSMutableArray *identities = [NSMutableArray arrayWithCapacity:contacts.count];
for (ContactEntity *contact in contacts) {
[identities addObject:contact.identity];
}
return identities;
}
- (NSArray *)validIdentities {
NSMutableArray *identities = [[NSMutableArray alloc] init];
EntityManager *backgroundEntityManager = [self backgroundEntityManager];
[backgroundEntityManager performAndWait:^{
NSArray *contacts = [backgroundEntityManager.entityFetcher contactEntities];
for (ContactEntity *contact in contacts) {
if (contact.contactState != ContactStateInvalid) {
[identities addObject:contact.identity];
}
}
}];
return identities;
}
- (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 onCompletion:^{
DDLogNotice(@"Successfully updated status for all contacts");
} onError:^(NSError * _Nonnull error) {
DDLogError(@"Contact multi device sync failed: %@", [error localizedDescription]);
}];
}
- (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval onCompletion:(nonnull void(^)(void))onCompletion onError:(nonnull void(^)(NSError * _Nonnull error))onError {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[self updateStatusForAllContactsIgnoreInterval:ignoreInterval contactSyncer:mediatorSyncableContacts onCompletion:^{
[mediatorSyncableContacts syncObjcWithCompletionHandler:^(NSError * _Nullable error) {
if (error) {
onError(error);
}
else {
onCompletion();
}
}];
} onError:^(NSError *error) {
if (error != nil) {
onError(error);
}
// This should hardly ever happen so we just call the completion handler.
else {
DDLogWarn(@"Update status for all contact failed, but error is nil.");
onCompletion();
}
}];
}
- (void)updateStatusForAllContactsIgnoreInterval:(BOOL)ignoreInterval contactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion onError:(nullable void(^)(NSError *error))onError {
if (ProcessInfoHelper.isRunningForScreenshots) {
// do nothing
} else {
if ([self needCheckStatus:ignoreInterval] == NO) {
DDLogNotice(@"[ContactSync] Do not update status and featuremasks");
if (onCompletion) {
onCompletion();
}
return;
}
[self updateStatusWithContactSyncer:mediatorSyncableContacts onCompletion:^() {
[self setupCheckStatusTimer];
DDLogNotice(@"[ContactSync] Update status and featuremasks finished");
if (onCompletion) {
onCompletion();
}
} onError:^(NSError *error){
[self setupCheckStatusTimer];
DDLogError(@"[ContactSync] Update status featuremasks finished with error: %@", error);
// For backwards compatibility the error handler is optional
// TODO: (IOS-4425) This should always return an error if it fails such that it can be handled by a caller if needed.
if (onError) {
onError(error);
} else if (onCompletion) {
onCompletion();
}
}];
}
}
- (void)updateStatusWithContactSyncer:(MediatorSyncableContacts *)mediatorSyncableContacts onCompletion:(void(^)(void))onCompletion onError:(void(^)(NSError *error))onError {
NSArray *identities = [self validIdentities];
ServerAPIConnector *conn = [[ServerAPIConnector alloc] init];
[conn checkStatusOfIdentities:identities onCompletion:^(NSArray *states, NSArray *types, NSArray *featureMasks, int checkInterval) {
[entityManager performAndWaitSave:^{
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];
ContactEntity *contact = [entityManager.entityFetcher contactEntityFor: identityString];
if ([ProcessInfoHelper isRunningForScreenshots]) {
// do not update active/inactive state for screenshots
}
else {
if (contact.contactState != state.intValue) {
contact.contactState = (ContactState)state.intValue;
[mediatorSyncableContacts updateStateWithIdentity:contact.identity value:[NSNumber numberWithInteger: contact.contactState]];
}
}
if ([type isEqualToNumber:@1]) {
[workIdentities addObject:contact.identity];
}
if (![featureMask isEqual:[NSNull null]]) {
NSNumber *oldFeatureMask = contact.featureMask;
[contact setFeatureMaskTo:featureMask.integerValue]; // Always update feature mask of local contact
if (![oldFeatureMask isEqualToNumber:featureMask]) {
[mediatorSyncableContacts updateFeatureMaskWithIdentity:contact.identity value:contact.featureMask];
}
}
}
if (![[NSUserDefaults standardUserDefaults] boolForKey:@"FASTLANE_SNAPSHOT"]) {
[self addAsWorkWithIdentities:workIdentities contactSyncer:mediatorSyncableContacts];
}
}];
NSUserDefaults *defaults = [AppGroup userDefaults];
[defaults setInteger:checkInterval forKey:@"CheckStatusInterval"];
[defaults synchronize];
onCompletion();
} onError:^(NSError *error) {
onError(error);
}];
}
- (NSArray<NSDictionary<NSString *, NSString *> *> *)cnContactEmailsForContact:(NSObject *)contactObject {
NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
ContactEntity *contact = (ContactEntity*)contactObject;
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<NSDictionary<NSString *, NSString *> *> *emails = [NSMutableArray new];
if (cnContacts.count == 1) {
for (CNContact *person in cnContacts) {
for (CNLabeledValue<NSString *> *label in person.emailAddresses) {
NSMutableDictionary<NSString *, NSString *> *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<NSDictionary<NSString *, NSString *> *> *)cnContactPhoneNumbersForContact:(NSObject *)contactObject {
NSAssert(contactObject == nil || [contactObject isKindOfClass:[ContactEntity class]], @"Parameter contactObject must be type of ContactEntity");
ContactEntity *contact = (ContactEntity*)contactObject;
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<NSDictionary<NSString *, NSString *> *> *phoneNumbers = [NSMutableArray new];
if (cnContacts.count == 1) {
for (CNContact *person in cnContacts) {
for (CNLabeledValue<CNPhoneNumber *> *label in person.phoneNumbers) {
NSMutableDictionary<NSString *, NSString *> *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];
}
#pragma mark - Read receipts
- (void)resetCustomReadReceipts {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[entityManager performSave:^{
NSArray *contactsWithCustomReadReceipts = [entityManager.entityFetcher contactEntitiesWithCustomReadReceipt];
if (contactsWithCustomReadReceipts != nil) {
for (ContactEntity *contact in contactsWithCustomReadReceipts) {
contact.readReceipt = ReadReceiptDefault;
[mediatorSyncableContacts updateReadReceiptWithIdentity:contact.identity value:ReadReceiptDefault];
}
}
}];
[mediatorSyncableContacts syncAsync];
}
#pragma mark - Multi Device Sync
- (void)reflectContact:(NSString *)identity {
MediatorSyncableContacts *mediatorSyncableContacts = [[MediatorSyncableContacts alloc] init];
[mediatorSyncableContacts updateAllWithIdentity:identity added:NO];
[mediatorSyncableContacts syncAsync];
}
@end
¤ Dauer der Verarbeitung: 0.25 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland
|
|
2026-11-01
|
|
|
|
|
Neuigkeiten |
| Aktuelles |
| Motto des Tages |
|
Open Source Software |
|
|
|
Jenseits des Üblichen ....
|
|
Besucherstatistik |
|
|
| Statistik |
| #Sources=1104850 |
| #Domains=416977 |
|
|