968 lines
38 KiB
Objective-C
968 lines
38 KiB
Objective-C
//
|
|
// MLXMPPManager.m
|
|
// Monal
|
|
//
|
|
// Created by Anurodh Pokharel on 6/27/13.
|
|
//
|
|
//
|
|
|
|
#import <UserNotifications/UserNotifications.h>
|
|
|
|
#import "MLConstants.h"
|
|
#import "MLXMPPManager.h"
|
|
#import "DataLayer.h"
|
|
#import "HelperTools.h"
|
|
#import "xmpp.h"
|
|
#import "XMPPMessage.h"
|
|
#import "MLNotificationQueue.h"
|
|
#import "MLNotificationManager.h"
|
|
#import "MLOMEMO.h"
|
|
#import <monalxmpp/monalxmpp-Swift.h>
|
|
|
|
@import Network;
|
|
@import MobileCoreServices;
|
|
@import SAMKeychain;
|
|
@import Intents;
|
|
|
|
static const int pingFreqencyMinutes = 5; //about the same Conversations uses
|
|
#define FIRST_LOGIN_TIMEOUT 30.0
|
|
|
|
@interface MLXMPPManager()
|
|
{
|
|
nw_path_monitor_t _path_monitor;
|
|
BOOL _hasConnectivity;
|
|
NSMutableArray* _connectedXMPP;
|
|
}
|
|
@end
|
|
|
|
@implementation MLXMPPManager
|
|
|
|
-(void) defaultSettings
|
|
{
|
|
[self upgradeBoolUserSettingsIfUnset:@"Sound" toDefault:YES];
|
|
[self upgradeObjectUserSettingsIfUnset:@"AlertSoundFile" toDefault:@"alert2"];
|
|
|
|
// upgrade ShowGeoLocation
|
|
[self upgradeBoolUserSettingsIfUnset:@"ShowGeoLocation" toDefault:YES];
|
|
|
|
// upgrade SendLastUserInteraction
|
|
[self upgradeBoolUserSettingsIfUnset:@"SendLastUserInteraction" toDefault:YES];
|
|
|
|
// upgrade SendLastChatState
|
|
[self upgradeBoolUserSettingsIfUnset:@"SendLastChatState" toDefault:YES];
|
|
|
|
// upgrade received and displayed markers
|
|
[self upgradeBoolUserSettingsIfUnset:@"SendReceivedMarkers" toDefault:YES];
|
|
[self upgradeBoolUserSettingsIfUnset:@"SendDisplayedMarkers" toDefault:YES];
|
|
|
|
//upgrade url preview
|
|
[self upgradeBoolUserSettingsIfUnset:@"ShowURLPreview" toDefault:YES];
|
|
|
|
//upgrade message autodeletion and migrate old "3 days" setting
|
|
NSNumber* oldAutodelete = [[HelperTools defaultsDB] objectForKey:@"AutodeleteAllMessagesAfter3Days"];
|
|
if(oldAutodelete != nil && [oldAutodelete boolValue])
|
|
{
|
|
[self upgradeIntegerUserSettingsIfUnset:@"AutodeleteInterval" toDefault:259200];
|
|
[self removeObjectUserSettingsIfSet:@"AutodeleteAllMessagesAfter3Days"];
|
|
}
|
|
else
|
|
[self upgradeIntegerUserSettingsIfUnset:@"AutodeleteInterval" toDefault:0];
|
|
|
|
//upgrade default omemo on
|
|
[self upgradeBoolUserSettingsIfUnset:@"OMEMODefaultOn" toDefault:YES];
|
|
|
|
// upgrade udp logger
|
|
[self upgradeBoolUserSettingsIfUnset:@"udpLoggerEnabled" toDefault:NO];
|
|
[self upgradeObjectUserSettingsIfUnset:@"udpLoggerHostname" toDefault:@""];
|
|
[self upgradeObjectUserSettingsIfUnset:@"udpLoggerPort" toDefault:@""];
|
|
[self upgradeObjectUserSettingsIfUnset:@"udpLoggerKey" toDefault:@""];
|
|
|
|
// upgrade Message Settings / Privacy
|
|
[self upgradeIntegerUserSettingsIfUnset:@"NotificationPrivacySetting" toDefault:NotificationPrivacySettingOptionDisplayNameAndMessage];
|
|
|
|
// upgrade filetransfer settings
|
|
[self upgradeBoolUserSettingsIfUnset:@"AutodownloadFiletransfers" toDefault:YES];
|
|
|
|
//upgrade syncErrorsDisplayed list
|
|
[self upgradeObjectUserSettingsIfUnset:@"syncErrorsDisplayed" toDefault:@{}];
|
|
|
|
[self upgradeFloatUserSettingsToInteger:@"AutodownloadFiletransfersMobileMaxSize"];
|
|
[self upgradeFloatUserSettingsToInteger:@"AutodownloadFiletransfersWifiMaxSize"];
|
|
[self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersMobileMaxSize" toDefault:5*1024*1024]; // 5 MiB
|
|
[self upgradeIntegerUserSettingsIfUnset:@"AutodownloadFiletransfersWifiMaxSize" toDefault:32*1024*1024]; // 32 MiB
|
|
|
|
// upgrade default image quality
|
|
[self upgradeFloatUserSettingsIfUnset:@"ImageUploadQuality" toDefault:0.50];
|
|
|
|
// remove old settings from shareSheet outbox
|
|
[self removeObjectUserSettingsIfSet:@"lastRecipient"];
|
|
[self removeObjectUserSettingsIfSet:@"lastAccount"];
|
|
// remove HasSeenIntro bool
|
|
[self removeObjectUserSettingsIfSet:@"HasSeenIntro"];
|
|
|
|
// add default pushserver
|
|
[self upgradeObjectUserSettingsIfUnset:@"selectedPushServer" toDefault:[HelperTools getSelectedPushServerBasedOnLocale]];
|
|
|
|
//upgrade background image settings
|
|
NSString* bgImage = [[HelperTools defaultsDB] objectForKey:@"BackgroundImage"];
|
|
//image was selected, but it was no custom image --> remove it
|
|
if(bgImage != nil && [@"CUSTOM" isEqualToString:bgImage])
|
|
[self removeObjectUserSettingsIfSet:@"BackgroundImage"];
|
|
[self removeObjectUserSettingsIfSet:@"ChatBackgrounds"];
|
|
|
|
// add STUN / TURN settings
|
|
[self upgradeBoolUserSettingsIfUnset:@"webrtcAllowP2P" toDefault:YES];
|
|
#ifdef IS_QUICKSY
|
|
[self upgradeBoolUserSettingsIfUnset:@"webrtcUseFallbackTurn" toDefault:NO];
|
|
#else
|
|
[self upgradeBoolUserSettingsIfUnset:@"webrtcUseFallbackTurn" toDefault:YES];
|
|
#endif
|
|
|
|
//jabber:iq:version
|
|
[self upgradeBoolUserSettingsIfUnset:@"allowVersionIQ" toDefault:YES];
|
|
|
|
//default value for sanbox is no (e.g. production)
|
|
[self upgradeBoolUserSettingsIfUnset:@"isSandboxAPNS" toDefault:NO];
|
|
|
|
//anti spam/privacy setting, but default to yes (current behavior, conversations behavior etc.)
|
|
[self upgradeBoolUserSettingsIfUnset:@"allowNonRosterContacts" toDefault:YES];
|
|
[self upgradeBoolUserSettingsIfUnset:@"allowCallsFromNonRosterContacts" toDefault:YES];
|
|
|
|
//mac catalyst will not show a soft-keyboard when setting focus, ios will
|
|
//--> only automatically set focus on macos and make this configurable
|
|
#if TARGET_OS_MACCATALYST
|
|
[self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:YES];
|
|
#else
|
|
[self upgradeBoolUserSettingsIfUnset:@"showKeyboardOnChatOpen" toDefault:NO];
|
|
#endif
|
|
|
|
#ifdef IS_ALPHA
|
|
[self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:YES];
|
|
#else
|
|
[self upgradeBoolUserSettingsIfUnset:@"useDnssecForAllConnections" toDefault:NO];
|
|
#endif
|
|
|
|
NSTimeZone* timeZone = [NSTimeZone localTimeZone];
|
|
DDLogVerbose(@"Current timezone name: '%@'...", [timeZone name]);
|
|
if([[timeZone name] containsString:@"Europe"])
|
|
[self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:NO];
|
|
else
|
|
[self upgradeBoolUserSettingsIfUnset:@"useInlineSafari" toDefault:YES];
|
|
|
|
[self upgradeBoolUserSettingsIfUnset:@"hasCompletedOnboarding" toDefault:NO];
|
|
|
|
[self upgradeBoolUserSettingsIfUnset:@"uploadImagesOriginal" toDefault:NO];
|
|
|
|
[self upgradeBoolUserSettingsIfUnset:@"hardlinkFiletransfersIntoDocuments" toDefault:YES];
|
|
|
|
[self upgradeBoolUserSettingsIfUnset:@"showAdvancedUI" toDefault:NO];
|
|
|
|
// //always show onboarding on simulator for now
|
|
// #if TARGET_OS_SIMULATOR
|
|
// [[HelperTools defaultsDB] setBool:NO forKey:@"hasCompletedOnboarding"];
|
|
// #endif
|
|
}
|
|
|
|
-(void) upgradeFloatUserSettingsToInteger:(NSString*) settingsName
|
|
{
|
|
if([[HelperTools defaultsDB] objectForKey:settingsName] == nil)
|
|
return;
|
|
NSInteger value = (NSInteger)[[HelperTools defaultsDB] floatForKey:settingsName];
|
|
[[HelperTools defaultsDB] setInteger:value forKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
|
|
-(void) upgradeBoolUserSettingsIfUnset:(NSString*) settingsName toDefault:(BOOL) defaultVal
|
|
{
|
|
NSNumber* currentSettingVal = [[HelperTools defaultsDB] objectForKey:settingsName];
|
|
if(currentSettingVal == nil)
|
|
{
|
|
[[HelperTools defaultsDB] setBool:defaultVal forKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
}
|
|
|
|
-(void) upgradeIntegerUserSettingsIfUnset:(NSString*) settingsName toDefault:(NSInteger) defaultVal
|
|
{
|
|
NSNumber* currentSettingVal = [[HelperTools defaultsDB] objectForKey:settingsName];
|
|
if(currentSettingVal == nil)
|
|
{
|
|
[[HelperTools defaultsDB] setInteger:defaultVal forKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
}
|
|
|
|
-(void) upgradeFloatUserSettingsIfUnset:(NSString*) settingsName toDefault:(float) defaultVal
|
|
{
|
|
NSNumber* currentSettingVal = [[HelperTools defaultsDB] objectForKey:settingsName];
|
|
if(currentSettingVal == nil)
|
|
{
|
|
[[HelperTools defaultsDB] setFloat:defaultVal forKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
}
|
|
|
|
-(void) upgradeObjectUserSettingsIfUnset:(NSString*) settingsName toDefault:(nullable id) defaultVal
|
|
{
|
|
NSNumber* currentSettingVal = [[HelperTools defaultsDB] objectForKey:settingsName];
|
|
if(currentSettingVal == nil)
|
|
{
|
|
[[HelperTools defaultsDB] setObject:defaultVal forKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
}
|
|
|
|
-(void) removeObjectUserSettingsIfSet:(NSString*) settingsName
|
|
{
|
|
NSObject* currentSettingsVal = [[HelperTools defaultsDB] objectForKey:settingsName];
|
|
if(currentSettingsVal != nil)
|
|
{
|
|
DDLogInfo(@"Removing defaultsDB Entry %@", settingsName);
|
|
[[HelperTools defaultsDB] removeObjectForKey:settingsName];
|
|
[[HelperTools defaultsDB] synchronize];
|
|
}
|
|
}
|
|
|
|
+(MLXMPPManager*) sharedInstance
|
|
{
|
|
static dispatch_once_t once;
|
|
static MLXMPPManager* sharedInstance;
|
|
dispatch_once(&once, ^{
|
|
sharedInstance = [MLXMPPManager new] ;
|
|
});
|
|
return sharedInstance;
|
|
}
|
|
|
|
-(id) init
|
|
{
|
|
self = [super init];
|
|
|
|
_connectedXMPP = [NSMutableArray new];
|
|
_hasConnectivity = NO;
|
|
_isBackgrounded = NO;
|
|
_isNotInFocus = NO;
|
|
_onMobile = NO;
|
|
_isConnectBlocked = NO;
|
|
|
|
[self defaultSettings];
|
|
[self setPushToken:nil]; //load push settings from defaultsDB (can be overwritten later on in mainapp, but *not* in appex)
|
|
|
|
//set up regular ping
|
|
dispatch_queue_t q_background = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_LOW, 0);
|
|
_pinger = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, q_background);
|
|
|
|
dispatch_source_set_timer(_pinger,
|
|
DISPATCH_TIME_NOW,
|
|
60ull * NSEC_PER_SEC * pingFreqencyMinutes,
|
|
60ull * NSEC_PER_SEC); //allow for better battery optimizations
|
|
|
|
dispatch_source_set_event_handler(_pinger, ^{
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
{
|
|
if(xmppAccount.accountState>=kStateBound) {
|
|
DDLogInfo(@"began a idle ping");
|
|
[xmppAccount sendPing:LONG_PING]; //long ping timeout because this is a background/interval ping
|
|
}
|
|
}
|
|
});
|
|
|
|
dispatch_source_set_cancel_handler(_pinger, ^{
|
|
DDLogInfo(@"pinger canceled");
|
|
});
|
|
|
|
dispatch_resume(_pinger);
|
|
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(handleSentMessage:) name:kMonalSentMessageNotice object:nil];
|
|
|
|
//this processes the sharesheet outbox only, the handler in the NotificationServiceExtension will do more interesting things
|
|
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(catchupFinished:) name:kMonalFinishedCatchup object:nil];
|
|
|
|
_path_monitor = nw_path_monitor_create();
|
|
nw_path_monitor_set_queue(_path_monitor, q_background);
|
|
nw_path_monitor_set_update_handler(_path_monitor, ^(nw_path_t path) {
|
|
DDLogVerbose(@"*** nw_path_monitor: update_handler called");
|
|
DDLogDebug(@"*** nw_path_monitor: nw_path_is_constrained=%@", bool2str(nw_path_is_constrained(path)));
|
|
DDLogDebug(@"*** nw_path_monitor: nw_path_is_expensive=%@", bool2str(nw_path_is_expensive(path)));
|
|
self->_onMobile = nw_path_is_constrained(path) || nw_path_is_expensive(path);
|
|
DDLogDebug(@"*** nw_path_monitor: on 'mobile' --> %@", bool2str(self->_onMobile));
|
|
if(nw_path_get_status(path) == nw_path_status_satisfied && !self->_hasConnectivity)
|
|
{
|
|
DDLogVerbose(@"reachable again");
|
|
self->_hasConnectivity = YES;
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
{
|
|
if(![HelperTools isAppExtension])
|
|
{
|
|
//try to send a ping. if it fails, it will reconnect
|
|
DDLogVerbose(@"manager pinging");
|
|
[xmppAccount sendPing:SHORT_PING]; //short ping timeout to quickly check if connectivity is still okay
|
|
}
|
|
else
|
|
{
|
|
//don't reconnect if appex has frozen our queues!
|
|
if(!xmppAccount.parseQueueFrozen)
|
|
[xmppAccount reconnect:0]; //try to immediately reconnect, don't bother pinging
|
|
else
|
|
DDLogDebug(@"Not trying to reconnect in 0s, parse queue frozen!");
|
|
}
|
|
}
|
|
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalConnectivityChange object:self userInfo:@{@"reachable": @YES}];
|
|
}
|
|
else if(nw_path_get_status(path) != nw_path_status_satisfied && self->_hasConnectivity)
|
|
{
|
|
DDLogVerbose(@"NOT reachable");
|
|
self->_hasConnectivity = NO;
|
|
|
|
DDLogVerbose(@"scheduling background fetching task to start app in background once our connectivity gets restored");
|
|
//this will automatically start the app if connectivity gets restored
|
|
//always force as soon as possible to make sure any missed pushes get compensated for
|
|
//don't queue this notification because it should be handled immediately
|
|
[[NSNotificationCenter defaultCenter] postNotificationName:kScheduleBackgroundTask object:nil userInfo:@{@"force": @YES}];
|
|
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalConnectivityChange object:self userInfo:@{@"reachable": @NO}];
|
|
}
|
|
else if(nw_path_get_status(path) == nw_path_status_satisfied)
|
|
{
|
|
DDLogVerbose(@"still reachable");
|
|
//when switching from wifi to mobile (or back) we sometimes don't have any unreachable state in between
|
|
//--> reconnect directly because switching from wifi to mobile will cut the connection a few seconds after the switch anyways
|
|
//NOTE: wait for 1 sec before reconnecting to compensate for multiple nw_path updates in a row
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
//don't reconnect if appex has frozen our queues!
|
|
if(!xmppAccount.parseQueueFrozen)
|
|
{
|
|
[NSThread sleepForTimeInterval:1];
|
|
[xmppAccount sendPing:SHORT_PING]; //short ping timeout to quickly check if connectivity is still okay
|
|
}
|
|
else
|
|
DDLogDebug(@"Not pinging after 1s, parse queue frozen!");
|
|
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalConnectivityChange object:self userInfo:@{@"reachable": @YES}];
|
|
}
|
|
else
|
|
DDLogVerbose(@"nothing changed, still NOT reachable");
|
|
});
|
|
nw_path_monitor_start(_path_monitor);
|
|
|
|
//trigger iq invalidations and idle timers from a background thread because timeouts aren't time critical
|
|
//we use this to decrement the timeout value of an iq handler / idle timer every second until it reaches zero
|
|
dispatch_async(dispatch_queue_create_with_target("im.monal.timeouts", DISPATCH_QUEUE_SERIAL, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0)), ^{
|
|
while(YES) {
|
|
for(xmpp* account in [MLXMPPManager sharedInstance].connectedXMPP)
|
|
[account updateIqHandlerTimeouts];
|
|
|
|
//needed to not crash the app with an obscure EXC_BREAKPOINT while deleting something in a currently open chat
|
|
//the crash report then contains: message at /usr/lib/system/libdispatch.dylib: API MISUSE: Resurrection of an object
|
|
//(triggered by [HelperTools dispatchAsync:reentrantOnQueue:withBlock:] in it's call to dispatch_get_current_queue())
|
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
NSInteger autodeleteInterval = [[HelperTools defaultsDB] integerForKey:@"AutodeleteInterval"];
|
|
if(autodeleteInterval > 0)
|
|
{
|
|
NSNumber* deletionCount = [[DataLayer sharedInstance] autoDeleteMessagesAfterInterval:(NSTimeInterval)autodeleteInterval];
|
|
//make sure our ui updates after a deletion
|
|
if(deletionCount.integerValue > 0)
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
|
|
}
|
|
});
|
|
|
|
[NSThread sleepForTimeInterval:1];
|
|
}
|
|
});
|
|
|
|
return self;
|
|
}
|
|
|
|
-(void) dealloc
|
|
{
|
|
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
|
if(_pinger)
|
|
dispatch_source_cancel(_pinger);
|
|
}
|
|
|
|
//this returns a copy to iterate on without the need of a synchronized block while iterating
|
|
-(NSArray*) connectedXMPP
|
|
{
|
|
@synchronized(_connectedXMPP) {
|
|
return [[NSArray alloc] initWithArray:_connectedXMPP];
|
|
}
|
|
}
|
|
|
|
-(void) catchupFinished:(NSNotification*) notification
|
|
{
|
|
xmpp* account = notification.object;
|
|
DDLogInfo(@"### MAM/SMACKS CATCHUP FINISHED FOR ACCOUNT NO %@ ###", account.accountID);
|
|
}
|
|
|
|
-(BOOL) allAccountsIdle
|
|
{
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
if(!xmppAccount.idle)
|
|
return NO;
|
|
return YES;
|
|
}
|
|
|
|
#pragma mark - app state
|
|
|
|
-(void) noLongerInFocus
|
|
{
|
|
_isBackgrounded = NO;
|
|
_isNotInFocus = YES;
|
|
}
|
|
|
|
-(void) nowBackgrounded
|
|
{
|
|
DDLogInfo(@"App now backgrounded...");
|
|
|
|
_isBackgrounded = YES;
|
|
_isNotInFocus = YES;
|
|
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
[xmppAccount setClientInactive];
|
|
}
|
|
|
|
-(void) nowForegrounded
|
|
{
|
|
DDLogInfo(@"App now foregrounded...");
|
|
|
|
_isBackgrounded = NO;
|
|
_isNotInFocus = NO;
|
|
|
|
//*** we don't need to check for a running service extension here because the appdelegate does this already for us ***
|
|
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
{
|
|
[xmppAccount unfreeze];
|
|
[xmppAccount sendPing:SHORT_PING]; //short ping timeout to quickly check if connectivity is still okay
|
|
[xmppAccount setClientActive];
|
|
}
|
|
|
|
//we are in foreground now (or at least we have been for a few seconds)
|
|
//--> clear sync error notifications so that they can appear again
|
|
//wait some time to make sure all xmpp class instances have been created
|
|
createTimer(1, (^{
|
|
[HelperTools clearSyncErrorsOnAppForeground];
|
|
}));
|
|
}
|
|
|
|
#pragma mark - Connection related
|
|
|
|
-(BOOL) isAccountForIdConnected:(NSNumber*) accountID
|
|
{
|
|
xmpp* account = [self getEnabledAccountForID:accountID];
|
|
if(account.accountState>=kStateBound) return YES;
|
|
return NO;
|
|
}
|
|
|
|
-(NSDate *) connectedTimeFor:(NSNumber*) accountID
|
|
{
|
|
xmpp* account = [self getEnabledAccountForID:accountID];
|
|
return account.connectedTime;
|
|
}
|
|
|
|
-(xmpp* _Nullable) getEnabledAccountForID:(NSNumber*) accountID
|
|
{
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
{
|
|
//using stringWithFormat: makes sure this REALLY is a string
|
|
if(xmppAccount.accountID.intValue == accountID.intValue)
|
|
return xmppAccount;
|
|
}
|
|
return nil;
|
|
}
|
|
|
|
-(void) connectAccount:(NSNumber*) accountID
|
|
{
|
|
NSDictionary* account = [[DataLayer sharedInstance] detailsForAccount:accountID];
|
|
if(!account)
|
|
DDLogError(@"Expected account settings in db for accountID: %@", accountID);
|
|
else
|
|
[self connectAccountWithDictionary:account];
|
|
}
|
|
|
|
-(void) connectAccountWithDictionary:(NSDictionary*) account
|
|
{
|
|
xmpp* existing = [self getEnabledAccountForID:[account objectForKey:kAccountID]];
|
|
if(existing)
|
|
{
|
|
if(![account[@"enabled"] boolValue])
|
|
{
|
|
DDLogInfo(@"existing but disabled account, ignoring");
|
|
return;
|
|
}
|
|
if(_isConnectBlocked)
|
|
{
|
|
DDLogWarn(@"connect blocked, ignoring");
|
|
return;
|
|
}
|
|
DDLogInfo(@"existing account, calling unfreeze");
|
|
[existing unfreeze];
|
|
DDLogInfo(@"existing account, just pinging.");
|
|
[existing sendPing:SHORT_PING]; //short ping timeout to quickly check if connectivity is still okay
|
|
return;
|
|
}
|
|
DDLogVerbose(@"connecting account %@@%@",[account objectForKey:kUsername], [account objectForKey:kDomain]);
|
|
|
|
NSError* error;
|
|
NSString* jid = [NSString stringWithFormat:@"%@@%@", account[kUsername], account[kDomain]];
|
|
NSString* password = [SAMKeychain passwordForService:kMonalKeychainName account:((NSNumber*)account[kAccountID]).stringValue error:&error];
|
|
if(error)
|
|
{
|
|
DDLogError(@"Keychain error: %@", error);
|
|
|
|
// Disable account because login will not be possible
|
|
[[DataLayer sharedInstance] disableAccountForPasswordMigration:account[kAccountID]];
|
|
[self disconnectAccount:account[kAccountID] withExplicitLogout:YES];
|
|
|
|
//show notifications for disabled accounts to warn user if in appex
|
|
if([HelperTools isAppExtension])
|
|
{
|
|
UNMutableNotificationContent* content = [UNMutableNotificationContent new];
|
|
content.title = NSLocalizedString(@"Account disabled", @"");;
|
|
content.subtitle = jid;
|
|
content.body = NSLocalizedString(@"You restored an iCloud backup of Monal, please open the app to reenable this account.", @"");
|
|
content.sound = [UNNotificationSound defaultSound];
|
|
content.categoryIdentifier = @"simple";
|
|
UNNotificationRequest* request = [UNNotificationRequest requestWithIdentifier:[NSString stringWithFormat:@"disabled::%@", jid] content:content trigger:nil];
|
|
error = [HelperTools postUserNotificationRequest:request];
|
|
if(error)
|
|
DDLogError(@"Error posting account disabled notification: %@", error);
|
|
}
|
|
|
|
return;
|
|
}
|
|
MLXMPPIdentity* identity = [[MLXMPPIdentity alloc] initWithJid:jid password:password andResource:[account objectForKey:kResource]];
|
|
MLXMPPServer* server = [[MLXMPPServer alloc] initWithHost:[account objectForKey:kServer] andPort:[account objectForKey:kPort] andDirectTLS:[[account objectForKey:kDirectTLS] boolValue]];
|
|
xmpp* xmppAccount = [[xmpp alloc] initWithServer:server andIdentity:identity andAccountID:[account objectForKey:kAccountID]];
|
|
xmppAccount.statusMessage = [account objectForKey:@"statusMessage"];
|
|
|
|
@synchronized(_connectedXMPP) {
|
|
[_connectedXMPP addObject:xmppAccount];
|
|
}
|
|
|
|
if(![account[@"enabled"] boolValue])
|
|
{
|
|
DDLogInfo(@"existing but disabled account, not connecting");
|
|
return;
|
|
}
|
|
if(!self.isConnectBlocked)
|
|
{
|
|
DDLogInfo(@"starting connect");
|
|
[xmppAccount connect];
|
|
}
|
|
else
|
|
DDLogWarn(@"connect blocked, not connecting newly created xmpp* instance");
|
|
}
|
|
|
|
-(void) disconnectAccount:(NSNumber*) accountID withExplicitLogout:(BOOL) explicitLogout
|
|
{
|
|
int index = 0;
|
|
int pos = -1;
|
|
xmpp* account;
|
|
@synchronized(_connectedXMPP) {
|
|
for(xmpp* xmppAccount in _connectedXMPP)
|
|
{
|
|
if(xmppAccount.accountID.intValue == accountID.intValue)
|
|
{
|
|
account = xmppAccount;
|
|
pos=index;
|
|
break;
|
|
}
|
|
index++;
|
|
}
|
|
|
|
if((pos >= 0) && (pos < (int)[_connectedXMPP count]))
|
|
{
|
|
[_connectedXMPP removeObjectAtIndex:pos];
|
|
DDLogVerbose(@"removed account at pos %d", pos);
|
|
}
|
|
}
|
|
if(account)
|
|
{
|
|
DDLogVerbose(@"got account and cleaning up.. ");
|
|
[account disconnect:explicitLogout];
|
|
account = nil;
|
|
DDLogVerbose(@"done cleaning up account ");
|
|
}
|
|
}
|
|
|
|
|
|
-(void) reconnectAll
|
|
{
|
|
NSArray* allAccounts = [[DataLayer sharedInstance] accountList]; //this will also "disconnect" disabled account, just to make sure
|
|
for(NSDictionary* account in allAccounts)
|
|
{
|
|
DDLogVerbose(@"Forcefully disconnecting account %@ (%@@%@)", [account objectForKey:kAccountID], [account objectForKey:@"username"], [account objectForKey:@"domain"]);
|
|
xmpp* xmppAccount = [self getEnabledAccountForID:[account objectForKey:kAccountID]];
|
|
if(xmppAccount != nil)
|
|
[xmppAccount disconnect:YES];
|
|
}
|
|
createTimer(2.0, (^{
|
|
[self connectIfNecessary];
|
|
}));
|
|
}
|
|
|
|
-(void) disconnectAll
|
|
{
|
|
DDLogVerbose(@"manager disconnecAll");
|
|
dispatch_queue_t queue = dispatch_queue_create("im.monal.disconnect", DISPATCH_QUEUE_CONCURRENT);
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
{
|
|
//disconnect to prevent endless loops trying to connect
|
|
dispatch_async(queue, ^{
|
|
DDLogVerbose(@"manager disconnecting: %@", xmppAccount.accountID);
|
|
[xmppAccount disconnect];
|
|
DDLogVerbose(@"manager disconnected: %@", xmppAccount.accountID);
|
|
});
|
|
}
|
|
dispatch_barrier_sync(queue, ^{
|
|
DDLogVerbose(@"manager disconnecAll done (inside barrier)");
|
|
});
|
|
DDLogVerbose(@"manager disconnecAll done");
|
|
}
|
|
|
|
-(void) connectIfNecessary
|
|
{
|
|
DDLogVerbose(@"manager connectIfNecessary");
|
|
NSArray* enabledAccountList = [[DataLayer sharedInstance] enabledAccountList];
|
|
for(NSDictionary* account in enabledAccountList)
|
|
[self connectAccountWithDictionary:account];
|
|
DDLogVerbose(@"manager connectIfNecessary done");
|
|
}
|
|
|
|
-(void) updatePassword:(NSString*) password forAccount:(NSNumber*) accountID
|
|
{
|
|
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
|
|
[SAMKeychain setPassword:password forService:kMonalKeychainName account:accountID.stringValue];
|
|
xmpp* xmpp = [self getEnabledAccountForID:accountID];
|
|
[xmpp.connectionProperties.identity updatPassword:password];
|
|
}
|
|
|
|
-(BOOL) isValidPassword:(NSString*) password forAccount:(NSNumber*) accountID
|
|
{
|
|
return [password isEqualToString:[SAMKeychain passwordForService:kMonalKeychainName account:accountID.stringValue]];
|
|
}
|
|
|
|
//this is only used by quicksy
|
|
-(NSString*) getPasswordForAccount:(NSNumber*) accountID
|
|
{
|
|
return [SAMKeychain passwordForService:kMonalKeychainName account:accountID.stringValue];
|
|
}
|
|
|
|
#pragma mark - XMPP commands
|
|
-(void) sendMessageAndAddToHistory:(NSString*) message havingType:(NSString*) messageType toContact:(MLContact*) contact isEncrypted:(BOOL) encrypted uploadInfo:(NSDictionary* _Nullable) uploadInfo withCompletionHandler:(void (^ _Nullable)(BOOL success, NSString* messageId)) completion
|
|
{
|
|
NSString* msgid = [[NSUUID UUID] UUIDString];
|
|
xmpp* account = contact.account;
|
|
|
|
MLAssert(message != nil, @"Message should not be nil");
|
|
MLAssert(account != nil, @"Account should not be nil");
|
|
MLAssert(contact != nil, @"Contact should not be nil");
|
|
MLAssert(uploadInfo == nil || messageType == kMessageTypeFiletransfer, @"You must use message type = filetransfer if you supply an uploadInfo!");
|
|
|
|
// Save message to history
|
|
NSNumber* messageDBId = [[DataLayer sharedInstance]
|
|
addMessageHistoryTo:contact.contactJid
|
|
forAccount:contact.accountID
|
|
withMessage:message
|
|
actuallyFrom:(contact.isMuc ? contact.accountNickInGroup : account.connectionProperties.identity.jid)
|
|
withId:msgid
|
|
encrypted:encrypted
|
|
messageType:messageType
|
|
mimeType:uploadInfo[@"mimeType"]
|
|
size:uploadInfo[@"size"]
|
|
];
|
|
// Send message
|
|
if(messageDBId != nil)
|
|
{
|
|
DDLogInfo(@"Message added to history with id %ld, now sending...", (long)[messageDBId intValue]);
|
|
[self sendMessage:message toContact:contact isEncrypted:encrypted isUpload:(uploadInfo != nil) messageId:msgid withCompletionHandler:^(BOOL successSend, NSString* messageIdSend) {
|
|
completion(successSend, messageIdSend);
|
|
}];
|
|
DDLogVerbose(@"Notifying active chats of change for contact %@", contact);
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMLMessageSentToContact object:self userInfo:@{@"contact":contact}];
|
|
|
|
//create and donate interaction to allow for share suggestions
|
|
[[MLNotificationManager sharedInstance] donateInteractionForOutgoingDBId:messageDBId];
|
|
}
|
|
else
|
|
{
|
|
DDLogError(@"Could not add message to history!");
|
|
completion(false, nil);
|
|
}
|
|
}
|
|
|
|
-(void) sendMessage:(NSString*) message toContact:(MLContact*) contact isEncrypted:(BOOL) encrypted isUpload:(BOOL) isUpload messageId:(NSString*) messageId withCompletionHandler:(void (^ _Nullable)(BOOL success, NSString* messageId)) completion
|
|
{
|
|
BOOL success = NO;
|
|
xmpp* account = contact.account;
|
|
if(account)
|
|
{
|
|
success = YES;
|
|
[account sendMessage:message toContact:contact isEncrypted:encrypted isUpload:isUpload andMessageId:messageId];
|
|
}
|
|
if(completion)
|
|
completion(success, messageId);
|
|
}
|
|
|
|
-(void) sendChatState:(BOOL) isTyping toContact:(MLContact*) contact
|
|
{
|
|
xmpp* account = contact.account;
|
|
if(account)
|
|
[account sendChatState:isTyping toContact:contact];
|
|
}
|
|
|
|
#pragma mark - login/register
|
|
|
|
-(NSNumber*) login:(NSString*) jid password:(NSString*) password
|
|
{
|
|
NSArray* elements = [jid componentsSeparatedByString:@"@"];
|
|
MLAssert([elements count] > 1, @"Got invalid jid", (@{@"jid": nilWrapper(jid), @"elements": elements}));
|
|
NSString* domain = ((NSString*)[elements objectAtIndex:1]).lowercaseString;
|
|
|
|
//we don't want to set kPlainActivated (not even according to our preload list) and default to plain_activated=false,
|
|
//because the error message will warn the user and direct them to the advanced account creation menu to activate PLAIN
|
|
//if they still want to connect to this server
|
|
//only exception: yax.im --> we don't want to suggest a server during account creation that has a scary warning
|
|
//when logging in using another device afterwards
|
|
//TODO: to be removed once yax.im and quicksy.im supports SASL2 and SSDP!!
|
|
//TODO: use preload list and allow PLAIN for all others once enough domains are on this list
|
|
//allow plain for all servers not on preload list, since prosody with SASL2 wasn't even released yet
|
|
BOOL defaultPlainActivated = YES;
|
|
BOOL plainActivated = ([domain isEqualToString:@"yax.im"] || [domain isEqualToString:@"quicksy.im"]) ? YES : defaultPlainActivated;
|
|
|
|
return [self login:jid password:password hardcodedServer:nil hardcodedPort:nil forceDirectTLS:NO allowPlainAuth:plainActivated];
|
|
}
|
|
|
|
-(NSNumber*) login:(NSString*) jid password:(NSString*) password hardcodedServer:(NSString*) hardcodedServer hardcodedPort:(NSString*) hardcodedPort forceDirectTLS:(BOOL) directTLS allowPlainAuth:(BOOL) plainActivated
|
|
{
|
|
//check if it is a JID
|
|
NSArray* elements = [jid componentsSeparatedByString:@"@"];
|
|
MLAssert([elements count] > 1, @"Got invalid jid", (@{@"jid": nilWrapper(jid), @"elements": elements}));
|
|
|
|
NSString* domain;
|
|
NSString* user;
|
|
user = ((NSString*)[elements objectAtIndex:0]).lowercaseString;
|
|
domain = ((NSString*)[elements objectAtIndex:1]).lowercaseString;
|
|
|
|
if([[DataLayer sharedInstance] doesAccountExistUser:user andDomain:domain])
|
|
{
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kXMPPError object:nil userInfo:@{
|
|
@"title": NSLocalizedString(@"Duplicate Account", @""),
|
|
@"description": NSLocalizedString(@"This account already exists on this instance", @"")
|
|
}];
|
|
return nil;
|
|
}
|
|
|
|
NSMutableDictionary* dic = [NSMutableDictionary new];
|
|
[dic setObject:domain forKey:kDomain];
|
|
[dic setObject:user forKey:kUsername];
|
|
[dic setObject:[HelperTools encodeRandomResource] forKey:kResource];
|
|
[dic setObject:@YES forKey:kEnabled];
|
|
if(hardcodedServer != nil)
|
|
[dic setObject:hardcodedServer forKey:kServer];
|
|
if(hardcodedPort != nil)
|
|
[dic setObject:hardcodedPort forKey:kPort];
|
|
[dic setObject:@(directTLS) forKey:kDirectTLS];
|
|
[dic setObject:@(plainActivated) forKey:kPlainActivated];
|
|
|
|
NSNumber* accountID = [[DataLayer sharedInstance] addAccountWithDictionary:dic];
|
|
if(accountID == nil)
|
|
return nil;
|
|
[self addNewAccountToKeychainAndConnectWithPassword:password andAccountID:accountID];
|
|
return accountID;
|
|
}
|
|
|
|
-(void) addNewAccountToKeychainAndConnectWithPassword:(NSString*) password andAccountID:(NSNumber*) accountID
|
|
{
|
|
if(accountID != nil && password != nil)
|
|
{
|
|
[SAMKeychain setAccessibilityType:kSecAttrAccessibleAfterFirstUnlock];
|
|
[SAMKeychain setPassword:password forService:kMonalKeychainName account:accountID.stringValue];
|
|
[self connectAccount:accountID];
|
|
}
|
|
}
|
|
|
|
-(void) removeAccountForAccountID:(NSNumber*) accountID
|
|
{
|
|
[self disconnectAccount:accountID withExplicitLogout:YES];
|
|
[[DataLayer sharedInstance] removeAccount:accountID];
|
|
[SAMKeychain deletePasswordForService:kMonalKeychainName account:accountID.stringValue];
|
|
[HelperTools removeAllShareInteractionsForAccountID:accountID];
|
|
// trigger UI removal
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalRefresh object:nil userInfo:nil];
|
|
}
|
|
|
|
#pragma mark - getting details
|
|
|
|
-(NSString*) getAccountNameForConnectedRow:(NSUInteger) row
|
|
{
|
|
xmpp* account;
|
|
@synchronized(_connectedXMPP) {
|
|
if(row<[_connectedXMPP count] && row>=0)
|
|
account = [_connectedXMPP objectAtIndex:row];
|
|
}
|
|
if(account)
|
|
return account.connectionProperties.identity.jid;
|
|
return @"";
|
|
}
|
|
|
|
#pragma mark - contact
|
|
|
|
//this handler will simply retry the removeContact: call
|
|
$$class_handler(handleRemoveContact, $$ID(MLContact*, contact))
|
|
[[MLXMPPManager sharedInstance] removeContact:contact];
|
|
$$
|
|
-(void) removeContact:(MLContact*) contact
|
|
{
|
|
xmpp* account = contact.account;
|
|
if(account)
|
|
{
|
|
//queue remove contact for execution once bound (e.g. on catchup done)
|
|
if(account.accountState < kStateBound)
|
|
{
|
|
[account addReconnectionHandler:$newHandler(self, handleRemoveContact, $ID(contact))];
|
|
return;
|
|
}
|
|
|
|
if(contact.isMuc)
|
|
[account leaveMuc:contact.contactJid];
|
|
else
|
|
[account removeFromRoster:contact];
|
|
|
|
//remove from DB
|
|
[[DataLayer sharedInstance] removeBuddy:contact.contactJid forAccount:contact.accountID];
|
|
[contact removeShareInteractions];
|
|
|
|
//notify the UI
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRemoved object:account userInfo:@{
|
|
@"contact": [MLContact createContactFromJid:contact.contactJid andAccountID:contact.accountID]
|
|
}];
|
|
}
|
|
}
|
|
|
|
-(void) addContact:(MLContact*) contact
|
|
{
|
|
[self addContact:contact withPreauthToken:nil];
|
|
}
|
|
|
|
//this handler will simply retry the addContact:withPreauthToken: call
|
|
$$class_handler(handleAddContact, $$ID(MLContact*, contact), $_ID(NSString*, preauthToken))
|
|
[[MLXMPPManager sharedInstance] addContact:contact withPreauthToken:preauthToken];
|
|
$$
|
|
-(void) addContact:(MLContact*) contact withPreauthToken:(NSString* _Nullable) preauthToken
|
|
{
|
|
xmpp* account = contact.account;
|
|
if(account)
|
|
{
|
|
//queue add contact for execution once bound (e.g. on catchup done)
|
|
if(account.accountState < kStateBound)
|
|
{
|
|
[account addReconnectionHandler:$newHandler(self, handleAddContact, $ID(contact), $ID(preauthToken))];
|
|
return;
|
|
}
|
|
|
|
if(contact.isMuc)
|
|
[account joinMuc:contact.contactJid];
|
|
else
|
|
{
|
|
[account addToRoster:contact withPreauthToken:preauthToken];
|
|
|
|
#ifndef DISABLE_OMEMO
|
|
// Request omemo devicelist
|
|
[account.omemo subscribeAndFetchDevicelistIfNoSessionExistsForJid:contact.contactJid];
|
|
#endif// DISABLE_OMEMO
|
|
}
|
|
|
|
//notify the UI
|
|
[[MLNotificationQueue currentQueue] postNotificationName:kMonalContactRefresh object:self userInfo:@{
|
|
@"contact": [MLContact createContactFromJid:contact.contactJid andAccountID:contact.accountID]
|
|
}];
|
|
}
|
|
}
|
|
|
|
-(void) getEntitySoftWareVersionForContact:(MLContact*) contact andResource:(NSString*) resource
|
|
{
|
|
xmpp* account = contact.account;
|
|
|
|
NSString* xmppId = @"";
|
|
if ((resource == nil) || ([resource length] == 0)) {
|
|
xmppId = [NSString stringWithFormat:@"%@",contact.contactJid];
|
|
} else {
|
|
xmppId = [NSString stringWithFormat:@"%@/%@",contact.contactJid, resource];
|
|
}
|
|
|
|
[account getEntitySoftWareVersion:xmppId];
|
|
}
|
|
|
|
-(void) block:(BOOL) isBlocked contact:(MLContact*) contact
|
|
{
|
|
DDLogVerbose(@"Blocking %@: %@", contact, bool2str(isBlocked));
|
|
xmpp* account = contact.account;
|
|
[account setBlocked:isBlocked forJid:contact.contactJid];
|
|
}
|
|
|
|
-(void) block:(BOOL) isBlocked fullJid:(NSString*) fullJid onAccount:(NSNumber*) accountID
|
|
{
|
|
DDLogVerbose(@"Blocking %@ on account %@: %@", fullJid, accountID, bool2str(isBlocked));
|
|
xmpp* account = [self getEnabledAccountForID:accountID];
|
|
[account setBlocked:isBlocked forJid:fullJid];
|
|
}
|
|
|
|
#pragma mark message signals
|
|
|
|
-(void) handleSentMessage:(NSNotification*) notification
|
|
{
|
|
XMPPMessage* msg = notification.userInfo[@"message"];
|
|
DDLogInfo(@"message %@, %@ sent, setting status accordingly", msg.id, msg.toUser);
|
|
[[DataLayer sharedInstance] setMessageId:msg.id andJid:msg.toUser sent:YES];
|
|
}
|
|
|
|
#pragma mark - APNS
|
|
|
|
-(void) setPushToken:(NSString* _Nullable) token
|
|
{
|
|
if(token && ![token isEqualToString:_pushToken])
|
|
{
|
|
_pushToken = token;
|
|
[[HelperTools defaultsDB] setObject:_pushToken forKey:@"pushToken"];
|
|
//this will be used by XMPPIQ setPushEnableWithNode and DataLayerMigrations
|
|
//save it when the token changes, to keep token and type in sync
|
|
[[HelperTools defaultsDB] setBool:[HelperTools isSandboxAPNS] forKey:@"isSandboxAPNS"];
|
|
}
|
|
else //use saved one if we are in NSE appex --> we can't get a new token and the old token might still be valid
|
|
_pushToken = [[HelperTools defaultsDB] objectForKey:@"pushToken"];
|
|
|
|
//check node and secret values
|
|
if(
|
|
_pushToken &&
|
|
_pushToken.length
|
|
)
|
|
{
|
|
DDLogInfo(@"push token valid, current push settings: token=%@, isSandboxAPNS=%@", _pushToken, [[HelperTools defaultsDB] boolForKey:@"isSandboxAPNS"] ? @"YES" : @"NO");
|
|
self.hasAPNSToken = YES;
|
|
}
|
|
else
|
|
{
|
|
self.hasAPNSToken = NO;
|
|
DDLogWarn(@"push token invalid, current push settings: token=%@, isSandboxAPNS=%@", _pushToken, [[HelperTools defaultsDB] boolForKey:@"isSandboxAPNS"] ? @"YES" : @"NO");
|
|
}
|
|
|
|
//only try to enable push if we have a node and secret value
|
|
if(self.hasAPNSToken)
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
[xmppAccount enablePush];
|
|
}
|
|
|
|
-(void) removeToken
|
|
{
|
|
DDLogWarn(@"APNS removing push token");
|
|
|
|
[[HelperTools defaultsDB] removeObjectForKey:@"pushToken"];
|
|
self.hasAPNSToken = NO;
|
|
for(xmpp* xmppAccount in [self connectedXMPP])
|
|
[xmppAccount disablePush];
|
|
}
|
|
|
|
@end
|