Initial commit

This commit is contained in:
Tha_14
2024-02-22 21:43:11 +02:00
commit 1b96a031d2
1108 changed files with 157706 additions and 0 deletions

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerBootstrap.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerBootstrapImpl : NSObject <OCTSubmanagerBootstrap, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,255 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerBootstrapImpl.h"
#import "OCTNode.h"
#import "OCTTox.h"
#import "OCTLogging.h"
#import "OCTRealmManager.h"
#import "OCTSettingsStorageObject.h"
static const NSTimeInterval kDidConnectDelay = 2.0; // in seconds
static const NSTimeInterval kIterationTime = 5.0; // in seconds
static const NSUInteger kNodesPerIteration = 20;
@interface OCTSubmanagerBootstrapImpl ()
@property (strong, nonatomic) NSMutableSet *addedNodes;
@property (assign, nonatomic) BOOL isBootstrapping;
@property (strong, nonatomic) NSObject *bootstrappingLock;
@property (assign, nonatomic) NSTimeInterval didConnectDelay;
@property (assign, nonatomic) NSTimeInterval iterationTime;
@end
@implementation OCTSubmanagerBootstrapImpl
@synthesize dataSource = _dataSource;
#pragma mark - Lifecycle
- (instancetype)init
{
self = [super init];
if (! self) {
return nil;
}
_addedNodes = [NSMutableSet new];
_bootstrappingLock = [NSObject new];
_didConnectDelay = kDidConnectDelay;
_iterationTime = kIterationTime;
return self;
}
#pragma mark - Public
- (void)addNodeWithIpv4Host:(nullable NSString *)ipv4Host
ipv6Host:(nullable NSString *)ipv6Host
udpPort:(OCTToxPort)udpPort
tcpPorts:(NSArray<NSNumber *> *)tcpPorts
publicKey:(NSString *)publicKey
{
OCTNode *node = [[OCTNode alloc] initWithIpv4Host:ipv4Host
ipv6Host:ipv6Host
udpPort:udpPort
tcpPorts:tcpPorts
publicKey:publicKey];
@synchronized(self.addedNodes) {
[self.addedNodes addObject:node];
}
}
- (void)addPredefinedNodes
{
NSString *file = [[self objcToxBundle] pathForResource:@"nodes" ofType:@"json"];
NSData *data = [NSData dataWithContentsOfFile:file];
NSDictionary *dictionary = [NSJSONSerialization JSONObjectWithData:data options:0 error:nil];
NSAssert(dictionary, @"Nodes json file is corrupted.");
for (NSDictionary *node in dictionary[@"nodes"]) {
NSUInteger lastPing = [node[@"last_ping"] unsignedIntegerValue];
if (lastPing == 0) {
// Skip nodes that weren't seen online.
continue;
}
NSString *ipv4 = node[@"ipv4"];
NSString *ipv6 = node[@"ipv6"];
OCTToxPort udpPort = [node[@"port"] unsignedShortValue];
NSArray<NSNumber *> *tcpPorts = node[@"tcp_ports"];
NSString *publicKey = node[@"public_key"];
// Check if addresses are valid.
if (ipv4.length <= 2) {
ipv4 = nil;
}
if (ipv6.length <= 2) {
ipv6 = nil;
}
NSAssert(ipv4, @"Nodes json file is corrupted");
NSAssert(udpPort > 0, @"Nodes json file is corrupted");
NSAssert(publicKey, @"Nodes json file is corrupted");
[self addNodeWithIpv4Host:ipv4 ipv6Host:ipv6 udpPort:udpPort tcpPorts:tcpPorts publicKey:publicKey];
}
}
- (void)bootstrap
{
@synchronized(self.bootstrappingLock) {
if (self.isBootstrapping) {
OCTLogWarn(@"bootstrap method called while already bootstrapping");
return;
}
self.isBootstrapping = YES;
}
OCTLogVerbose(@"bootstrapping with %lu nodes", (unsigned long)self.addedNodes.count);
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
if (realmManager.settingsStorage.bootstrapDidConnect) {
OCTLogVerbose(@"did connect before, waiting %g seconds", self.didConnectDelay);
[self tryToBootstrapAfter:self.didConnectDelay];
}
else {
[self tryToBootstrap];
}
}
#pragma mark - Private
- (void)tryToBootstrapAfter:(NSTimeInterval)after
{
__weak OCTSubmanagerBootstrapImpl *weakSelf = self;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, after * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
__strong OCTSubmanagerBootstrapImpl *strongSelf = weakSelf;
if (! strongSelf) {
OCTLogInfo(@"OCTSubmanagerBootstrap is dead, seems that OCTManager was killed, quiting.");
return;
}
[strongSelf tryToBootstrap];
});
}
- (void)tryToBootstrap
{
if ([self.dataSource managerIsToxConnected]) {
OCTLogInfo(@"trying to bootstrap... tox is connected, exiting");
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
[realmManager updateObject:realmManager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
object.bootstrapDidConnect = YES;
}];
[self finishBootstrapping];
return;
}
NSArray *selectedNodes = [self selectedNodesForIteration];
if (! selectedNodes.count) {
OCTLogInfo(@"trying to bootstrap... no nodes left, exiting");
[self finishBootstrapping];
return;
}
OCTLogInfo(@"trying to bootstrap... picked %lu nodes", (unsigned long)selectedNodes.count);
for (OCTNode *node in selectedNodes) {
// HINT: do not do this async on a thread. since "node" will loose its value
// dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
[self safeBootstrapFromHost:node.ipv4Host port:node.udpPort publicKey:node.publicKey];
[self safeBootstrapFromHost:node.ipv6Host port:node.udpPort publicKey:node.publicKey];
for (NSNumber *tcpPort in node.tcpPorts) {
[self safeAddTcpRelayWithHost:node.ipv4Host port:tcpPort.intValue publicKey:node.publicKey];
[self safeAddTcpRelayWithHost:node.ipv6Host port:tcpPort.intValue publicKey:node.publicKey];
}
// });
}
[self tryToBootstrapAfter:self.iterationTime];
}
- (void)safeBootstrapFromHost:(NSString *)host port:(OCTToxPort)port publicKey:(NSString *)publicKey
{
if (! host) {
return;
}
NSError *error;
if (! [[self.dataSource managerGetTox] bootstrapFromHost:host port:port publicKey:publicKey error:&error]) {
OCTLogWarn(@"trying to bootstrap... bootstrap failed with address %@, error %@", host, error);
}
}
- (void)safeAddTcpRelayWithHost:(NSString *)host port:(OCTToxPort)port publicKey:(NSString *)publicKey
{
if (! host) {
return;
}
NSError *error;
if (! [[self.dataSource managerGetTox] addTCPRelayWithHost:host port:port publicKey:publicKey error:&error]) {
OCTLogWarn(@"trying to bootstrap... tcp relay failed with address %@, error %@", host, error);
}
}
- (void)finishBootstrapping
{
@synchronized(self.bootstrappingLock) {
self.isBootstrapping = NO;
}
}
- (NSArray *)selectedNodesForIteration
{
NSMutableArray *allNodes;
NSMutableArray *selectedNodes = [NSMutableArray new];
@synchronized(self.addedNodes) {
allNodes = [[self.addedNodes allObjects] mutableCopy];
}
while (allNodes.count && (selectedNodes.count < kNodesPerIteration)) {
NSUInteger index = arc4random_uniform((u_int32_t)allNodes.count);
[selectedNodes addObject:allNodes[index]];
[allNodes removeObjectAtIndex:index];
}
@synchronized(self.addedNodes) {
[self.addedNodes minusSet:[NSSet setWithArray:selectedNodes]];
}
return [selectedNodes copy];
}
- (NSBundle *)objcToxBundle
{
NSBundle *mainBundle = [NSBundle bundleForClass:[self class]];
NSBundle *objcToxBundle = [NSBundle bundleWithPath:[mainBundle pathForResource:@"objcTox" ofType:@"bundle"]];
// objcToxBundle is used when installed with CocoaPods. If we run tests/demo app mainBundle would be used.
return objcToxBundle ?: mainBundle;
}
@end

View File

@ -0,0 +1,27 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerCalls.h"
#import "OCTSubmanagerProtocol.h"
#import "OCTToxAV.h"
#import "OCTManagerConstants.h"
#import "OCTAudioEngine.h"
#import "OCTVideoEngine.h"
#import "OCTRealmManager.h"
#import "OCTCall+Utilities.h"
#import "OCTCallTimer.h"
@class OCTTox;
@interface OCTSubmanagerCallsImpl : NSObject <OCTSubmanagerCalls, OCTSubmanagerProtocol>
/**
* Initialize the OCTSubmanagerCall
*/
- (instancetype)initWithTox:(OCTTox *)tox;
- (instancetype)init NS_UNAVAILABLE;
+ (instancetype)new NS_UNAVAILABLE;
@end

View File

@ -0,0 +1,564 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerCallsImpl.h"
#import "OCTLogging.h"
#import "OCTTox.h"
const OCTToxAVAudioBitRate kDefaultAudioBitRate = OCTToxAVAudioBitRate48;
const OCTToxAVVideoBitRate kDefaultVideoBitRate = 2500;
@interface OCTSubmanagerCallsImpl () <OCTToxAVDelegate>
@property (strong, nonatomic) OCTToxAV *toxAV;
@property (strong, nonatomic) OCTAudioEngine *audioEngine;
@property (strong, nonatomic) OCTVideoEngine *videoEngine;
@property (strong, nonatomic) OCTCallTimer *timer;
@property (nonatomic, assign) dispatch_once_t setupOnceToken;
@end
@implementation OCTSubmanagerCallsImpl : NSObject
@synthesize delegate = _delegate;
@synthesize dataSource = _dataSource;
#pragma mark - Lifecycle
- (instancetype)initWithTox:(OCTTox *)tox
{
self = [super init];
if (! self) {
return nil;
}
_toxAV = [[OCTToxAV alloc] initWithTox:tox error:nil];
_toxAV.delegate = self;
[_toxAV start];
return self;
}
- (void)dealloc
{
[self endAllCalls];
}
#pragma mark - Public
- (BOOL)setupAndReturnError:(NSError **)error
{
NSAssert(self.dataSource, @"dataSource is needed before setup of OCTSubmanagerCalls");
__block BOOL status = NO;
dispatch_once(&_setupOnceToken, ^{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
self.timer = [[OCTCallTimer alloc] initWithRealmManager:realmManager];
self.audioEngine = [OCTAudioEngine new];
self.audioEngine.toxav = self.toxAV;
self.videoEngine = [OCTVideoEngine new];
self.videoEngine.toxav = self.toxAV;
status = [self.videoEngine setupAndReturnError:error];
});
return status;
}
- (OCTCall *)callToChat:(OCTChat *)chat enableAudio:(BOOL)enableAudio enableVideo:(BOOL)enableVideo error:(NSError **)error
{
OCTToxAVAudioBitRate audioBitRate = (enableAudio) ? kDefaultAudioBitRate : OCTToxAVAudioBitRateDisabled;
OCTToxAVVideoBitRate videoBitRate = (enableVideo) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
if (chat.friends.count == 1) {
OCTFriend *friend = chat.friends.lastObject;
self.audioEngine.friendNumber = friend.friendNumber;
if (! [self.toxAV callFriendNumber:friend.friendNumber
audioBitRate:audioBitRate
videoBitRate:videoBitRate
error:error]) {
return nil;
}
[self checkForCurrentActiveCallAndPause];
OCTCall *call = [self createCallWithFriend:friend status:OCTCallStatusDialing];
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
callToUpdate.status = OCTCallStatusDialing;
callToUpdate.videoIsEnabled = enableVideo;
}];
self.enableMicrophone = YES;
return call;
}
else {
// TO DO: Group Calls
return nil;
}
return nil;
}
- (BOOL)enableVideoSending:(BOOL)enable forCall:(OCTCall *)call error:(NSError **)error
{
OCTToxAVVideoBitRate bitrate = (enable) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
if (! [self setVideoBitrate:bitrate forCall:call error:error]) {
return NO;
}
if (enable && (! [call isPaused])) {
OCTFriend *friend = [call.chat.friends firstObject];
self.videoEngine.friendNumber = friend.friendNumber;
[self.videoEngine startSendingVideo];
}
else {
[self.videoEngine stopSendingVideo];
}
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
callToUpdate.videoIsEnabled = enable;
}];
return YES;
}
- (BOOL)answerCall:(OCTCall *)call enableAudio:(BOOL)enableAudio enableVideo:(BOOL)enableVideo error:(NSError **)error
{
OCTToxAVAudioBitRate audioBitRate = (enableAudio) ? kDefaultAudioBitRate : OCTToxAVAudioBitRateDisabled;
OCTToxAVVideoBitRate videoBitRate = (enableVideo) ? kDefaultVideoBitRate : kOCTToxAVVideoBitRateDisable;
if (call.chat.friends.count == 1) {
OCTFriend *friend = call.chat.friends.firstObject;
if (! [self.toxAV answerIncomingCallFromFriend:friend.friendNumber
audioBitRate:audioBitRate
videoBitRate:videoBitRate
error:error]) {
return NO;
}
[self checkForCurrentActiveCallAndPause];
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:call withBlock:^(OCTCall *callToUpdate) {
call.status = OCTCallStatusActive;
callToUpdate.videoIsEnabled = enableVideo;
}];
self.enableMicrophone = YES;
[self startEnginesAndTimer:YES forCall:call];
return YES;
}
else {
// TO DO: Group Calls
return NO;
}
}
- (BOOL)enableMicrophone
{
return self.audioEngine.enableMicrophone;
}
- (void)setEnableMicrophone:(BOOL)enableMicrophone
{
self.audioEngine.enableMicrophone = enableMicrophone;
}
- (BOOL)sendCallControl:(OCTToxAVCallControl)control toCall:(OCTCall *)call error:(NSError **)error
{
if (call.chat.friends.count == 1) {
OCTFriend *friend = call.chat.friends.firstObject;
if (! [self.toxAV sendCallControl:control toFriendNumber:friend.friendNumber error:error]) {
return NO;
}
switch (control) {
case OCTToxAVCallControlResume:
[self checkForCurrentActiveCallAndPause];
[self putOnPause:NO call:call];
break;
case OCTToxAVCallControlCancel:
[self addMessageAndDeleteCall:call];
if ((self.audioEngine.friendNumber == friend.friendNumber) &&
([self.audioEngine isAudioRunning:nil])) {
[self startEnginesAndTimer:NO forCall:call];
}
break;
case OCTToxAVCallControlPause:
[self putOnPause:YES call:call];
break;
case OCTToxAVCallControlUnmuteAudio:
break;
case OCTToxAVCallControlMuteAudio:
break;
case OCTToxAVCallControlHideVideo:
break;
case OCTToxAVCallControlShowVideo:
break;
}
return YES;
}
else {
return NO;
}
}
- (OCTView *)videoFeed
{
return [self.videoEngine videoFeed];
}
- (void)getVideoCallPreview:(void (^)(CALayer *))completionBlock
{
[self.videoEngine getVideoCallPreview:completionBlock];
}
- (BOOL)setAudioBitrate:(int)bitrate forCall:(OCTCall *)call error:(NSError **)error
{
if (call.chat.friends.count == 1) {
OCTFriend *friend = call.chat.friends.firstObject;
return [self.toxAV setAudioBitRate:bitrate force:NO forFriend:friend.friendNumber error:error];
}
else {
// TO DO: Group Calls
return NO;
}
}
#pragma mark - Setting IO devices
#if ! TARGET_OS_IPHONE
- (BOOL)setAudioInputDevice:(NSString *)deviceUniqueID error:(NSError **)error
{
return [self.audioEngine setInputDeviceID:deviceUniqueID error:error];
}
- (BOOL)setAudioOutputDevice:(NSString *)deviceUniqueID error:(NSError **)error
{
return [self.audioEngine setOutputDeviceID:deviceUniqueID error:error];
}
- (BOOL)setVideoInputDevice:(NSString *)deviceUniqueID error:(NSError **)error
{
return [self.videoEngine switchToCamera:deviceUniqueID error:error];
}
#else
- (BOOL)routeAudioToSpeaker:(BOOL)speaker error:(NSError **)error
{
return [self.audioEngine routeAudioToSpeaker:speaker error:error];
}
- (BOOL)switchToCameraFront:(BOOL)front error:(NSError **)error
{
return [self.videoEngine useFrontCamera:front error:error];
}
#endif
#pragma mark Private methods
- (OCTCall *)createCallWithFriend:(OCTFriend *)friend status:(OCTCallStatus)status
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
return [realmManager createCallWithChat:chat status:status];
}
- (OCTCall *)getCurrentCallForFriendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
return [realmManager getCurrentCallForChat:chat];
}
- (void)updateCall:(OCTCall *)call withStatus:(OCTCallStatus)status
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
callToUpdate.status = status;
}];
}
- (void)endAllCalls
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
[self startEnginesAndTimer:NO forCall:nil];
RLMResults *calls = [realmManager objectsWithClass:[OCTCall class] predicate:nil];
for (OCTCall *call in calls) {
OCTFriend *friend = call.chat.friends.firstObject;
[self.toxAV sendCallControl:OCTToxAVCallControlCancel toFriendNumber:friend.friendNumber error:nil];
}
[realmManager convertAllCallsToMessages];
}
- (void)putOnPause:(BOOL)pause call:(OCTCall *)call
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
BOOL wasPaused = call.pausedStatus != OCTCallPausedStatusNone;
if (pause) {
if (! wasPaused) {
[self startEnginesAndTimer:NO forCall:call];
}
}
else {
OCTFriend *friend = [call.chat.friends firstObject];
self.audioEngine.friendNumber = friend.friendNumber;
if (call.pausedStatus == OCTCallPausedStatusByUser) {
[self startEnginesAndTimer:YES forCall:call];
}
}
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
if (pause) {
callToUpdate.pausedStatus |= OCTCallPausedStatusByUser;
callToUpdate.onHoldStartInterval = callToUpdate.onHoldStartInterval ?: [[NSDate date] timeIntervalSince1970];
}
else {
callToUpdate.pausedStatus &= ~OCTCallPausedStatusByUser;
callToUpdate.onHoldStartInterval = 0;
}
}];
}
- (void)addMessageAndDeleteCall:(OCTCall *)call
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
[realmManager addMessageCall:call];
if (! [call isPaused]) {
[self.timer stopTimer];
}
// TODO: this one crashes when video was active, and is called directly
// there is a race condition somewhere -> fix me!
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC), dispatch_get_main_queue(), ^{
[realmManager deleteObject:call];
});
}
- (void)updateCall:(OCTCall *)call withState:(OCTToxAVCallState)state pausedStatus:(OCTCallPausedStatus)pausedStatus
{
BOOL sendingAudio = NO, sendingVideo = NO, acceptingAudio = NO, acceptingVideo = NO;
if (state & OCTToxAVFriendCallStateAcceptingAudio) {
acceptingAudio = YES;
}
if (state & OCTToxAVFriendCallStateAcceptingVideo) {
acceptingVideo = YES;
}
if (state & OCTToxAVFriendCallStateSendingAudio) {
sendingAudio = YES;
}
if (state & OCTToxAVFriendCallStateSendingVideo) {
sendingVideo = YES;
}
BOOL wasPaused = call.pausedStatus != OCTCallPausedStatusNone;
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
callToUpdate.friendAcceptingAudio = acceptingAudio;
callToUpdate.friendAcceptingVideo = acceptingVideo;
callToUpdate.friendSendingAudio = sendingAudio;
callToUpdate.friendSendingVideo = sendingVideo;
callToUpdate.pausedStatus = pausedStatus;
if (! wasPaused && (state == OCTToxAVFriendCallStatePaused)) {
callToUpdate.onHoldStartInterval = [[NSDate date] timeIntervalSince1970];
}
}];
}
- (void)checkForCurrentActiveCallAndPause
{
if ([self.audioEngine isAudioRunning:nil] || [self.videoEngine isSendingVideo]) {
OCTCall *call = [self getCurrentCallForFriendNumber:self.audioEngine.friendNumber];
[self sendCallControl:OCTToxAVCallControlPause toCall:call error:nil];
}
}
- (BOOL)setVideoBitrate:(int)bitrate forCall:(OCTCall *)call error:(NSError **)error
{
if (call.chat.friends.count == 1) {
OCTFriend *friend = call.chat.friends.firstObject;
return [self.toxAV setVideoBitRate:bitrate force:NO forFriend:friend.friendNumber error:error];
}
else {
// TO DO: Group Calls
return NO;
}
}
- (void)startEnginesAndTimer:(BOOL)start forCall:(OCTCall *)call
{
if (start) {
OCTFriend *friend = [call.chat.friends firstObject];
NSError *error;
if (! [self.audioEngine startAudioFlow:&error]) {
OCTLogVerbose(@"Error starting audio flow %@", error);
}
if (call.videoIsEnabled) {
[self.videoEngine startSendingVideo];
}
self.audioEngine.friendNumber = friend.friendNumber;
self.videoEngine.friendNumber = friend.friendNumber;
[self.timer startTimerForCall:call];
}
else {
[self.audioEngine stopAudioFlow:nil];
[self.videoEngine stopSendingVideo];
[self.timer stopTimer];
}
}
#pragma mark OCTToxAV delegate methods
- (void)toxAV:(OCTToxAV *)toxAV receiveCallAudioEnabled:(BOOL)audio videoEnabled:(BOOL)video friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
OCTCall *call = [self createCallWithFriend:friend status:OCTCallStatusRinging];
[realmManager updateObject:call withBlock:^(OCTCall *callToUpdate) {
callToUpdate.status = OCTCallStatusRinging;
callToUpdate.caller = friend;
callToUpdate.friendSendingAudio = audio;
callToUpdate.friendAcceptingAudio = audio;
callToUpdate.friendSendingVideo = video;
callToUpdate.friendAcceptingVideo = video;
}];
if ([self.delegate respondsToSelector:@selector(callSubmanager:receiveCall:audioEnabled:videoEnabled:)]) {
[self.delegate callSubmanager:self receiveCall:call audioEnabled:audio videoEnabled:video];
}
}
- (void)toxAV:(OCTToxAV *)toxAV callStateChanged:(OCTToxAVCallState)state friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTCall *call = [self getCurrentCallForFriendNumber:friendNumber];
if ((state & OCTToxAVFriendCallStateFinished) || (state & OCTToxAVFriendCallStateError)) {
[self addMessageAndDeleteCall:call];
if ((self.audioEngine.friendNumber == friendNumber) && [self.audioEngine isAudioRunning:nil]) {
[self.audioEngine stopAudioFlow:nil];
}
if ((self.videoEngine.friendNumber == friendNumber) && [self.videoEngine isSendingVideo]) {
[self.videoEngine stopSendingVideo];
}
return;
}
if (call.status == OCTCallStatusDialing) {
[self updateCall:call withStatus:OCTCallStatusActive];
[self startEnginesAndTimer:YES forCall:call];
}
OCTCallPausedStatus pauseStatus = call.pausedStatus;
if ((pauseStatus == OCTCallPausedStatusNone) && (state == OCTToxAVFriendCallStatePaused)) {
[self startEnginesAndTimer:NO forCall:call];
}
if ((pauseStatus == OCTCallPausedStatusByFriend) && (state != OCTToxAVFriendCallStatePaused)) {
[self startEnginesAndTimer:YES forCall:call];
}
if (state == OCTToxAVFriendCallStatePaused) {
pauseStatus |= OCTCallPausedStatusByFriend;
}
else {
pauseStatus &= ~OCTCallPausedStatusByFriend;
}
[self updateCall:call withState:state pausedStatus:pauseStatus];
}
- (void) toxAV:(OCTToxAV *)toxAV
receiveAudio:(OCTToxAVPCMData *)pcm
sampleCount:(OCTToxAVSampleCount)sampleCount
channels:(OCTToxAVChannels)channels
sampleRate:(OCTToxAVSampleRate)sampleRate
friendNumber:(OCTToxFriendNumber)friendNumber
{
// TOXAUDIO: -incoming-audio-
[self.audioEngine provideAudioFrames:pcm sampleCount:sampleCount channels:channels sampleRate:sampleRate fromFriend:friendNumber];
}
- (void)toxAV:(OCTToxAV *)toxAV audioBitRateStatus:(OCTToxAVAudioBitRate)audioBitRate forFriendNumber:(OCTToxFriendNumber)friendNumber
{
// TODO https://github.com/Antidote-for-Tox/objcTox/issues/88
}
- (void)toxAV:(OCTToxAV *)toxAV videoBitRateStatus:(OCTToxAVVideoBitRate)audioBitRate forFriendNumber:(OCTToxFriendNumber)friendNumber
{
// TODO https://github.com/Antidote-for-Tox/objcTox/issues/88
}
- (void) toxAV:(OCTToxAV *)toxAV
receiveVideoFrameWithWidth:(OCTToxAVVideoWidth)width height:(OCTToxAVVideoHeight)height
yPlane:(OCTToxAVPlaneData *)yPlane uPlane:(OCTToxAVPlaneData *)uPlane
vPlane:(OCTToxAVPlaneData *)vPlane
yStride:(OCTToxAVStrideData)yStride uStride:(OCTToxAVStrideData)uStride
vStride:(OCTToxAVStrideData)vStride
friendNumber:(OCTToxFriendNumber)friendNumber
{
[self.videoEngine receiveVideoFrameWithWidth:width
height:height
yPlane:yPlane
uPlane:uPlane
vPlane:vPlane
yStride:yStride
uStride:uStride
vStride:vStride
friendNumber:friendNumber];
}
@end

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerChats.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerChatsImpl : NSObject <OCTSubmanagerChats, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,529 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerChatsImpl.h"
#import "OCTTox.h"
#import "OCTRealmManager.h"
#import "OCTMessageAbstract.h"
#import "OCTMessageText.h"
#import "OCTChat.h"
#import "OCTLogging.h"
#import "OCTSendMessageOperation.h"
#import "OCTTox+Private.h"
#import "OCTToxOptions+Private.h"
#import "Firebase.h"
@interface OCTSubmanagerChatsImpl ()
@property (strong, nonatomic, readonly) NSOperationQueue *sendMessageQueue;
@end
@implementation OCTSubmanagerChatsImpl
@synthesize dataSource = _dataSource;
- (instancetype)init
{
self = [super init];
if (! self) {
return nil;
}
_sendMessageQueue = [NSOperationQueue new];
_sendMessageQueue.maxConcurrentOperationCount = 1;
return self;
}
- (void)dealloc
{
[self.dataSource.managerGetNotificationCenter removeObserver:self];
}
- (void)configure
{
[self.dataSource.managerGetNotificationCenter addObserver:self
selector:@selector(friendConnectionStatusChangeNotification:)
name:kOCTFriendConnectionStatusChangeNotification
object:nil];
}
#pragma mark - Public
- (OCTChat *)getOrCreateChatWithFriend:(OCTFriend *)friend
{
return [[self.dataSource managerGetRealmManager] getOrCreateChatWithFriend:friend];
}
- (void)removeMessages:(NSArray<OCTMessageAbstract *> *)messages
{
[[self.dataSource managerGetRealmManager] removeMessages:messages];
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTScheduleFileTransferCleanupNotification object:nil];
}
- (void)removeAllMessagesInChat:(OCTChat *)chat removeChat:(BOOL)removeChat
{
[[self.dataSource managerGetRealmManager] removeAllMessagesInChat:chat removeChat:removeChat];
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTScheduleFileTransferCleanupNotification object:nil];
}
- (void)sendOwnPush
{
NSLog(@"PUSH:sendOwnPush");
NSString *token = [FIRMessaging messaging].FCMToken;
if (token.length > 0)
{
NSString *my_pushToken = [NSString stringWithFormat:@"https://tox.zoff.xyz/toxfcm/fcm.php?id=%@&type=1", token];
// NSLog(@"token push url=%@", my_pushToken);
triggerPush(my_pushToken, nil, nil, nil);
}
else
{
NSLog(@"PUSH:sendOwnPush:no token");
}
}
- (void)sendMessagePushToChat:(OCTChat *)chat
{
NSParameterAssert(chat);
NSLog(@"PUSH:sendMessagePushToChat");
__weak OCTSubmanagerChatsImpl *weakSelf = self;
dispatch_async(dispatch_get_main_queue(), ^{
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
OCTFriend *friend = [chat.friends firstObject];
__block NSString *friend_pushToken = friend.pushToken;
if (friend_pushToken == nil)
{
NSLog(@"sendMessagePushToChat:Friend has No Pushtoken");
}
else
{
// HINT: only select outgoing messages (senderUniqueIdentifier == NULL)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.isDelivered == 0 AND messageText.sentPush == 0 AND senderUniqueIdentifier == nil", chat.uniqueIdentifier];
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
OCTMessageAbstract *message_found = [results firstObject];
if (message_found) {
[realmManager updateObject:message_found withBlock:^(OCTMessageAbstract *theMessage) {
theMessage.messageText.sentPush = YES;
}];
triggerPush(friend_pushToken, message_found.messageText.msgv3HashHex, strongSelf, chat);
}
}
});
}
triggerPush(NSString *used_pushToken,
NSString *msgv3HashHex,
OCTSubmanagerChatsImpl *strongSelf,
OCTChat *chat)
{
// HINT: call push token (URL) here
// best in a background thread
//
NSLog(@"PUSH:triggerPush");
if ((used_pushToken != nil) && (used_pushToken.length > 5)) {
// check push url starts with allowed values
if (
([used_pushToken hasPrefix:@"https://tox.zoff.xyz/toxfcm/fcm.php?id="])
||
([used_pushToken hasPrefix:@"https://gotify1.unifiedpush.org/UP?token="])
||
([used_pushToken hasPrefix:@"https://ntfy.sh/"])
) {
NSString *strong_pushToken = used_pushToken;
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
// NSString *strong_pushToken = weak_pushToken;
int PUSH_URL_TRIGGER_AGAIN_MAX_COUNT = 8;
int PUSH_URL_TRIGGER_AGAIN_SECONDS = 21;
for (int i=0; i<(PUSH_URL_TRIGGER_AGAIN_MAX_COUNT + 1); i++)
{
if (chat == nil) {
__block UIApplicationState as = UIApplicationStateBackground;
dispatch_sync(dispatch_get_main_queue(), ^{
as =[[UIApplication sharedApplication] applicationState];
});
if (as == UIApplicationStateActive) {
NSLog(@"PUSH:fg->break:1");
break;
}
}
NSMutableURLRequest *urlRequest = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:strong_pushToken]];
NSString *userUpdate = [NSString stringWithFormat:@"&text=1", nil];
[urlRequest setHTTPMethod:@"POST"];
NSData *data1 = [userUpdate dataUsingEncoding:NSUTF8StringEncoding];
[urlRequest setHTTPBody:data1];
[urlRequest setCachePolicy:NSURLRequestReloadIgnoringCacheData];
[urlRequest setTimeoutInterval:10]; // HINT: 10 seconds
NSString *userAgent = @"Mozilla/5.0 (Windows NT 6.1; rv:60.0) Gecko/20100101 Firefox/60.0";
[urlRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"];
[urlRequest setValue:@"no-cache, no-store, must-revalidate" forHTTPHeaderField:@"Cache-Control"];
[urlRequest setValue:@"no-cache" forHTTPHeaderField:@"Pragma"];
[urlRequest setValue:@"0" forHTTPHeaderField:@"Expires"];
// NSLog(@"PUSH:for msgv3HashHex=%@", msgv3HashHex);
// NSLog(@"PUSH:for friend.pushToken=%@", strong_pushToken);
NSURLSession *session = [NSURLSession sharedSession];
NSURLSessionDataTask *dataTask = [session dataTaskWithRequest:urlRequest completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
if ((httpResponse.statusCode < 300) && (httpResponse.statusCode > 199)) {
NSLog(@"calling PUSH URL:CALL:SUCCESS");
}
else {
NSLog(@"calling PUSH URL:-ERROR:01-");
}
}];
NSLog(@"calling PUSH URL:CALL:start");
[dataTask resume];
if (i < PUSH_URL_TRIGGER_AGAIN_MAX_COUNT)
{
NSLog(@"calling PUSH URL:WAIT:start");
[NSThread sleepForTimeInterval:PUSH_URL_TRIGGER_AGAIN_SECONDS];
NSLog(@"calling PUSH URL:WAIT:done");
}
if (chat == nil) {
__block UIApplicationState as = UIApplicationStateBackground;
dispatch_sync(dispatch_get_main_queue(), ^{
as =[[UIApplication sharedApplication] applicationState];
});
if (as == UIApplicationStateActive) {
NSLog(@"PUSH:fg->break:2");
break;
}
}
if (msgv3HashHex != nil)
{
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
__block BOOL msgIsDelivered = NO;
NSLog(@"calling PUSH URL:DB check:start");
dispatch_sync(dispatch_get_main_queue(), ^{
// HINT: only select outgoing messages (senderUniqueIdentifier == NULL)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier == nil",
chat.uniqueIdentifier, msgv3HashHex];
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
OCTMessageAbstract *message_found = [results firstObject];
if (message_found) {
if (message_found.messageText) {
msgIsDelivered = message_found.messageText.isDelivered;
}
}
NSLog(@"calling PUSH URL:DB check:end_real");
});
NSLog(@"calling PUSH URL:DB check:end");
if (msgIsDelivered == YES) {
// OCTLogInfo(@"PUSH:for msgv3HashHex isDelivered=YES");
NSLog(@"PUSH:for msgv3HashHex isDelivered=YES");
break;
}
}
}
});
}
else {
NSLog(@"unsafe PUSH URL not allowed:-ERROR:02-");
}
}
}
- (void)sendMessageToChat:(OCTChat *)chat
text:(NSString *)text
type:(OCTToxMessageType)type
successBlock:(void (^)(OCTMessageAbstract *message))userSuccessBlock
failureBlock:(void (^)(NSError *error))userFailureBlock
{
NSParameterAssert(chat);
NSParameterAssert(text);
OCTFriend *friend = [chat.friends firstObject];
uint8_t *message_v3_hash_bin = calloc(1, TOX_MSGV3_MSGID_LENGTH);
uint8_t *message_v3_hash_hexstr = calloc(1, (TOX_MSGV3_MSGID_LENGTH * 2) + 1);
NSString *msgv3HashHex = nil;
UInt32 msgv3tssec = 0;
if ((message_v3_hash_bin) && (message_v3_hash_hexstr))
{
tox_messagev3_get_new_message_id(message_v3_hash_bin);
bin_to_hex((const char *)message_v3_hash_bin, (size_t)TOX_MSGV3_MSGID_LENGTH, message_v3_hash_hexstr);
msgv3HashHex = [[NSString alloc] initWithBytes:message_v3_hash_hexstr length:(TOX_MSGV3_MSGID_LENGTH * 2) encoding:NSUTF8StringEncoding];
// HINT: set sent timestamp to now() as unixtimestamp value
msgv3tssec = [[NSNumber numberWithDouble: [[NSDate date] timeIntervalSince1970]] integerValue];
free(message_v3_hash_bin);
free(message_v3_hash_hexstr);
}
__weak OCTSubmanagerChatsImpl *weakSelf = self;
OCTSendMessageOperationSuccessBlock successBlock = ^(OCTToxMessageId messageId) {
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
BOOL sent_push = NO;
if (messageId == -1) {
if ((friend.pushToken != nil) && (friend.pushToken.length > 5)) {
// check push url starts with allowed values
if (
([friend.pushToken hasPrefix:@"https://tox.zoff.xyz/toxfcm/fcm.php?id="])
||
([friend.pushToken hasPrefix:@"https://gotify1.unifiedpush.org/UP?token="])
||
([friend.pushToken hasPrefix:@"https://ntfy.sh/"])
) {
sent_push = YES;
}
}
}
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
OCTMessageAbstract *message = [realmManager addMessageWithText:text type:type chat:chat sender:nil messageId:messageId msgv3HashHex:msgv3HashHex sentPush:sent_push tssent:msgv3tssec tsrcvd:0];
if (userSuccessBlock) {
userSuccessBlock(message);
}
};
OCTSendMessageOperationFailureBlock failureBlock = ^(NSError *error) {
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
if ((error.code == OCTToxErrorFriendSendMessageFriendNotConnected) &&
[strongSelf.dataSource managerUseFauxOfflineMessaging]) {
NSString *friend_pushToken = friend.pushToken;
triggerPush(friend_pushToken, msgv3HashHex, strongSelf, chat);
successBlock(-1);
return;
}
if (userFailureBlock) {
userFailureBlock(error);
}
};
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
friendNumber:friend.friendNumber
messageType:type
message:text
msgv3HashHex:msgv3HashHex
msgv3tssec:msgv3tssec
successBlock:successBlock
failureBlock:failureBlock];
[self.sendMessageQueue addOperation:operation];
}
- (BOOL)setIsTyping:(BOOL)isTyping inChat:(OCTChat *)chat error:(NSError **)error
{
NSParameterAssert(chat);
OCTFriend *friend = [chat.friends firstObject];
OCTTox *tox = [self.dataSource managerGetTox];
return [tox setUserIsTyping:isTyping forFriendNumber:friend.friendNumber error:error];
}
#pragma mark - NSNotification
- (void)friendConnectionStatusChangeNotification:(NSNotification *)notification
{
OCTFriend *friend = notification.object;
if (! friend) {
OCTLogWarn(@"no friend received in notification %@, exiting", notification);
return;
}
if (friend.isConnected) {
[self resendUndeliveredMessagesToFriend:friend];
}
}
#pragma mark - Private
- (void)resendUndeliveredMessagesToFriend:(OCTFriend *)friend
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@"
@" AND senderUniqueIdentifier == nil"
@" AND messageText.isDelivered == NO",
chat.uniqueIdentifier];
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
for (OCTMessageAbstract *message in results) {
OCTLogInfo(@"Resending message to friend %@", friend);
__weak OCTSubmanagerChatsImpl *weakSelf = self;
OCTSendMessageOperationSuccessBlock successBlock = ^(OCTToxMessageId messageId) {
__strong OCTSubmanagerChatsImpl *strongSelf = weakSelf;
OCTRealmManager *realmManager = [strongSelf.dataSource managerGetRealmManager];
[realmManager updateObject:message withBlock:^(OCTMessageAbstract *theMessage) {
theMessage.messageText.messageId = messageId;
}];
};
OCTSendMessageOperationFailureBlock failureBlock = ^(NSError *error) {
OCTLogWarn(@"Cannot resend message to friend %@, error %@", friend, error);
};
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
friendNumber:friend.friendNumber
messageType:message.messageText.type
message:message.messageText.text
msgv3HashHex:message.messageText.msgv3HashHex
msgv3tssec:message.tssent
successBlock:successBlock
failureBlock:failureBlock];
[self.sendMessageQueue addOperation:operation];
}
}
#pragma mark - OCTToxDelegate
/*
* send mesgV3 high level ACK message.
*/
- (void)tox:(OCTTox *)tox sendFriendHighlevelACK:(NSString *)message
friendNumber:(OCTToxFriendNumber)friendNumber
msgv3HashHex:(NSString *)msgv3HashHex
sendTimestamp:(uint32_t)sendTimestamp
{
OCTSendMessageOperation *operation = [[OCTSendMessageOperation alloc] initWithTox:[self.dataSource managerGetTox]
friendNumber:friendNumber
messageType:OCTToxMessageTypeHighlevelack
message:message
msgv3HashHex:msgv3HashHex
msgv3tssec:sendTimestamp
successBlock:nil
failureBlock:nil];
[self.sendMessageQueue addOperation:operation];
}
/*
* Process incoming text message from friend.
*/
- (void)tox:(OCTTox *)tox friendMessage:(NSString *)message
type:(OCTToxMessageType)type
friendNumber:(OCTToxFriendNumber)friendNumber
msgv3HashHex:(NSString *)msgv3HashHex
sendTimestamp:(uint32_t)sendTimestamp
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
if (msgv3HashHex != nil)
{
// HINT: check for double message, but only select incoming messages (senderUniqueIdentifier != NULL)
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier != nil",
chat.uniqueIdentifier, msgv3HashHex];
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
OCTMessageAbstract *message_found = [results firstObject];
if (message_found) {
OCTLogInfo(@"friendMessage ignoring double message i %@", chat.uniqueIdentifier);
OCTLogInfo(@"friendMessage ignoring double message f %@", friend);
return;
}
}
[realmManager addMessageWithText:message type:type chat:chat sender:friend messageId:0 msgv3HashHex:msgv3HashHex sentPush:NO tssent:sendTimestamp tsrcvd:0];
}
- (void)tox:(OCTTox *)tox friendHighLevelACK:(NSString *)message
friendNumber:(OCTToxFriendNumber)friendNumber
msgv3HashHex:(NSString *)msgv3HashHex
sendTimestamp:(uint32_t)sendTimestamp
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
// HINT: only select outgoing messages
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.msgv3HashHex == %@ AND senderUniqueIdentifier == nil",
chat.uniqueIdentifier, msgv3HashHex];
// HINT: we still sort and use only 1 result row, just in case more than 1 row is returned.
// but if more than 1 row is returned that would actually be an error.
// we use the newest message with this Hash
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
results = [results sortedResultsUsingKeyPath:@"dateInterval" ascending:YES];
OCTMessageAbstract *message_found = [results firstObject];
if (! message_found) {
return;
}
OCTLogInfo(@"friendHighLevelACK recevied from friend %@", friend);
[realmManager updateObject:message_found withBlock:^(OCTMessageAbstract *theMessage) {
theMessage.messageText.isDelivered = YES;
}];
}
- (void)tox:(OCTTox *)tox messageDelivered:(OCTToxMessageId)messageId friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
if (friend.msgv3Capability == YES)
{
// HINT: if friend has msgV3 capability, we ignore the low level ACK and keep waiting for the high level ACK
OCTLogInfo(@"messageDelivered ignoring low level ACK %@", friend);
return;
}
OCTChat *chat = [realmManager getOrCreateChatWithFriend:friend];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"chatUniqueIdentifier == %@ AND messageText.messageId == %d",
chat.uniqueIdentifier, messageId];
// messageId is reset on every launch, so we want to update delivered status on latest message.
RLMResults *results = [realmManager objectsWithClass:[OCTMessageAbstract class] predicate:predicate];
results = [results sortedResultsUsingKeyPath:@"dateInterval" ascending:NO];
OCTMessageAbstract *message = [results firstObject];
if (! message) {
return;
}
[realmManager updateObject:message withBlock:^(OCTMessageAbstract *theMessage) {
theMessage.messageText.isDelivered = YES;
}];
}
@end

View File

@ -0,0 +1,46 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import <Foundation/Foundation.h>
@class OCTTox;
@class OCTRealmManager;
@protocol OCTFileStorageProtocol;
/**
* Notification is send when connection status of friend has changed.
*
* - object OCTFriend whose status has changed.
* - userInfo nil
*/
static NSString *const kOCTFriendConnectionStatusChangeNotification = @"kOCTFriendConnectionStatusChangeNotification";
/**
* Notification is send on user avatar update.
*
* - object nil
* - userInfo nil
*/
static NSString *const kOCTUserAvatarWasUpdatedNotification = @"kOCTUserAvatarWasUpdatedNotification";
/**
* Send this notifications to schedule cleanup of uploaded/downloaded files. All files without OCTMessageFile
* will be removed.
*
* - object nil
* - userInfo nil
*/
static NSString *const kOCTScheduleFileTransferCleanupNotification = @"kOCTScheduleFileTransferCleanupNotification";
@protocol OCTSubmanagerDataSource <NSObject>
- (OCTTox *)managerGetTox;
- (BOOL)managerIsToxConnected;
- (void)managerSaveTox;
- (OCTRealmManager *)managerGetRealmManager;
- (id<OCTFileStorageProtocol>)managerGetFileStorage;
- (NSNotificationCenter *)managerGetNotificationCenter;
- (BOOL)managerUseFauxOfflineMessaging;
@end

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerFiles.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerFilesImpl : NSObject <OCTSubmanagerFiles, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerFriends.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerFriendsImpl : NSObject <OCTSubmanagerFriends, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,361 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerFriendsImpl.h"
#import "OCTLogging.h"
#import "OCTTox.h"
#import "OCTFriend.h"
#import "OCTFriendRequest.h"
#import "OCTRealmManager.h"
#import "Firebase.h"
@implementation OCTSubmanagerFriendsImpl
@synthesize dataSource = _dataSource;
#pragma mark - Public
- (BOOL)sendFriendRequestToAddress:(NSString *)address message:(NSString *)message error:(NSError **)error
{
NSParameterAssert(address);
NSParameterAssert(message);
OCTTox *tox = [self.dataSource managerGetTox];
OCTToxFriendNumber friendNumber = [tox addFriendWithAddress:address message:message error:error];
if (friendNumber == kOCTToxFriendNumberFailure) {
return NO;
}
[self.dataSource managerSaveTox];
return [self createFriendWithFriendNumber:friendNumber error:error];
}
- (BOOL)approveFriendRequest:(OCTFriendRequest *)friendRequest error:(NSError **)error
{
NSParameterAssert(friendRequest);
OCTTox *tox = [self.dataSource managerGetTox];
OCTToxFriendNumber friendNumber = [tox addFriendWithNoRequestWithPublicKey:friendRequest.publicKey error:error];
if (friendNumber == kOCTToxFriendNumberFailure) {
return NO;
}
[self.dataSource managerSaveTox];
[[self.dataSource managerGetRealmManager] deleteObject:friendRequest];
return [self createFriendWithFriendNumber:friendNumber error:error];
}
- (void)removeFriendRequest:(OCTFriendRequest *)friendRequest
{
NSParameterAssert(friendRequest);
[[self.dataSource managerGetRealmManager] deleteObject:friendRequest];
}
- (BOOL)removeFriend:(OCTFriend *)friend error:(NSError **)error
{
NSParameterAssert(friend);
OCTTox *tox = [self.dataSource managerGetTox];
if (! [tox deleteFriendWithFriendNumber:friend.friendNumber error:error]) {
return NO;
}
[self.dataSource managerSaveTox];
[[self.dataSource managerGetRealmManager] deleteObject:friend];
return YES;
}
#pragma mark - Private category
- (void)configure
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
OCTTox *tox = [self.dataSource managerGetTox];
[realmManager updateObjectsWithClass:[OCTFriend class] predicate:nil updateBlock:^(OCTFriend *friend) {
// Tox may change friendNumber after relaunch, resetting them.
friend.friendNumber = kOCTToxFriendNumberFailure;
}];
for (NSNumber *friendNumber in [tox friendsArray]) {
OCTToxFriendNumber number = [friendNumber intValue];
NSError *error;
NSString *publicKey = [tox publicKeyFromFriendNumber:number error:&error];
if (! publicKey) {
@throw [NSException exceptionWithName:@"Cannot find publicKey for existing friendNumber, Tox save data is broken"
reason:error.debugDescription
userInfo:nil];
}
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"publicKey == %@", publicKey];
RLMResults *results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
if (results.count == 0) {
// It seems that friend is in Tox but isn't in Realm. Let's add it.
[self createFriendWithFriendNumber:number error:nil];
continue;
}
OCTFriend *friend = [results firstObject];
// Reset some fields for friends.
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.friendNumber = number;
theFriend.status = OCTToxUserStatusNone;
theFriend.isConnected = NO;
theFriend.connectionStatus = OCTToxConnectionStatusNone;
theFriend.isTyping = NO;
NSDate *dateOffline = [tox friendGetLastOnlineWithFriendNumber:number error:nil];
theFriend.lastSeenOnlineInterval = [dateOffline timeIntervalSince1970];
}];
}
// Remove all OCTFriend's which aren't bounded to tox. User cannot interact with them anyway.
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"friendNumber == %d", kOCTToxFriendNumberFailure];
RLMResults *results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
for (OCTFriend *friend in results) {
[realmManager deleteObject:friend];
}
}
#pragma mark - OCTToxDelegate
- (void)tox:(OCTTox *)tox friendRequestWithMessage:(NSString *)message publicKey:(NSString *)publicKey
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSPredicate *predicate = [NSPredicate predicateWithFormat:@"publicKey == %@", publicKey];
RLMResults *results = [realmManager objectsWithClass:[OCTFriendRequest class] predicate:predicate];
if (results.count > 0) {
// friendRequest already exists
return;
}
results = [realmManager objectsWithClass:[OCTFriend class] predicate:predicate];
if (results.count > 0) {
// friend with such publicKey already exists
return;
}
OCTFriendRequest *request = [OCTFriendRequest new];
request.publicKey = publicKey;
request.message = message;
request.dateInterval = [[NSDate date] timeIntervalSince1970];
[realmManager addObject:request];
}
- (void)tox:(OCTTox *)tox friendNameUpdate:(NSString *)name friendNumber:(OCTToxFriendNumber)friendNumber
{
[self.dataSource managerSaveTox];
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.name = name;
if (name.length && [theFriend.nickname isEqualToString:theFriend.publicKey]) {
theFriend.nickname = name;
}
}];
}
- (void)tox:(OCTTox *)tox friendPushTokenUpdate:(NSString *)pushToken friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.pushToken = pushToken;
}];
}
- (void)tox:(OCTTox *)tox friendStatusMessageUpdate:(NSString *)statusMessage friendNumber:(OCTToxFriendNumber)friendNumber
{
[self.dataSource managerSaveTox];
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.statusMessage = statusMessage;
}];
}
- (void)tox:(OCTTox *)tox friendStatusUpdate:(OCTToxUserStatus)status friendNumber:(OCTToxFriendNumber)friendNumber
{
[self.dataSource managerSaveTox];
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.status = status;
}];
}
- (void)tox:(OCTTox *)tox friendIsTypingUpdate:(BOOL)isTyping friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.isTyping = isTyping;
}];
}
- (void)tox:(OCTTox *)tox friendSetMsgv3Capability:(BOOL)msgv3Capability friendNumber:(OCTToxFriendNumber)friendNumber
{
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
if (friend.msgv3Capability != msgv3Capability)
{
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
theFriend.msgv3Capability = msgv3Capability;
}];
}
}
- (void)tox:(OCTTox *)tox friendConnectionStatusChanged:(OCTToxConnectionStatus)status friendNumber:(OCTToxFriendNumber)friendNumber
{
[self.dataSource managerSaveTox];
OCTRealmManager *realmManager = [self.dataSource managerGetRealmManager];
NSString *publicKey = [[self.dataSource managerGetTox] publicKeyFromFriendNumber:friendNumber error:nil];
OCTFriend *friend = [realmManager friendWithPublicKey:publicKey];
[realmManager updateObject:friend withBlock:^(OCTFriend *theFriend) {
if ((status != OCTToxConnectionStatusNone)
&& (theFriend.connectionStatus == OCTToxConnectionStatusNone))
{
// Friend is coming online now
OCTToxCapabilities f_caps = [tox friendGetCapabilitiesWithFriendNumber:friendNumber];
OCTLogVerbose(@"f_caps=%lu", f_caps);
NSString* cap_string = [NSString stringWithFormat:@"%lu", f_caps];
theFriend.capabilities2 = cap_string;
NSString *token = [FIRMessaging messaging].FCMToken;
if (token.length > 0)
{
// HINT: prepend a dummy "A" char as placeholder for Tox Packet ID.
// it will be replaced in sendLosslessPacketWithFriendNumber by pktid
NSString *data = [NSString stringWithFormat:@"Ahttps://tox.zoff.xyz/toxfcm/fcm.php?id=%@&type=1", token];
// NSLog(@"token push url=%@", data);
NSError *error;
// HINT: pktid 181 is for sending push urls to friends
BOOL result = [tox sendLosslessPacketWithFriendNumber:friendNumber
pktid:181
data:data
error:&error];
}
}
theFriend.isConnected = (status != OCTToxConnectionStatusNone);
theFriend.connectionStatus = status;
if (! theFriend.isConnected) {
// Friend is offline now
NSDate *dateOffline = [tox friendGetLastOnlineWithFriendNumber:friendNumber error:nil];
NSTimeInterval timeSince = [dateOffline timeIntervalSince1970];
theFriend.lastSeenOnlineInterval = timeSince;
}
}];
[[self.dataSource managerGetNotificationCenter] postNotificationName:kOCTFriendConnectionStatusChangeNotification object:friend];
}
#pragma mark - Private
- (BOOL)createFriendWithFriendNumber:(OCTToxFriendNumber)friendNumber error:(NSError **)userError
{
OCTTox *tox = [self.dataSource managerGetTox];
NSError *error;
OCTFriend *friend = [OCTFriend new];
friend.friendNumber = friendNumber;
friend.publicKey = [tox publicKeyFromFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.name = [tox friendNameWithFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.statusMessage = [tox friendStatusMessageWithFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.status = [tox friendStatusWithFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.connectionStatus = [tox friendConnectionStatusWithFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
NSDate *lastSeenOnline = [tox friendGetLastOnlineWithFriendNumber:friendNumber error:&error];
friend.lastSeenOnlineInterval = [lastSeenOnline timeIntervalSince1970];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.isTyping = [tox isFriendTypingWithFriendNumber:friendNumber error:&error];
if ([self checkForError:error andAssignTo:userError]) {
return NO;
}
friend.isConnected = (friend.connectionStatus != OCTToxConnectionStatusNone);
friend.nickname = friend.name.length ? friend.name : friend.publicKey;
[[self.dataSource managerGetRealmManager] addObject:friend];
return YES;
}
- (BOOL)checkForError:(NSError *)toCheck andAssignTo:(NSError **)toAssign
{
if (! toCheck) {
return NO;
}
if (toAssign) {
*toAssign = toCheck;
}
return YES;
}
@end

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerObjects.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerObjectsImpl : NSObject <OCTSubmanagerObjects, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,103 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerObjectsImpl.h"
#import "OCTRealmManager.h"
#import "OCTFriend.h"
#import "OCTFriendRequest.h"
#import "OCTChat.h"
#import "OCTCall.h"
#import "OCTMessageAbstract.h"
#import "OCTSettingsStorageObject.h"
@implementation OCTSubmanagerObjectsImpl
@synthesize dataSource = _dataSource;
#pragma mark - Public
- (void)setGenericSettingsData:(NSData *)data
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:manager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
object.genericSettingsData = data;
}];
}
- (NSData *)genericSettingsData
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
return manager.settingsStorage.genericSettingsData;
}
- (RLMResults *)objectsForType:(OCTFetchRequestType)type predicate:(NSPredicate *)predicate
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
return [manager objectsWithClass:[self classForFetchRequestType:type] predicate:predicate];
}
- (OCTObject *)objectWithUniqueIdentifier:(NSString *)uniqueIdentifier forType:(OCTFetchRequestType)type
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
return [manager objectWithUniqueIdentifier:uniqueIdentifier class:[self classForFetchRequestType:type]];
}
#pragma mark - Friends
- (void)changeFriend:(OCTFriend *)friend nickname:(NSString *)nickname
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:friend withBlock:^(OCTFriend *theFriend) {
if (nickname.length) {
theFriend.nickname = nickname;
}
else if (theFriend.name.length) {
theFriend.nickname = theFriend.name;
}
else {
theFriend.nickname = theFriend.publicKey;
}
}];
}
#pragma mark - Chats
- (void)changeChat:(OCTChat *)chat enteredText:(NSString *)enteredText
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:chat withBlock:^(OCTChat *theChat) {
theChat.enteredText = enteredText;
}];
}
- (void)changeChat:(OCTChat *)chat lastReadDateInterval:(NSTimeInterval)lastReadDateInterval
{
OCTRealmManager *manager = [self.dataSource managerGetRealmManager];
[manager updateObject:chat withBlock:^(OCTChat *theChat) {
theChat.lastReadDateInterval = lastReadDateInterval;
}];
}
#pragma mark - Private
- (Class)classForFetchRequestType:(OCTFetchRequestType)type
{
switch (type) {
case OCTFetchRequestTypeFriend:
return [OCTFriend class];
case OCTFetchRequestTypeFriendRequest:
return [OCTFriendRequest class];
case OCTFetchRequestTypeChat:
return [OCTChat class];
case OCTFetchRequestTypeCall:
return [OCTCall class];
case OCTFetchRequestTypeMessageAbstract:
return [OCTMessageAbstract class];
}
}
@end

View File

@ -0,0 +1,16 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerDataSource.h"
#import "OCTToxDelegate.h"
@protocol OCTSubmanagerProtocol <OCTToxDelegate>
@property (weak, nonatomic) id<OCTSubmanagerDataSource> dataSource;
@optional
- (void)configure;
@end

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerUser.h"
#import "OCTSubmanagerProtocol.h"
@interface OCTSubmanagerUserImpl : NSObject <OCTSubmanagerUser, OCTSubmanagerProtocol>
@end

View File

@ -0,0 +1,132 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
#import "OCTSubmanagerUserImpl.h"
#import "OCTTox.h"
#import "OCTManagerConstants.h"
#import "OCTRealmManager.h"
#import "OCTSettingsStorageObject.h"
@implementation OCTSubmanagerUserImpl
@synthesize delegate = _delegate;
@synthesize dataSource = _dataSource;
#pragma mark - Properties
- (OCTToxConnectionStatus)connectionStatus
{
return [self.dataSource managerGetTox].connectionStatus;
}
- (NSString *)userAddress
{
return [self.dataSource managerGetTox].userAddress;
}
- (OCTToxCapabilities)capabilities
{
return [self.dataSource managerGetTox].capabilities;
}
- (NSString *)publicKey
{
return [self.dataSource managerGetTox].publicKey;
}
#pragma mark - Public
- (OCTToxNoSpam)nospam
{
return [self.dataSource managerGetTox].nospam;
}
- (void)setNospam:(OCTToxNoSpam)nospam
{
[self.dataSource managerGetTox].nospam = nospam;
[self.dataSource managerSaveTox];
}
- (OCTToxUserStatus)userStatus
{
return [self.dataSource managerGetTox].userStatus;
}
- (void)setUserStatus:(OCTToxUserStatus)userStatus
{
[self.dataSource managerGetTox].userStatus = userStatus;
[self.dataSource managerSaveTox];
}
- (BOOL)setUserName:(NSString *)name error:(NSError **)error
{
if ([[self.dataSource managerGetTox] setNickname:name error:error]) {
[self.dataSource managerSaveTox];
return YES;
}
return NO;
}
- (NSString *)userName
{
return [[self.dataSource managerGetTox] userName];
}
- (BOOL)setUserStatusMessage:(NSString *)statusMessage error:(NSError **)error
{
if ([[self.dataSource managerGetTox] setUserStatusMessage:statusMessage error:error]) {
[self.dataSource managerSaveTox];
return YES;
}
return NO;
}
- (NSString *)userStatusMessage
{
return [[self.dataSource managerGetTox] userStatusMessage];
}
- (BOOL)setUserAvatar:(NSData *)avatar error:(NSError **)error
{
if (avatar && (avatar.length > kOCTManagerMaxAvatarSize)) {
if (error) {
*error = [NSError errorWithDomain:kOCTManagerErrorDomain
code:OCTSetUserAvatarErrorTooBig
userInfo:@{
NSLocalizedDescriptionKey : @"Cannot set user avatar",
NSLocalizedFailureReasonErrorKey : @"Avatar is too big",
}];
}
return NO;
}
OCTRealmManager *realmManager = self.dataSource.managerGetRealmManager;
[realmManager updateObject:realmManager.settingsStorage withBlock:^(OCTSettingsStorageObject *object) {
object.userAvatarData = avatar;
}];
[self.dataSource.managerGetNotificationCenter postNotificationName:kOCTUserAvatarWasUpdatedNotification object:nil];
return YES;
}
- (NSData *)userAvatar
{
return self.dataSource.managerGetRealmManager.settingsStorage.userAvatarData;
}
#pragma mark - OCTToxDelegate
- (void)tox:(OCTTox *)tox connectionStatus:(OCTToxConnectionStatus)connectionStatus
{
if (connectionStatus != OCTToxConnectionStatusNone) {
[self.dataSource managerSaveTox];
}
[self.delegate submanagerUser:self connectionStatusUpdate:connectionStatus];
}
@end