diff --git a/NextcloudTalk/Calls/NCCallController.m b/NextcloudTalk/Calls/NCCallController.m index ce1158a1e..923ebfc9f 100644 --- a/NextcloudTalk/Calls/NCCallController.m +++ b/NextcloudTalk/Calls/NCCallController.m @@ -27,7 +27,6 @@ #import "NCDatabaseManager.h" #import "NCSettingsController.h" #import "NCSignalingController.h" -#import "NCExternalSignalingController.h" #import "NCScreensharingController.h" #import "NextcloudTalk-Swift.h" diff --git a/NextcloudTalk/Chat/BaseChatViewController.swift b/NextcloudTalk/Chat/BaseChatViewController.swift index 924c5a84f..53da8b366 100644 --- a/NextcloudTalk/Chat/BaseChatViewController.swift +++ b/NextcloudTalk/Chat/BaseChatViewController.swift @@ -1321,9 +1321,11 @@ import SwiftUI } if let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId) { - let mySessionId = signalingController.sessionId() - let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") - signalingController.sendCall(message) + let mySessionId = signalingController.sessionId + + if let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") { + signalingController.sendCallMessage(message) + } } } @@ -1335,13 +1337,12 @@ import SwiftUI let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId) else { return } - let participantMap = signalingController.getParticipantMap() - let mySessionId = signalingController.sessionId() + // TODO: This should be part of the external signaling controller + let mySessionId = signalingController.sessionId - for (key, _) in participantMap { - if let sessionId = key as? String { - let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") - signalingController.sendCall(message) + for (sessionId, _) in signalingController.participantsMap { + if let message = NCStartedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") { + signalingController.sendCallMessage(message) } } } @@ -1352,13 +1353,12 @@ import SwiftUI let signalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: self.room.accountId) else { return } - let participantMap = signalingController.getParticipantMap() - let mySessionId = signalingController.sessionId() + // TODO: This should be part of the external signaling controller + let mySessionId = signalingController.sessionId - for (key, _) in participantMap { - if let sessionId = key as? String { - let message = NCStoppedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") - signalingController.sendCall(message) + for (sessionId, _) in signalingController.participantsMap { + if let message = NCStoppedTypingMessage(from: mySessionId, sendTo: sessionId, withPayload: [:], forRoomType: "") { + signalingController.sendCallMessage(message) } } } diff --git a/NextcloudTalk/Chat/ChatViewController.swift b/NextcloudTalk/Chat/ChatViewController.swift index f271f84be..275b566ae 100644 --- a/NextcloudTalk/Chat/ChatViewController.swift +++ b/NextcloudTalk/Chat/ChatViewController.swift @@ -557,12 +557,12 @@ import SwiftUI NotificationCenter.default.addObserver(self, selector: #selector(didReceiveMessagesInBackground(notification:)), name: NSNotification.Name.NCChatControllerDidReceiveMessagesInBackground, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didChangeRoomCapabilities(notification:)), name: NSNotification.Name.NCDatabaseManagerRoomCapabilitiesChanged, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantJoin(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveJoinOfParticipant, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantLeave(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveLeaveOfParticipant, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStartedTyping(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveStartedTyping, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStoppedTyping(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidReceiveStoppedTyping, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantJoin(notification:)), name: .extSignalingDidReceiveJoinOfParticipant, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveParticipantLeave(notification:)), name: .extSignalingDidReceiveLeaveOfParticipant, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStartedTyping(notification:)), name: .extSignalingDidReceiveStartedTyping, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didReceiveStoppedTyping(notification:)), name: .extSignalingDidReceiveStoppedTyping, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(didFailRequestingCallTransaction(notification:)), name: NSNotification.Name.CallKitManagerDidFailRequestingCallTransaction, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(didUpdateParticipants(notification:)), name: NSNotification.Name.NCExternalSignalingControllerDidUpdateParticipants, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(didUpdateParticipants(notification:)), name: .extSignalingDidUpdateParticipants, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(notification:)), name: UIApplication.didBecomeActiveNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(appWillResignActive(notification:)), name: UIApplication.willResignActiveNotification, object: nil) diff --git a/NextcloudTalk/NextcloudTalk-Bridging-Header.h b/NextcloudTalk/NextcloudTalk-Bridging-Header.h index af3743b7f..92f9c8905 100644 --- a/NextcloudTalk/NextcloudTalk-Bridging-Header.h +++ b/NextcloudTalk/NextcloudTalk-Bridging-Header.h @@ -18,7 +18,7 @@ #import "NCContactsManager.h" #import "NCDatabaseManager.h" #import "NCChatFileController.h" -#import "NCExternalSignalingController.h" +#import "NCSignalingMessage.h" #import "NCNavigationController.h" #import "NCPoll.h" #import "NCRoomsManager.h" diff --git a/NextcloudTalk/Rooms/NCRoomsManager.m b/NextcloudTalk/Rooms/NCRoomsManager.m index b2b75e955..dc042dd86 100644 --- a/NextcloudTalk/Rooms/NCRoomsManager.m +++ b/NextcloudTalk/Rooms/NCRoomsManager.m @@ -13,7 +13,6 @@ #import "NCChatController.h" #import "NCChatMessage.h" #import "NCDatabaseManager.h" -#import "NCExternalSignalingController.h" #import "NCSettingsController.h" #import "NCUserInterfaceController.h" #import "NotificationCenterNotifications.h" diff --git a/NextcloudTalk/Rooms/NCRoomsManagerExtensions.swift b/NextcloudTalk/Rooms/NCRoomsManagerExtensions.swift index 5029bb4a6..61a12e307 100644 --- a/NextcloudTalk/Rooms/NCRoomsManagerExtensions.swift +++ b/NextcloudTalk/Rooms/NCRoomsManagerExtensions.swift @@ -187,7 +187,7 @@ import Foundation let federation = signalingSettings?.getFederationJoinDictionary() - extSignalingController.joinRoom(token, withSessionId: sessionId, withFederation: federation) { error in + extSignalingController.joinRoom(withRoomId: token, withSessionId: sessionId, withFederation: federation) { error in // If the sessionId is not the same anymore we tried to join with, we either already left again before // joining the external signaling server succeeded, or we already have another join in process if !self.isJoiningRoom(withToken: token) { @@ -250,7 +250,7 @@ import Foundation self.joiningRoomToken = token self.joinRoomTask = NCAPIController.sharedInstance().joinRoom(token, forAccount: activeAccount, completionBlock: { sessionId, room, error, statusCode, statusReason in - if error == nil { + if error == nil, let sessionId { roomController.userSessionId = sessionId roomController.inCall = true @@ -269,7 +269,7 @@ import Foundation let federation = signalingSettings?.getFederationJoinDictionary() - extSignalingController.joinRoom(token, withSessionId: sessionId, withFederation: federation) { error in + extSignalingController.joinRoom(withRoomId: token, withSessionId: sessionId, withFederation: federation) { error in if error == nil { NCUtils.log("Re-Joined room \(token) in external signaling server successfully.") completionBlock(sessionId, room, nil, 0, nil) @@ -320,7 +320,7 @@ import Foundation print("Could not exit room. Error: \(error.localizedDescription)") } else { if let extSignalingController = NCSettingsController.sharedInstance().externalSignalingController(forAccountId: activeAccount.accountId) { - extSignalingController.leaveRoom(token) + extSignalingController.leaveRoom(withRoomId: token) } self.checkForPendingToStartCalls() diff --git a/NextcloudTalk/Settings/NCSettingsController.m b/NextcloudTalk/Settings/NCSettingsController.m index 15feb774d..a07f5ea58 100644 --- a/NextcloudTalk/Settings/NCSettingsController.m +++ b/NextcloudTalk/Settings/NCSettingsController.m @@ -19,7 +19,6 @@ #import "NCAPIController.h" #import "NCAppBranding.h" #import "NCDatabaseManager.h" -#import "NCExternalSignalingController.h" #import "NCKeyChainController.h" #import "NCRoomsManager.h" #import "NCUserInterfaceController.h" @@ -583,7 +582,7 @@ - (NCExternalSignalingController * _Nullable)setSignalingConfigurationForAccount } TalkAccount *account = [[NCDatabaseManager sharedInstance] talkAccountForAccountId:accountId]; - extSignalingController = [[NCExternalSignalingController alloc] initWithAccount:account server:signalingSettings.server andTicket:signalingSettings.ticket]; + extSignalingController = [[NCExternalSignalingController alloc] initWithAccount:account serverUrl:signalingSettings.server ticket:signalingSettings.ticket]; [self->_externalSignalingControllers setObject:extSignalingController forKey:accountId]; [bgTask stopBackgroundTask]; diff --git a/NextcloudTalk/WebRTC/NCExternalSignalingController.h b/NextcloudTalk/WebRTC/NCExternalSignalingController.h deleted file mode 100644 index b18e53489..000000000 --- a/NextcloudTalk/WebRTC/NCExternalSignalingController.h +++ /dev/null @@ -1,62 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import - -#import "NCSignalingMessage.h" - -@class NCExternalSignalingController; -@class TalkAccount; -@class SignalingParticipant; - -extern NSString * const NCExternalSignalingControllerDidUpdateParticipantsNotification; -extern NSString * const NCExternalSignalingControllerDidReceiveJoinOfParticipantNotification; -extern NSString * const NCExternalSignalingControllerDidReceiveLeaveOfParticipantNotification; -extern NSString * const NCExternalSignalingControllerDidReceiveStartedTypingNotification; -extern NSString * const NCExternalSignalingControllerDidReceiveStoppedTypingNotification; - -typedef NS_ENUM(NSInteger, NCExternalSignalingSendMessageStatus) { - SendMessageSuccess = 0, - SendMessageSocketError, - SendMessageApplicationError -}; - -@protocol NCExternalSignalingControllerDelegate - -- (void)externalSignalingController:(NCExternalSignalingController *)externalSignalingController didReceivedSignalingMessage:(NSDictionary *)signalingMessageDict; -- (void)externalSignalingController:(NCExternalSignalingController *)externalSignalingController didReceivedParticipantListMessage:(NSDictionary *)participantListMessageDict; -- (void)externalSignalingControllerShouldRejoinCall:(NCExternalSignalingController *)externalSignalingController; -- (void)externalSignalingControllerWillRejoinCall:(NCExternalSignalingController *)externalSignalingController; -- (void)externalSignalingController:(NCExternalSignalingController *)externalSignalingController shouldSwitchToCall:(NSString *)roomToken; - -@end - -@interface NCExternalSignalingController : NSObject - -typedef void (^SendMessageCompletionBlock)(NSURLSessionWebSocketTask *task, NCExternalSignalingSendMessageStatus status); -typedef void (^JoinRoomExternalSignalingCompletionBlock)(NSError *error); - -@property (nonatomic, strong) NSString *currentRoom; -@property (nonatomic, strong) TalkAccount *account; -@property (nonatomic, assign) BOOL disconnected; -@property (nonatomic, weak) id delegate; - -- (instancetype)initWithAccount:(TalkAccount *)account server:(NSString *)serverUrl andTicket:(NSString *)ticket; -- (BOOL)hasMCU; -- (NSString *)sessionId; -- (void)joinRoom:(NSString *)roomId withSessionId:(NSString *)sessionId withFederation:(NSDictionary * _Nullable)federationDict withCompletionBlock:(JoinRoomExternalSignalingCompletionBlock)block; -- (void)leaveRoom:(NSString *)roomId; -- (void)sendCallMessage:(NCSignalingMessage *)message; -- (void)sendSendOfferMessageWithSessionId:(NSString *)sessionId andRoomType:(NSString *)roomType; -- (void)sendRoomMessageOfType:(NSString *)messageType andRoomType:(NSString *)roomType; -- (void)requestOfferForSessionId:(NSString *)sessionId andRoomType:(NSString *)roomType; -- (SignalingParticipant * _Nullable)getParticipantFromSessionId:(NSString * _Nonnull)sessionId; -- (NSMutableDictionary * _Nonnull)getParticipantMap; -- (void)connect; -- (void)forceConnect; -- (void)disconnect; -- (void)forceReconnectForRejoin; - -@end diff --git a/NextcloudTalk/WebRTC/NCExternalSignalingController.m b/NextcloudTalk/WebRTC/NCExternalSignalingController.m deleted file mode 100644 index ba7656759..000000000 --- a/NextcloudTalk/WebRTC/NCExternalSignalingController.m +++ /dev/null @@ -1,893 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import "NCExternalSignalingController.h" - -#import "NCAPIController.h" -#import "NCDatabaseManager.h" -#import "NCRoomsManager.h" -#import "NCSettingsController.h" -#import "WSMessage.h" - -#import "NextcloudTalk-Swift.h" - -static NSTimeInterval kInitialReconnectInterval = 1; -static NSTimeInterval kMaxReconnectInterval = 16; -static NSTimeInterval kWebSocketTimeoutInterval = 15; - -NSString * const NCExternalSignalingControllerDidUpdateParticipantsNotification = @"NCExternalSignalingControllerDidUpdateParticipantsNotification"; -NSString * const NCExternalSignalingControllerDidReceiveJoinOfParticipantNotification = @"NCExternalSignalingControllerDidReceiveJoinOfParticipant"; -NSString * const NCExternalSignalingControllerDidReceiveLeaveOfParticipantNotification = @"NCExternalSignalingControllerDidReceiveLeaveOfParticipant"; -NSString * const NCExternalSignalingControllerDidReceiveStartedTypingNotification = @"NCExternalSignalingControllerDidReceiveStartedTypingNotification"; -NSString * const NCExternalSignalingControllerDidReceiveStoppedTypingNotification = @"NCExternalSignalingControllerDidReceiveStoppedTypingNotification"; - -@interface NCExternalSignalingController () - -@property (nonatomic, strong) NSURLSessionWebSocketTask *webSocket; -@property (nonatomic, strong) NSString* serverUrl; -@property (nonatomic, strong) NSString* ticket; -@property (nonatomic, strong) NSString* resumeId; -@property (nonatomic, strong) NSString* sessionId; -@property (nonatomic, strong) NSString* userId; -@property (nonatomic, strong) NSString* authenticationBackendUrl; -@property (nonatomic, assign) BOOL helloResponseReceived; -@property (nonatomic, assign) BOOL mcuSupport; -@property (nonatomic, strong) NSMutableDictionary* participantsMap; -@property (nonatomic, strong) NSMutableArray* pendingMessages; -@property (nonatomic, assign) NSInteger messageId; -@property (nonatomic, strong) WSMessage *helloMessage; -@property (nonatomic, strong) NSMutableArray *messagesWithCompletionBlocks; -@property (nonatomic, assign) NSInteger reconnectInterval; -@property (nonatomic, strong) NSTimer *reconnectTimer; -@property (nonatomic, assign) NSInteger disconnectTime; - -@end - -@implementation NCExternalSignalingController - -- (instancetype)initWithAccount:(TalkAccount *)account server:(NSString *)serverUrl andTicket:(NSString *)ticket -{ - self = [super init]; - if (self) { - _account = account; - _userId = _account.userId; - _authenticationBackendUrl = [[NCAPIController sharedInstance] authenticationBackendUrlForAccount:_account]; - [self setServer:serverUrl andTicket:ticket]; - } - return self; -} - -- (BOOL)hasMCU -{ - return _mcuSupport; -} - -- (NSString *)sessionId -{ - return _sessionId; -} - -- (void)setServer:(NSString *)serverUrl andTicket:(NSString *)ticket -{ - _serverUrl = [self getWebSocketUrlForServer:serverUrl]; - _ticket = ticket; - _reconnectInterval = kInitialReconnectInterval; - _pendingMessages = [NSMutableArray new]; - - [self connect]; -} - -- (NSString *)getWebSocketUrlForServer:(NSString *)serverUrl -{ - NSString *wsUrl = [serverUrl copy]; - - // Change to websocket protocol - wsUrl = [wsUrl stringByReplacingOccurrencesOfString:@"https://" withString:@"wss://"]; - wsUrl = [wsUrl stringByReplacingOccurrencesOfString:@"http://" withString:@"ws://"]; - // Remove trailing slash - if([wsUrl hasSuffix:@"/"]) { - wsUrl = [wsUrl substringToIndex:[wsUrl length] - 1]; - } - // Add spreed endpoint - wsUrl = [wsUrl stringByAppendingString:@"/spreed"]; - - return wsUrl; -} - -#pragma mark - WebSocket connection - -- (void)connect -{ - [self connect:NO]; -} - -- (void)forceConnect -{ - [self connect:YES]; -} - -- (void)connect:(BOOL)force -{ - BOOL forceConnect = force || [NCRoomsManager sharedInstance].callViewController; - // Do not try to connect if the app is running in the background (unless forcing a connection or in a call) - if (!forceConnect && [[UIApplication sharedApplication] applicationState] == UIApplicationStateBackground) { - [NCUtils log:@"Trying to create websocket connection while app is in the background"]; - _disconnected = YES; - return; - } - - [self invalidateReconnectionTimer]; - - _disconnected = NO; - _messageId = 1; - _messagesWithCompletionBlocks = [NSMutableArray new]; - _helloResponseReceived = NO; - - [NCUtils log:[NSString stringWithFormat:@"Connecting to: %@", _serverUrl]]; - NSURL *url = [NSURL URLWithString:_serverUrl]; - - NSString *userAgent = [NSString stringWithFormat:@"Mozilla/5.0 (iOS) Nextcloud-Talk v%@", - [[[NSBundle mainBundle] infoDictionary] objectForKey:@"CFBundleShortVersionString"]]; - - NSURLSession *wsSession = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration defaultSessionConfiguration] delegate:self delegateQueue:nil]; - NSMutableURLRequest *wsRequest = [[NSMutableURLRequest alloc] initWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:kWebSocketTimeoutInterval]; - [wsRequest setValue:userAgent forHTTPHeaderField:@"User-Agent"]; - - if (self.resumeId) { - NSTimeInterval currentTimestamp = [[NSDate date] timeIntervalSince1970]; - - // We are only allowed to resume a session 30s after disconnect - if (!self.disconnectTime || (currentTimestamp - self.disconnectTime) >= 30) { - [NCUtils log:@"We have a resumeId, but we disconnected outside of the 30s resume window. Connecting without resumeId."]; - self.resumeId = nil; - } - } - - NSURLSessionWebSocketTask *webSocket = [wsSession webSocketTaskWithRequest:wsRequest]; - _webSocket = webSocket; - - [_webSocket resume]; - - [self receiveMessage]; -} - -- (void)reconnect -{ - // Note: Make sure to call reconnect only from the main-thread! - if (_reconnectTimer) { - return; - } - - [NCUtils log:[NSString stringWithFormat:@"Reconnecting to: %@", _serverUrl]]; - - [self resetWebSocket]; - - // Execute completion blocks on all messages - for (WSMessage *message in self->_messagesWithCompletionBlocks) { - [message executeCompletionBlockWithStatus:SendMessageSocketError]; - } - - [self setReconnectionTimer]; -} - -- (void)forceReconnect -{ - dispatch_async(dispatch_get_main_queue(), ^{ - self->_resumeId = nil; - self->_currentRoom = nil; - [self reconnect]; - }); -} - -- (void)forceReconnectForRejoin -{ - // In case we force reconnect in order to rejoin the call again, we need to keep the currently joined room. - // In `helloResponseReceived` we determine that we were in a room and that the sessionId changed, in that case - // we trigger a re-join in `NCRoomsManager` which takes care of re-joining. - dispatch_async(dispatch_get_main_queue(), ^{ - NSDictionary *byeDict = @{ - @"type": @"bye", - @"bye": @{} - }; - - // Close our current session. Don't leave the room, as that would defeat the above mentioned purpose - [self sendMessage:byeDict withCompletionBlock:^(NSURLSessionWebSocketTask *task, NCExternalSignalingSendMessageStatus status) { - self->_resumeId = nil; - [self reconnect]; - }]; - }); -} - -- (void)disconnect -{ - [NCUtils log:[NSString stringWithFormat:@"Disconnecting from: %@", _serverUrl]]; - - self.disconnectTime = [[NSDate date] timeIntervalSince1970]; - - dispatch_async(dispatch_get_main_queue(), ^{ - [self invalidateReconnectionTimer]; - [self resetWebSocket]; - }); -} - -- (void)resetWebSocket -{ - [_webSocket cancel]; - _webSocket = nil; - _helloResponseReceived = NO; - [_helloMessage ignoreCompletionBlock]; - _helloMessage = nil; - _disconnected = YES; -} - -- (void)setReconnectionTimer -{ - [self invalidateReconnectionTimer]; - // Wiggle interval a little bit to prevent all clients from connecting - // simultaneously in case the server connection is interrupted. - NSInteger interval = _reconnectInterval - (_reconnectInterval / 2) + arc4random_uniform((int)_reconnectInterval); - NSLog(@"Reconnecting in %ld", (long)interval); - dispatch_async(dispatch_get_main_queue(), ^{ - self->_reconnectTimer = [NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(connect) userInfo:nil repeats:NO]; - }); - _reconnectInterval = _reconnectInterval * 2; - if (_reconnectInterval > kMaxReconnectInterval) { - _reconnectInterval = kMaxReconnectInterval; - } -} - -- (void)invalidateReconnectionTimer -{ - [_reconnectTimer invalidate]; - _reconnectTimer = nil; -} - -#pragma mark - WebSocket messages - -- (void)sendMessage:(NSDictionary *)jsonDict withCompletionBlock:(SendMessageCompletionBlock)block -{ - WSMessage *wsMessage = [[WSMessage alloc] initWithMessage:jsonDict withCompletionBlock:block]; - - // Add message as pending message if websocket is not connected - if (!_helloResponseReceived && !wsMessage.isHelloMessage) { - dispatch_async(dispatch_get_main_queue(), ^{ - if (wsMessage.isJoinMessage) { - // We join a new room, so any message which wasn't send by now is not relevant for the new room anymore - self->_pendingMessages = [NSMutableArray new]; - } - - [NCUtils log:@"Trying to send message before we received a hello response -> adding to pendingMessages"]; - [self->_pendingMessages addObject:wsMessage]; - }); - - return; - } - - [self sendMessage:wsMessage]; -} - -- (void)sendMessage:(WSMessage *)wsMessage -{ - // Assign messageId and timeout to messages with completionBlocks - if (wsMessage.completionBlock) { - NSString *messageIdString = [NSString stringWithFormat: @"%ld", (long)_messageId++]; - wsMessage.messageId = messageIdString; - - if (wsMessage.isHelloMessage) { - [_helloMessage ignoreCompletionBlock]; - _helloMessage = wsMessage; - } else { - [_messagesWithCompletionBlocks addObject:wsMessage]; - } - } - - if (!wsMessage.webSocketMessage) { - NSLog(@"Error creating websocket message"); - [wsMessage executeCompletionBlockWithStatus:SendMessageApplicationError]; - return; - } - - [wsMessage sendMessageWithWebSocket:_webSocket]; -} - -- (void)sendHelloMessage -{ - NSDictionary *helloDict = @{ - @"type": @"hello", - @"hello": @{ - @"version": @"1.0", - @"auth": @{ - @"url": _authenticationBackendUrl, - @"params": @{ - @"userid": _userId, - @"ticket": _ticket - } - } - } - }; - // Try to resume session - if (_resumeId) { - helloDict = @{ - @"type": @"hello", - @"hello": @{ - @"version": @"1.0", - @"resumeid": _resumeId - } - }; - } - - [NCUtils log:@"Sending hello message"]; - - [self sendMessage:helloDict withCompletionBlock:^(NSURLSessionWebSocketTask *task, NCExternalSignalingSendMessageStatus status) { - if (status == SendMessageSocketError && task == self->_webSocket) { - [NCUtils log:[NSString stringWithFormat:@"Reconnecting from sendHelloMessage"]]; - [self reconnect]; - } - }]; -} - -- (void)helloResponseReceived:(NSDictionary *)messageDict -{ - _helloResponseReceived = YES; - - [NCUtils log:[NSString stringWithFormat:@"Hello received with %ld pending messages", _pendingMessages.count]]; - - NSString *messageId = [messageDict objectForKey:@"id"]; - [self executeCompletionBlockForMessageId:messageId withStatus:SendMessageSuccess]; - - NSDictionary *helloDict = [messageDict objectForKey:@"hello"]; - _resumeId = [helloDict objectForKey:@"resumeid"]; - - NSString *newSessionId = [helloDict objectForKey:@"sessionid"]; - BOOL sessionChanged = _sessionId && ![_sessionId isEqualToString:newSessionId]; - _sessionId = newSessionId; - - NSArray *serverFeatures = [[helloDict objectForKey:@"server"] objectForKey:@"features"]; - for (NSString *feature in serverFeatures) { - if ([feature isEqualToString:@"mcu"]) { - _mcuSupport = YES; - } - } - - NSString *serverVersion = [[helloDict objectForKey:@"server"] objectForKey:@"version"]; - dispatch_async(dispatch_get_main_queue(), ^{ - BGTaskHelper *bgTask = [BGTaskHelper startBackgroundTaskWithName:@"NCUpdateSignalingVersionTransaction" expirationHandler:nil]; - [[NCDatabaseManager sharedInstance] setExternalSignalingServerVersion:serverVersion forAccountId:self->_account.accountId]; - - // Send pending messages - for (WSMessage *wsMessage in self->_pendingMessages) { - [self sendMessage:wsMessage]; - } - self->_pendingMessages = [NSMutableArray new]; - - [bgTask stopBackgroundTask]; - }); - - // Re-join if user was in a room - if (_currentRoom && sessionChanged) { - sessionChanged = NO; - [self.delegate externalSignalingControllerWillRejoinCall:self]; - - [[NCRoomsManager sharedInstance] rejoinRoomForCall:_currentRoom completionBlock:^(NSString * _Nullable sessionId, NCRoom * _Nullable room, NSError * _Nullable error, NSInteger statusCode, NSString * _Nullable statusReason) { - [self.delegate externalSignalingControllerShouldRejoinCall:self]; - }]; - } -} - -- (void)errorResponseReceived:(NSDictionary *)messageDict -{ - NSString *errorCode = [[messageDict objectForKey:@"error"] objectForKey:@"code"]; - NSString *messageId = [messageDict objectForKey:@"id"]; - - [NCUtils log:[NSString stringWithFormat:@"Received error response %@", errorCode]]; - - if ([errorCode isEqualToString:@"no_such_session"] || [errorCode isEqualToString:@"too_many_requests"]) { - // We could not resume the previous session, but the websocket is still alive -> resend the hello message without a resumeId - _resumeId = nil; - [self sendHelloMessage]; - - return; - } else if ([errorCode isEqualToString:@"already_joined"]) { - // We already joined this room on the signaling server - NSDictionary *details = [[messageDict objectForKey:@"error"] objectForKey:@"details"]; - NSString *roomId = [[details objectForKey:@"room"] objectForKey:@"roomid"]; - - // If we are aware that we were in this room before, we should treat this as a success - if ([_currentRoom isEqualToString:roomId]) { - [self executeCompletionBlockForMessageId:messageId withStatus:SendMessageSuccess]; - - return; - } - } - - [self executeCompletionBlockForMessageId:messageId withStatus:SendMessageApplicationError]; -} - -- (void)joinRoom:(NSString *)roomId withSessionId:(NSString *)sessionId withFederation:(NSDictionary *)federationDict withCompletionBlock:(JoinRoomExternalSignalingCompletionBlock)block -{ - - if (_disconnected) { - [NCUtils log:[NSString stringWithFormat:@"Joining room %@, but the websocket is disconnected.", roomId]]; - } - - if (_webSocket == nil) { - [NCUtils log:[NSString stringWithFormat:@"Joining room %@, but the websocket is nil.", roomId]]; - } - - NSDictionary *messageDict = @{ - @"type": @"room", - @"room": @{ - @"roomid": roomId, - @"sessionid": sessionId - } - }; - - if (federationDict) { - messageDict = @{ - @"type": @"room", - @"room": @{ - @"roomid": roomId, - @"sessionid": sessionId, - @"federation": federationDict - } - }; - } - - [self sendMessage:messageDict withCompletionBlock:^(NSURLSessionWebSocketTask *task, NCExternalSignalingSendMessageStatus status) { - if (status == SendMessageSocketError && task == self->_webSocket) { - // Reconnect if this is still the same socket we tried to send the message on - [NCUtils log:[NSString stringWithFormat:@"Reconnect from joinRoom"]]; - - // When we failed to join a room, we shouldn't try to resume a session but instead do a force reconnect - [self forceReconnect]; - } - - if (block) { - NSError *error = nil; - - if (status != SendMessageSuccess) { - error = [NSError errorWithDomain:NSCocoaErrorDomain code:0 userInfo:nil]; - } - - block(error); - } - }]; -} - -- (void)leaveRoom:(NSString *)roomId -{ - if ([_currentRoom isEqualToString:roomId]) { - _currentRoom = nil; - [self joinRoom:@"" withSessionId:@"" withFederation:nil withCompletionBlock:nil]; - } else { - NSLog(@"External signaling: Not leaving because it's not room we joined"); - } -} - -- (void)sendCallMessage:(NCSignalingMessage *)message -{ - NSDictionary *messageDict = @{ - @"type": @"message", - @"message": @{ - @"recipient": @{ - @"type": @"session", - @"sessionid": message.to - }, - @"data": [message functionDict] - } - }; - - [self sendMessage:messageDict withCompletionBlock:nil]; -} - -- (void)sendSendOfferMessageWithSessionId:(NSString *)sessionId andRoomType:(NSString *)roomType -{ - NSDictionary *messageDict = @{ - @"type": @"message", - @"message": @{ - @"recipient": @{ - @"type": @"session", - @"sessionid": sessionId - }, - @"data": @{ - @"type": @"sendoffer", - @"roomType": roomType - } - } - }; - - [self sendMessage:messageDict withCompletionBlock:nil]; -} - -- (void)sendRoomMessageOfType:(NSString *)messageType andRoomType:(NSString *)roomType -{ - NSDictionary *messageDict = @{ - @"type": @"message", - @"message": @{ - @"recipient": @{ - @"type": @"room" - }, - @"data": @{ - @"type": messageType, - @"roomType": roomType - } - } - }; - - [self sendMessage:messageDict withCompletionBlock:nil]; -} - -- (void)requestOfferForSessionId:(NSString *)sessionId andRoomType:(NSString *)roomType -{ - NSDictionary *messageDict = @{ - @"type": @"message", - @"message": @{ - @"recipient": @{ - @"type": @"session", - @"sessionid": sessionId - }, - @"data": @{ - @"type": @"requestoffer", - @"roomType": roomType - } - } - }; - - [self sendMessage:messageDict withCompletionBlock:nil]; -} - -- (void)roomMessageReceived:(NSDictionary *)messageDict -{ - NSString *newRoomId = [[messageDict objectForKey:@"room"] objectForKey:@"roomid"]; - - // Only reset the participant map when the room actually changed - // Otherwise we would loose participant information for example when a recording is started - if (![_currentRoom isEqualToString:newRoomId]) { - _participantsMap = [NSMutableDictionary new]; - _currentRoom = newRoomId; - } - - NSString *messageId = [messageDict objectForKey:@"id"]; - [self executeCompletionBlockForMessageId:messageId withStatus:SendMessageSuccess]; -} - -- (void)eventMessageReceived:(NSDictionary *)eventDict -{ - NSString *eventTarget = [eventDict objectForKey:@"target"]; - if ([eventTarget isEqualToString:@"room"]) { - [self processRoomEvent:eventDict]; - } else if ([eventTarget isEqualToString:@"roomlist"]) { - [self processRoomListEvent:eventDict]; - } else if ([eventTarget isEqualToString:@"participants"]) { - [self processRoomParticipantsEvent:eventDict]; - } else { - NSLog(@"Unsupported event target: %@", eventDict); - } -} - -- (void)processRoomEvent:(NSDictionary *)eventDict -{ - NSString *eventType = [eventDict objectForKey:@"type"]; - if ([eventType isEqualToString:@"join"]) { - NSArray *joins = [eventDict objectForKey:@"join"]; - for (NSDictionary *participantDict in joins) { - SignalingParticipant *participant = [[SignalingParticipant alloc] initWithJoinDictionary:participantDict]; - - if (!participant.isFederated && [participant.userId isEqualToString:_userId]) { - NSLog(@"App user joined room."); - } else { - NSLog(@"Participant joined room."); - - // Only notify if another participant joined the room and not ourselves from a different device - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; - - if (_currentRoom && participant.signalingSessionId){ - [userInfo setObject:_currentRoom forKey:@"roomToken"]; - [userInfo setObject:participant.signalingSessionId forKey:@"sessionId"]; - } - - [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveJoinOfParticipantNotification - object:self - userInfo:userInfo]; - } - - [_participantsMap setObject:participant forKey:participant.signalingSessionId]; - } - } else if ([eventType isEqualToString:@"leave"]) { - NSArray *leftSessions = [eventDict objectForKey:@"leave"]; - for (NSString *sessionId in leftSessions) { - SignalingParticipant *participant = [self getParticipantFromSessionId:sessionId]; - - [_participantsMap removeObjectForKey:sessionId]; - - if ([participant.signalingSessionId isEqualToString:_sessionId] || (!participant.isFederated && [participant.userId isEqualToString:_userId])) { - // Ignore own session - continue; - } - - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; - - if (_currentRoom && sessionId){ - [userInfo setObject:_currentRoom forKey:@"roomToken"]; - [userInfo setObject:sessionId forKey:@"sessionId"]; - - if (participant.userId) { - [userInfo setObject:participant.userId forKey:@"userId"]; - } - } - - [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveLeaveOfParticipantNotification - object:self - userInfo:userInfo]; - } - } else if ([eventType isEqualToString:@"message"]) { - [self processRoomMessageEvent:[eventDict objectForKey:@"message"]]; - } else if ([eventType isEqualToString:@"switchto"]) { - [self processSwitchToMessageEvent:[eventDict objectForKey:@"switchto"]]; - } else { - NSLog(@"Unknown room event: %@", eventDict); - } -} - -- (void)processRoomMessageEvent:(NSDictionary *)messageDict -{ - NSString *messageType = [[messageDict objectForKey:@"data"] objectForKey:@"type"]; - if ([messageType isEqualToString:@"chat"]) { - NSLog(@"Chat message received."); - } else if ([messageType isEqualToString:@"recording"]) { - [self.delegate externalSignalingController:self didReceivedSignalingMessage:messageDict]; - } else { - NSLog(@"Unknown room message type: %@", messageDict); - } -} - -- (void)processSwitchToMessageEvent:(NSDictionary *)messageDict -{ - NSString *roomToken = [messageDict objectForKey:@"roomid"]; - if (roomToken.length > 0) { - [self.delegate externalSignalingController:self shouldSwitchToCall:roomToken]; - } else { - NSLog(@"Unknown switchTo message: %@", messageDict); - } -} - -- (void)processRoomListEvent:(NSDictionary *)eventDict -{ - NSLog(@"Refresh room list."); -} - -- (void)processRoomParticipantsEvent:(NSDictionary *)eventDict -{ - NSString *eventType = [eventDict objectForKey:@"type"]; - if ([eventType isEqualToString:@"update"]) { - //NSLog(@"Participant list changed: %@", [eventDict objectForKey:@"update"]); - NSDictionary *updateDict = [eventDict objectForKey:@"update"]; - [self.delegate externalSignalingController:self didReceivedParticipantListMessage:updateDict]; - - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; - NSString *roomToken = [updateDict objectForKey:@"roomid"]; - if (roomToken){ - [userInfo setObject:roomToken forKey:@"roomToken"]; - } - NSArray *users = [updateDict objectForKey:@"users"]; - if (users) { - for (NSDictionary *userDict in users) { - NSString *sessionId = [userDict objectForKey:@"sessionId"]; - SignalingParticipant *participant = [self getParticipantFromSessionId:sessionId]; - - if (participant) { - [participant updateWithUpdateDictionary:userDict]; - } - } - - [userInfo setObject:users forKey:@"users"]; - } - [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidUpdateParticipantsNotification - object:self - userInfo:userInfo]; - } else { - NSLog(@"Unknown room event: %@", eventDict); - } -} - -- (void)messageReceived:(NSDictionary *)messageDict -{ - NSString *messageType = [[messageDict objectForKey:@"data"] objectForKey:@"type"]; - if ([messageType isEqualToString:@"startedTyping"] || [messageType isEqualToString:@"stoppedTyping"]) { - NSMutableDictionary *userInfo = [[NSMutableDictionary alloc] init]; - - NSDictionary *sender = [messageDict objectForKey:@"sender"]; - NSString *fromSession = [sender objectForKey:@"sessionid"]; - NSString *fromUser = [sender objectForKey:@"userid"]; - - if (_currentRoom && fromSession){ - [userInfo setObject:_currentRoom forKey:@"roomToken"]; - [userInfo setObject:fromSession forKey:@"sessionId"]; - - if (fromUser) { - [userInfo setObject:fromUser forKey:@"userId"]; - } - - SignalingParticipant *participant = [_participantsMap objectForKey:fromSession]; - if (participant) { - BOOL isFederated = participant.isFederated; - [userInfo setObject:@(isFederated) forKey:@"isFederated"]; - - if (participant.displayName != nil) { - [userInfo setObject:participant.displayName forKey:@"displayName"]; - } - } - } - - if ([messageType isEqualToString:@"startedTyping"]) { - [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveStartedTypingNotification - object:self - userInfo:userInfo]; - } else { - [[NSNotificationCenter defaultCenter] postNotificationName:NCExternalSignalingControllerDidReceiveStoppedTypingNotification - object:self - userInfo:userInfo]; - } - } else { - [self.delegate externalSignalingController:self didReceivedSignalingMessage:messageDict]; - } -} - -#pragma mark - Completion blocks - -- (void)executeCompletionBlockForMessageId:(NSString *)messageId withStatus:(NCExternalSignalingSendMessageStatus)status -{ - if (!messageId) { - return; - } - - dispatch_async(dispatch_get_main_queue(), ^{ - if ([self->_helloMessage.messageId isEqualToString:messageId]) { - [self->_helloMessage executeCompletionBlockWithStatus:status]; - self->_helloMessage = nil; - return; - } - - for (WSMessage *message in self->_messagesWithCompletionBlocks) { - if ([messageId isEqualToString:message.messageId]) { - [message executeCompletionBlockWithStatus:status]; - [self->_messagesWithCompletionBlocks removeObject:message]; - break; - } - } - }); -} - -#pragma mark - NSURLSessionWebSocketDelegate - -- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didOpenWithProtocol:(NSString *)protocol -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (webSocketTask != self->_webSocket) { - return; - } - - - [NCUtils log:@"WebSocket Connected!"]; - self->_reconnectInterval = kInitialReconnectInterval; - [self sendHelloMessage]; - }); -} - -- (void)URLSession:(NSURLSession *)session webSocketTask:(NSURLSessionWebSocketTask *)webSocketTask didCloseWithCode:(NSURLSessionWebSocketCloseCode)closeCode reason:(NSData *)reason -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (webSocketTask != self->_webSocket) { - return; - } - - [NCUtils log:[NSString stringWithFormat:@"WebSocket didCloseWithCode:%ld reason:%@", (long)closeCode, reason]]; - [self reconnect]; - }); -} - -- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(NSError *)error -{ - dispatch_async(dispatch_get_main_queue(), ^{ - if (task != self->_webSocket) { - return; - } - - if (error) { - [NCUtils log:[NSString stringWithFormat:@"WebSocket session didCompleteWithError: %@", error.description]]; - [self reconnect]; - } - }); -} - -- (void)receiveMessage { - __weak NCExternalSignalingController *weakSelf = self; - __block NSURLSessionWebSocketTask *receivingWebSocket = _webSocket; - - [_webSocket receiveMessageWithCompletionHandler:^(NSURLSessionWebSocketMessage * _Nullable message, NSError * _Nullable error) { - if (!error) { - NSData *messageData = message.data; - NSString *messageString = message.string; - - if (message.type == NSURLSessionWebSocketMessageTypeString) { - messageData = [message.string dataUsingEncoding:NSUTF8StringEncoding]; - } - - if (message.type == NSURLSessionWebSocketMessageTypeData) { - messageString = [[NSString alloc] initWithData:messageData encoding:NSUTF8StringEncoding]; - } - - //NSLog(@"WebSocket didReceiveMessage: %@", messageString); - NSDictionary *messageDict = [weakSelf getWebSocketMessageFromJSONData:messageData]; - NSString *messageType = [messageDict objectForKey:@"type"]; - if ([messageType isEqualToString:@"hello"]) { - [weakSelf helloResponseReceived:messageDict]; - } else if ([messageType isEqualToString:@"error"]) { - [weakSelf errorResponseReceived:messageDict]; - } else if ([messageType isEqualToString:@"room"]) { - [weakSelf roomMessageReceived:messageDict]; - } else if ([messageType isEqualToString:@"event"]) { - [weakSelf eventMessageReceived:[messageDict objectForKey:@"event"]]; - } else if ([messageType isEqualToString:@"message"]) { - [weakSelf messageReceived:[messageDict objectForKey:@"message"]]; - } else if ([messageType isEqualToString:@"control"]) { - [weakSelf messageReceived:[messageDict objectForKey:@"control"]]; - } - - // Completion block for messageId should have been handled already at this point - NSString *messageId = [messageDict objectForKey:@"id"]; - [weakSelf executeCompletionBlockForMessageId:messageId withStatus:SendMessageApplicationError]; - - [weakSelf receiveMessage]; - } else { - dispatch_async(dispatch_get_main_queue(), ^{ - // Only try to reconnect if the webSocket is still the one we tried to receive a message on - if (receivingWebSocket != weakSelf.webSocket) { - return; - } - - [NCUtils log:[NSString stringWithFormat:@"WebSocket receiveMessageWithCompletionHandler error %@", error.description]]; - [weakSelf reconnect]; - }); - } - }]; -} - -- (void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler -{ - if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) { - NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]; - completionHandler(NSURLSessionAuthChallengeUseCredential, credential); - } else { - completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil); - } -} - -#pragma mark - Utils - -- (SignalingParticipant *)getParticipantFromSessionId:(NSString *)sessionId -{ - return [_participantsMap objectForKey:sessionId]; -} - -- (NSMutableDictionary *)getParticipantMap -{ - return _participantsMap; -} - -- (NSDictionary *)getWebSocketMessageFromJSONData:(NSData *)jsonData -{ - NSError *error; - NSDictionary* messageDict = [NSJSONSerialization JSONObjectWithData:jsonData - options:kNilOptions - error:&error]; - if (!messageDict) { - NSLog(@"Error parsing websocket message: %@", error); - } - - return messageDict; -} - -@end diff --git a/NextcloudTalk/WebRTC/NCExternalSignalingController.swift b/NextcloudTalk/WebRTC/NCExternalSignalingController.swift new file mode 100644 index 000000000..18446110a --- /dev/null +++ b/NextcloudTalk/WebRTC/NCExternalSignalingController.swift @@ -0,0 +1,839 @@ +// +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +@objc public protocol NCExternalSignalingControllerDelegate { + @objc func externalSignalingController(_ externalSignalingController: NCExternalSignalingController, didReceivedSignalingMessage signalingMessageDict: [AnyHashable: Any]) + @objc func externalSignalingController(_ externalSignalingController: NCExternalSignalingController, didReceivedParticipantListMessage participantListMessageDict: [AnyHashable: Any]) + @objc func externalSignalingControllerShouldRejoinCall(_ externalSignalingController: NCExternalSignalingController) + @objc func externalSignalingControllerWillRejoinCall(_ externalSignalingController: NCExternalSignalingController) + @objc func externalSignalingController(_ externalSignalingController: NCExternalSignalingController, shouldSwitchToCall roomToken: String) +} + +extension Notification.Name { + static let extSignalingDidUpdateParticipants = Notification.Name(rawValue: "NCExternalSignalingControllerDidUpdateParticipantsNotification") + static let extSignalingDidReceiveJoinOfParticipant = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveJoinOfParticipantNotification") + static let extSignalingDidReceiveLeaveOfParticipant = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveLeaveOfParticipantNotification") + static let extSignalingDidReceiveStartedTyping = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveStartedTypingNotification") + static let extSignalingDidReceiveStoppedTyping = Notification.Name(rawValue: "NCExternalSignalingControllerDidReceiveStoppedTypingNotification") +} + +public typealias SendMessageCompletionBlock = (_ task: URLSessionWebSocketTask?, _ status: NCExternalSignalingSendMessageStatus) -> Void + +public enum NCExternalSignalingSendMessageStatus { + case success + case socketError + case applicationError +} + +@objcMembers public class NCExternalSignalingController: NSObject, URLSessionWebSocketDelegate { + + public weak var delegate: NCExternalSignalingControllerDelegate? + + public var currentRoom: String? + + public private(set) var account: TalkAccount + public private(set) var disconnected: Bool = true + public private(set) var hasMCU: Bool = false + public private(set) var sessionId: String? + public private(set) var participantsMap = [String: SignalingParticipant]() + + private let initialReconnectInterval = 1 + private let maxReconnectInterval = 16 + private let webSocketTimeoutInterval = 15.0 + + private var webSocket: URLSessionWebSocketTask? + private var serverUrl: String + private var ticket: String + private var resumeId: String? + private var authenticationBackendUrl: String + private var helloResponseReceived = false + private var nextMessageId: Int = 0 + private var pendingMessages = [WSMessage]() + private var helloMessage: WSMessage? + private var messagesWithCompletionBlock = [WSMessage]() + private var reconnectInterval: Int = 0 + private var reconnectTimer: Timer? + private var disconnectTime: TimeInterval? + + init(account: TalkAccount, serverUrl: String, ticket: String) { + self.account = account + self.serverUrl = serverUrl + self.ticket = ticket + + self.authenticationBackendUrl = NCAPIController.sharedInstance().authenticationBackendUrl(for: account) + self.serverUrl = NCExternalSignalingController.getWebSocketUrl(forServer: serverUrl) + + super.init() + + self.reconnectInterval = self.initialReconnectInterval + self.connect() + } + + static func getWebSocketUrl(forServer server: String) -> String { + var wsUrl = server + + wsUrl = wsUrl.replacingOccurrences(of: "https://", with: "wss://") + wsUrl = wsUrl.replacingOccurrences(of: "http://", with: "ws://") + + if wsUrl.hasSuffix("/") { + wsUrl = String(wsUrl.dropLast()) + } + + wsUrl += "/spreed" + + return wsUrl + } + + // MARK: - WebSocket connection + + func connect() { + self.connect(force: false) + } + + func forceConnect() { + self.connect(force: true) + } + + func connect(force: Bool) { + let forceConnect = force || NCRoomsManager.sharedInstance().callViewController != nil + + // Do not try to connect if the app is running in the background (unless forcing a connection or in a call) + if !forceConnect, UIApplication.shared.applicationState == .background { + NCUtils.log("Trying to create websocket connection while app is in the background") + self.disconnected = true + return + } + + guard let url = URL(string: self.serverUrl) else { return } + + self.invalidateReconnectionTimer() + + self.disconnected = false + self.nextMessageId = 1 + self.messagesWithCompletionBlock = [] + self.helloResponseReceived = false + + NCUtils.log("Connecting to: \(self.serverUrl)") + + let userAgent = "Mozilla/5.0 (iOS) Nextcloud-Talk v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] ?? "Unknown")" + + let wsSession = URLSession(configuration: .default, delegate: self, delegateQueue: nil) + var wsRequest = URLRequest(url: url, cachePolicy: .useProtocolCachePolicy, timeoutInterval: webSocketTimeoutInterval) + wsRequest.setValue(userAgent, forHTTPHeaderField: "User-Agent") + + if self.resumeId != nil { + let currentTimestamp = Date().timeIntervalSince1970 + + // We are only allowed to resume a session 30s after disconnect + if self.disconnectTime == nil || (currentTimestamp - (self.disconnectTime ?? 0)) >= 30 { + NCUtils.log("We have a resumeId, but we disconnected outside of the 30s resume window. Connecting without resumeId.") + self.resumeId = nil + } + } + + let webSocket = wsSession.webSocketTask(with: wsRequest) + self.webSocket = webSocket + + webSocket.resume() + self.receiveMessage() + } + + func reconnect() { + // Note: Make sure to call reconnect only from the main-thread! + dispatchPrecondition(condition: .onQueue(.main)) + + guard self.reconnectTimer == nil else { return } + + NCUtils.log("Reconnecting to: \(self.serverUrl)") + + self.resetWebSocket() + + // Execute completion blocks on all messages + for message in self.messagesWithCompletionBlock { + message.executeCompletionBlock(withStatus: .socketError) + } + + self.setReconnectionTimer() + } + + func forceReconnect() { + DispatchQueue.main.async { + self.resumeId = nil + self.currentRoom = nil + self.reconnect() + } + } + + func forceReconnectForRejoin() { + // In case we force reconnect in order to rejoin the call again, we need to keep the currently joined room. + // In `helloResponseReceived` we determine that we were in a room and that the sessionId changed, in that case + // we trigger a re-join in `NCRoomsManager` which takes care of re-joining. + + DispatchQueue.main.async { + let byeDict = [ + "type": "bye", + "bye": [:] + ] + + // Close our current session. Don't leave the room, as that would defeat the above mentioned purpose + self.send(message: byeDict) { _, _ in + self.resumeId = nil + self.reconnect() + } + } + } + + func disconnect() { + NCUtils.log("Disconnecting from: \(self.serverUrl)") + + self.disconnectTime = Date().timeIntervalSince1970 + + DispatchQueue.main.async { + self.invalidateReconnectionTimer() + self.resetWebSocket() + } + } + + func resetWebSocket() { + self.webSocket?.cancel() + self.webSocket = nil + self.helloResponseReceived = false + self.helloMessage?.ignoreCompletionBlock() + self.helloMessage = nil + self.disconnected = true + } + + func setReconnectionTimer() { + self.invalidateReconnectionTimer() + + // Wiggle interval a little bit to prevent all clients from connecting + // simultaneously in case the server connection is interrupted. + let interval = self.reconnectInterval - (self.reconnectInterval / 2) + Int.random(in: 1...self.reconnectInterval) + print("Reconnecting in \(interval)") + + DispatchQueue.main.async { + self.reconnectTimer = Timer.scheduledTimer(withTimeInterval: TimeInterval(interval), repeats: false, block: { [weak self] _ in + self?.connect() + }) + } + + self.reconnectInterval = min(self.reconnectInterval * 2, maxReconnectInterval) + } + + func invalidateReconnectionTimer() { + self.reconnectTimer?.invalidate() + self.reconnectTimer = nil + } + + // MARK: - WebSocket messages + + func send(message jsonDict: [AnyHashable: Any], withCompletionBlock block: SendMessageCompletionBlock?) { + let wsMessage = WSMessage(message: jsonDict, completionBlock: block) + + // Add message as pending message if websocket is not connected + if !self.helloResponseReceived, !wsMessage.isHelloMessage { + DispatchQueue.main.async { + if wsMessage.isJoinMessage { + // We join a new room, so any message which wasn't send by now is not relevant for the new room anymore + self.pendingMessages = [] + } + + NCUtils.log("Trying to send message before we received a hello response -> adding to pendingMessages") + self.pendingMessages.append(wsMessage) + } + + return + } + + self.send(message: wsMessage) + } + + func send(message wsMessage: WSMessage) { + guard let webSocket = self.webSocket else { return } + + // Assign messageId and timeout to messages with completionBlocks + if wsMessage.completionBlock != nil { + wsMessage.messageId = "\(self.nextMessageId)" + self.nextMessageId += 1 + + if wsMessage.isHelloMessage { + self.helloMessage?.ignoreCompletionBlock() + self.helloMessage = wsMessage + } else { + self.messagesWithCompletionBlock.append(wsMessage) + } + } + + wsMessage.send(withWebSocket: webSocket) + } + + func sendHelloMessage() { + var helloDict: [String: Any] = [ + "type": "hello", + "hello": [ + "version": "1.0", + "auth": [ + "url": self.authenticationBackendUrl, + "params": [ + "userid": account.userId, + "ticket": ticket + ] + ] + ] + ] + + if let resumeId = self.resumeId { + helloDict = [ + "type": "hello", + "hello": [ + "version": "1.0", + "resumeid": resumeId + ] + ] + } + + NCUtils.log("Sending hello message") + + self.send(message: helloDict) { task, status in + if status == .socketError, task == self.webSocket { + NCUtils.log("Reconnecting from sendHelloMessage") + self.reconnect() + } + } + } + + func helloResponseReceived(messageDict: [AnyHashable: Any]) { + self.helloResponseReceived = true + + NCUtils.log("Hello received with \(self.pendingMessages.count) pending messages") + + let messageId = messageDict["id"] as? String ?? "0" + self.executeCompletionBlock(forMessageId: messageId, withStatus: .success) + + guard let helloDict = messageDict["hello"] as? [AnyHashable: Any], + let newSessionId = helloDict["sessionid"] as? String + else { + NCUtils.log("Unable to access hello dictionary") + return + } + + self.resumeId = helloDict["resumeid"] as? String + + let sessionChanged = self.sessionId != newSessionId + self.sessionId = newSessionId + + guard let serverDict = helloDict["server"] as? [AnyHashable: Any], + let serverFeatures = serverDict["features"] as? [String], + let serverVersion = serverDict["version"] as? String + else { + NCUtils.log("Unable to access server dictionary") + return + } + + self.hasMCU = serverFeatures.contains(where: { $0 == "mcu" }) + + DispatchQueue.main.async { + let bgTask = BGTaskHelper.startBackgroundTask(withName: "NCUpdateSignalingVersionTransaction") + NCDatabaseManager.sharedInstance().setExternalSignalingServerVersion(serverVersion, forAccountId: self.account.accountId) + + // Send pending messages + for wsMessage in self.pendingMessages { + self.send(message: wsMessage) + } + + self.pendingMessages = [] + + bgTask.stopBackgroundTask() + } + + // Re-join if user was in a room + if let currentRoom, sessionChanged { + self.delegate?.externalSignalingControllerWillRejoinCall(self) + + NCRoomsManager.sharedInstance().rejoinRoomForCall(currentRoom) { _, _, _, _, _ in + self.delegate?.externalSignalingControllerShouldRejoinCall(self) + } + } + } + + func errorResponseReceived(messageDict: [AnyHashable: Any]) { + guard let errorDict = messageDict["error"] as? [AnyHashable: Any], + let errorCode = errorDict["code"] as? String, + let messageId = messageDict["id"] as? String + else { return } + + NCUtils.log("Received error response \(errorCode)") + + if errorCode == "no_such_session" || errorCode == "too_many_requests" { + // We could not resume the previous session, but the websocket is still alive -> resend the hello message without a resumeId + self.resumeId = nil + self.sendHelloMessage() + + return + } else if errorCode == "already_joined" { + // We already joined this room on the signaling server + guard let detailsDict = errorDict["details"] as? [AnyHashable: Any], + let roomDict = detailsDict["room"] as? [AnyHashable: Any], + let roomId = roomDict["roomid"] as? String + else { return } + + // If we are aware that we were in this room before, we should treat this as a success + if currentRoom == roomId { + self.executeCompletionBlock(forMessageId: messageId, withStatus: .success) + return + } + } + + self.executeCompletionBlock(forMessageId: messageId, withStatus: .applicationError) + } + + func joinRoom(withRoomId roomId: String, withSessionId sessionId: String, withFederation federationDict: [AnyHashable: Any]?, withCompletionBlock block: ((_ error: NSError?) -> Void)?) { + if self.disconnected { + NCUtils.log("Joining room \(roomId), but the websocket is disconnected.") + } + + if self.webSocket == nil { + NCUtils.log("Joining room \(roomId), but the websocket is nil.") + } + + var messageDict: [AnyHashable: Any] = [ + "type": "room", + "room": [ + "roomid": roomId, + "sessionid": sessionId + ] + ] + + if let federationDict { + messageDict = [ + "type": "room", + "room": [ + "roomid": roomId, + "sessionid": sessionId, + "federation": federationDict + ] + ] + } + + self.send(message: messageDict) { task, status in + if status == .socketError, task == self.webSocket { + // Reconnect if this is still the same socket we tried to send the message on + NCUtils.log("Reconnect from joinRoom") + + // When we failed to join a room, we shouldn't try to resume a session but instead do a force reconnect + self.forceReconnect() + } + + if let block { + if status != .success { + block(NSError(domain: NSCocoaErrorDomain, code: 0)) + } else { + block(nil) + } + } + } + } + + func leaveRoom(withRoomId roomId: String) { + if self.currentRoom == roomId { + self.currentRoom = nil + self.joinRoom(withRoomId: "", withSessionId: "", withFederation: nil, withCompletionBlock: nil) + } else { + print("External signaling: Not leaving because it's not the room we joined") + } + } + + func sendCallMessage(_ message: NCSignalingMessage) { + let messageDict: [AnyHashable: Any] = [ + "type": "message", + "message": [ + "recipient": [ + "type": "session", + "sessionid": message.to! + ], + "data": message.functionDict() + ] + ] + + self.send(message: messageDict, withCompletionBlock: nil) + } + + func sendSendOfferMessage(withSessionId sessionId: String, andRoomType roomType: String) { + let messageDict: [AnyHashable: Any] = [ + "type": "message", + "message": [ + "recipient": [ + "type": "session", + "sessionid": sessionId + ], + "data": [ + "type": "sendoffer", + "roomType": roomType + ] + ] + ] + + self.send(message: messageDict, withCompletionBlock: nil) + } + + func requestOffer(forSessionId sessionId: String, andRoomType roomType: String) { + let messageDict: [AnyHashable: Any] = [ + "type": "message", + "message": [ + "recipient": [ + "type": "session", + "sessionid": sessionId + ], + "data": [ + "type": "requestoffer", + "roomType": roomType + ] + ] + ] + + self.send(message: messageDict, withCompletionBlock: nil) + } + + func sendRoomMessage(ofType messageType: String, andRoomType roomType: String) { + let messageDict: [AnyHashable: Any] = [ + "type": "message", + "message": [ + "recipient": [ + "type": "room" + ], + "data": [ + "type": messageType, + "roomType": roomType + ] + ] + ] + + self.send(message: messageDict, withCompletionBlock: nil) + } + + func roomMessageReceived(messageDict: [AnyHashable: Any]) { + guard let roomDict = messageDict["room"] as? [AnyHashable: Any], + let newRoomId = roomDict["roomid"] as? String + else { return } + + // Only reset the participant map when the room actually changed + // Otherwise we would loose participant information for example when a recording is started + if self.currentRoom != newRoomId { + self.participantsMap = [:] + self.currentRoom = newRoomId + } + + if let messageId = messageDict["id"] as? String { + self.executeCompletionBlock(forMessageId: messageId, withStatus: .success) + } + } + + func eventMessageReceived(eventDict: [AnyHashable: Any]) { + let eventTarget = eventDict["target"] as? String + + if eventTarget == "room" { + self.processRoomEvent(eventDict: eventDict) + } else if eventTarget == "roomlist" { + self.processRoomListEvent(eventDict: eventDict) + } else if eventTarget == "participants" { + self.processRoomParticipantsEvents(eventDict: eventDict) + } else { + print("Unsupported event target: \(eventDict)") + } + } + + func processRoomEvent(eventDict: [AnyHashable: Any]) { + guard let eventType = eventDict["type"] as? String + else { return } + + if eventType == "join" { + guard let joinDict = eventDict["join"] as? [[AnyHashable: Any]] + else { return } + + for participantDict in joinDict { + let participant = SignalingParticipant(withJoinDictionary: participantDict) + + if !participant.isFederated, participant.userId == self.account.userId { + print("App user joined room") + continue + } + + // Only notify if another participant joined the room and not ourselves from a different device + print("Participant joined room") + + guard let currentRoom, let signalingSessionId = participant.signalingSessionId + else { continue } + + var userInfo = [String: String]() + userInfo["roomToken"] = currentRoom + userInfo["sessionId"] = signalingSessionId + + NotificationCenter.default.post(name: .extSignalingDidReceiveJoinOfParticipant, object: self, userInfo: userInfo) + self.participantsMap[signalingSessionId] = participant + } + } else if eventType == "leave" { + guard let leftSessions = eventDict["leave"] as? [String] + else { return } + + for sessionId in leftSessions { + guard let participant = self.getParticipant(fromSessionId: sessionId) + else { return } + + self.participantsMap.removeValue(forKey: sessionId) + + guard let currentRoom else { continue } + + if participant.signalingSessionId == self.sessionId || (participant.isFederated && participant.userId == self.account.userId) { + // Ignore own session + continue + } + + var userInfo = [String: String]() + userInfo["roomToken"] = currentRoom + userInfo["sessionId"] = sessionId + + if let userId = participant.userId { + userInfo["userId"] = userId + } + + NotificationCenter.default.post(name: .extSignalingDidReceiveLeaveOfParticipant, object: self, userInfo: userInfo) + } + } else if eventType == "message", let wrappedMessage = eventDict["message"] as? [AnyHashable: Any] { + self.processRoomMessageEvent(messageDict: wrappedMessage) + } else if eventType == "switchto", let wrappedMessage = eventDict["switchto"] as? [AnyHashable: Any] { + self.processSwitchToMessageEvent(messageDict: wrappedMessage) + } else { + print("Unknown room event: \(eventDict)") + } + } + + func processRoomMessageEvent(messageDict: [AnyHashable: Any]) { + guard let dataDict = messageDict["data"] as? [AnyHashable: Any], + let messageType = dataDict["type"] as? String + else { return } + + if messageType == "chat" { + print("Chat message received") + } else if messageType == "recording" { + self.delegate?.externalSignalingController(self, didReceivedSignalingMessage: messageDict) + } else { + print("Unknown room message type \(messageDict)") + } + } + + func processSwitchToMessageEvent(messageDict: [AnyHashable: Any]) { + let roomToken = messageDict["roomid"] as? String + + if let roomToken, !roomToken.isEmpty { + self.delegate?.externalSignalingController(self, shouldSwitchToCall: roomToken) + } else { + print("Unknown switchTo message: \(messageDict)") + } + } + + func processRoomListEvent(eventDict: [AnyHashable: Any]) { + print("Refresh room list.") + } + + func processRoomParticipantsEvents(eventDict: [AnyHashable: Any]) { + guard let eventType = eventDict["type"] as? String + else { return } + + if eventType == "update" { + guard let updateDict = eventDict["update"] as? [AnyHashable: Any] + else { return } + + self.delegate?.externalSignalingController(self, didReceivedParticipantListMessage: updateDict) + + var userInfo = [String: Any]() + + if let roomToken = updateDict["roomid"] as? String { + userInfo["roomToken"] = roomToken + } + + if let users = updateDict["users"] as? [[AnyHashable: Any]] { + for userDict in users { + if let sessionId = userDict["sessionId"] as? String { + self.getParticipant(fromSessionId: sessionId)?.update(withUpdateDictionary: userDict) + } + } + + userInfo["users"] = users + } + + NotificationCenter.default.post(name: .extSignalingDidUpdateParticipants, object: self, userInfo: userInfo) + } else { + print("Unknown room event: \(eventDict)") + } + } + + func messageReceived(messageDict: [AnyHashable: Any]) { + guard let dataDict = messageDict["data"] as? [AnyHashable: Any], + let messageType = dataDict["type"] as? String + else { return } + + if messageType == "startedTyping" || messageType == "stoppedTyping" { + var userInfo = [String: Any]() + + guard let sender = messageDict["sender"] as? [AnyHashable: Any], + let fromSession = sender["sessionid"] as? String, + let currentRoom + else { return } + + userInfo["roomToken"] = currentRoom + userInfo["sessionId"] = fromSession + + if let fromUser = sender["userid"] as? String { + userInfo["userId"] = fromUser + } + + if let participant = self.getParticipant(fromSessionId: fromSession) { + userInfo["isFederated"] = participant.isFederated + + if let displayName = participant.displayName { + userInfo["displayName"] = displayName + } + } + + if messageType == "startedTyping" { + NotificationCenter.default.post(name: .extSignalingDidReceiveStartedTyping, object: self, userInfo: userInfo) + } else { + NotificationCenter.default.post(name: .extSignalingDidReceiveStoppedTyping, object: self, userInfo: userInfo) + } + } else { + self.delegate?.externalSignalingController(self, didReceivedSignalingMessage: messageDict) + } + } + + // MARK: - Completion blocks + + func executeCompletionBlock(forMessageId messageId: String, withStatus status: NCExternalSignalingSendMessageStatus) { + DispatchQueue.main.async { + if let helloMessage = self.helloMessage, helloMessage.messageId == messageId { + self.helloMessage?.executeCompletionBlock(withStatus: status) + self.helloMessage = nil + + return + } + + if let message = self.messagesWithCompletionBlock.first(where: { messageId == $0.messageId }) { + message.executeCompletionBlock(withStatus: status) + self.messagesWithCompletionBlock.removeAll(where: { messageId == $0.messageId }) + } + } + } + + // MARK: - NSURLSessionWebSocketDelegate + + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didOpenWithProtocol protocol: String?) { + DispatchQueue.main.async { + guard webSocketTask == self.webSocket else { return } + + NCUtils.log("WebSocket connected!") + self.reconnectInterval = self.initialReconnectInterval + self.sendHelloMessage() + } + } + + public func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, didCloseWith closeCode: URLSessionWebSocketTask.CloseCode, reason: Data?) { + DispatchQueue.main.async { + guard webSocketTask == self.webSocket else { return } + + NCUtils.log("WebSocket didCloseWithCode: \(closeCode) reason: \(reason, default: "Unknown")") + self.reconnect() + } + } + + public func urlSession(_ session: URLSession, task: URLSessionTask, didCompleteWithError error: (any Error)?) { + DispatchQueue.main.async { + guard task == self.webSocket else { return } + + if let error { + NCUtils.log("WebSocket session didCompleteWithError: \(error)") + self.reconnect() + } + } + } + + public func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) { + if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust, let serverTrust = challenge.protectionSpace.serverTrust { + let credentials = URLCredential(trust: serverTrust) + completionHandler(.useCredential, credentials) + } else { + completionHandler(.cancelAuthenticationChallenge, nil) + } + } + + public func receiveMessage() { + guard let webSocket else { return } + + let receivingWebSocket = webSocket + + webSocket.receive { [weak self] result in + guard let self else { return } + + switch result { + case .success(let message): + switch message { + case .string(let string): + self.handleReceivedMessage(message: string.data(using: .utf8)!) + case .data(let data): + self.handleReceivedMessage(message: data) + @unknown default: + break + } + + self.receiveMessage() + case .failure(let error): + DispatchQueue.main.async { + // Only try to reconnect if the webSocket is still the one we tried to receive a message on + guard receivingWebSocket == self.webSocket else { return } + + NCUtils.log("WebSocket receiveMessageWithCompletionHandler error \(error)") + self.reconnect() + } + } + } + } + + private func handleReceivedMessage(message: Data) { + guard let messageDict = self.getWebSocketMessageFromJSONData(jsonData: message), + let messageType = messageDict["type"] as? String + else { return } + + if messageType == "hello" { + self.helloResponseReceived(messageDict: messageDict) + } else if messageType == "error" { + self.errorResponseReceived(messageDict: messageDict) + } else if messageType == "room" { + self.roomMessageReceived(messageDict: messageDict) + } else if messageType == "event", let wrappedMessage = messageDict["event"] as? [AnyHashable: Any] { + self.eventMessageReceived(eventDict: wrappedMessage) + } else if messageType == "message", let wrappedMessage = messageDict["message"] as? [AnyHashable: Any] { + self.messageReceived(messageDict: wrappedMessage) + } else if messageType == "control", let wrappedMessage = messageDict["control"] as? [AnyHashable: Any] { + self.messageReceived(messageDict: wrappedMessage) + } + + // Completion block for messageId should have been handled already at this point + if let messageId = messageDict["id"] as? String { + self.executeCompletionBlock(forMessageId: messageId, withStatus: .applicationError) + } + } + + // MARK: - Utils + + func getParticipant(fromSessionId sessionId: String) -> SignalingParticipant? { + return self.participantsMap[sessionId] + } + + func getWebSocketMessageFromJSONData(jsonData: Data) -> [AnyHashable: Any]? { + let messageDict = try? JSONSerialization.jsonObject(with: jsonData) as? [AnyHashable: Any] + return messageDict + } + +} diff --git a/NextcloudTalk/WebRTC/WSMessage.h b/NextcloudTalk/WebRTC/WSMessage.h deleted file mode 100644 index db1fa25c2..000000000 --- a/NextcloudTalk/WebRTC/WSMessage.h +++ /dev/null @@ -1,26 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import - -#import "NCExternalSignalingController.h" - -@interface WSMessage : NSObject - -@property (nonatomic, copy) NSString *messageId; -@property (nonatomic, copy) NSDictionary *message; -@property (nonatomic, copy) SendMessageCompletionBlock completionBlock; - -- (instancetype)initWithMessage:(NSDictionary *)message; -- (instancetype)initWithMessage:(NSDictionary *)message withCompletionBlock:(SendMessageCompletionBlock)block; -- (NSString *)webSocketMessage; -- (BOOL)isHelloMessage; -- (BOOL)isJoinMessage; -- (void)setMessageTimeout; -- (void)ignoreCompletionBlock; -- (void)executeCompletionBlockWithStatus:(NCExternalSignalingSendMessageStatus)status; -- (void)sendMessageWithWebSocket:(NSURLSessionWebSocketTask *)webSocketTask; - -@end diff --git a/NextcloudTalk/WebRTC/WSMessage.m b/NextcloudTalk/WebRTC/WSMessage.m deleted file mode 100644 index c5adf95b8..000000000 --- a/NextcloudTalk/WebRTC/WSMessage.m +++ /dev/null @@ -1,132 +0,0 @@ -/** - * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -#import "WSMessage.h" -#import "NextcloudTalk-Swift.h" - -static NSTimeInterval kSendMessageTimeoutInterval = 15; - -@interface WSMessage () - -@property NSTimer *timeoutTimer; -@property NSURLSessionWebSocketTask *webSocketTask; - -@end - -@implementation WSMessage - -- (instancetype)initWithMessage:(NSDictionary *)message -{ - self = [super init]; - if (self) { - self.message = message; - } - return self; -} - -- (instancetype)initWithMessage:(NSDictionary *)message withCompletionBlock:(SendMessageCompletionBlock)block -{ - self = [self initWithMessage:message]; - if (self) { - self.completionBlock = block; - } - return self; -} - -- (void)setMessageId:(NSString *)messageId -{ - _messageId = messageId; - - NSMutableDictionary *newMessageDict = [[NSMutableDictionary alloc] initWithDictionary:_message]; - [newMessageDict setObject:messageId forKey:@"id"]; - _message = newMessageDict; -} - -- (BOOL)isHelloMessage -{ - if ([[_message objectForKey:@"type"] isEqualToString:@"hello"]) { - return YES; - } - - return NO; -} - -- (BOOL)isJoinMessage -{ - if ([[_message objectForKey:@"type"] isEqualToString:@"room"]) { - return YES; - } - - return NO; -} - -- (void)setMessageTimeout -{ - // NSTimer uses the runloop of the current thread. Only the main thread guarantees a runloop, so make sure we dispatch it to main! - // This is mainly a problem for the "hello message", because it's send from a NSURL delegate and the timer sometimes fails to run - dispatch_async(dispatch_get_main_queue(), ^{ - self->_timeoutTimer = [NSTimer scheduledTimerWithTimeInterval:kSendMessageTimeoutInterval target:self selector:@selector(executeCompletionBlockWithSocketError) userInfo:nil repeats:NO]; - }); -} - -- (void)ignoreCompletionBlock -{ - dispatch_async(dispatch_get_main_queue(), ^{ - self.completionBlock = nil; - [self->_timeoutTimer invalidate]; - }); -} - -- (void)executeCompletionBlockWithStatus:(NCExternalSignalingSendMessageStatus)status -{ - // As the timer was create on the main thread, it needs to be invalidated on the main thread as well - dispatch_async(dispatch_get_main_queue(), ^{ - if (self.completionBlock) { - self.completionBlock(self.webSocketTask, status); - self.completionBlock = nil; - [self->_timeoutTimer invalidate]; - } - }); -} - -- (void)executeCompletionBlockWithSocketError -{ - [self executeCompletionBlockWithStatus:SendMessageSocketError]; -} - -- (NSString *)webSocketMessage -{ - NSError *error; - NSString *jsonString = nil; - NSData *jsonData = [NSJSONSerialization dataWithJSONObject:_message - options:0 - error:&error]; - if (!jsonData) { - NSLog(@"Error creating websocket message: %@", error); - } else { - jsonString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding]; - } - - return jsonString; -} - -- (void)sendMessageWithWebSocket:(NSURLSessionWebSocketTask *)webSocketTask -{ - self.webSocketTask = webSocketTask; - - if (self.completionBlock) { - [self setMessageTimeout]; - } - - //NSLog(@"Sending: %@", self.webSocketMessage); - NSURLSessionWebSocketMessage *message = [[NSURLSessionWebSocketMessage alloc] initWithString:self.webSocketMessage]; - [webSocketTask sendMessage:message completionHandler:^(NSError * _Nullable error) { - if (error && self.completionBlock) { - [self executeCompletionBlockWithSocketError]; - } - }]; -} - -@end diff --git a/NextcloudTalk/WebRTC/WSMessage.swift b/NextcloudTalk/WebRTC/WSMessage.swift new file mode 100644 index 000000000..62c53db23 --- /dev/null +++ b/NextcloudTalk/WebRTC/WSMessage.swift @@ -0,0 +1,98 @@ +// +// SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors +// SPDX-License-Identifier: GPL-3.0-or-later +// + +import Foundation + +@objc public class WSMessage: NSObject, URLSessionWebSocketDelegate { + + public var messageId: String? { + didSet { + message["id"] = messageId + } + } + + public var message: [AnyHashable: Any] + public var completionBlock: SendMessageCompletionBlock? + + private let sendMessageTimeoutInterval = 15.0 + + private var timeoutTimer: Timer? + private var webSocketTask: URLSessionWebSocketTask? + + public var isHelloMessage: Bool { + return message["type"] as? String == "hello" + } + + public var isJoinMessage: Bool { + return message["type"] as? String == "room" + } + + public init(message: [AnyHashable : Any], completionBlock: SendMessageCompletionBlock? = nil) { + self.message = message + self.completionBlock = completionBlock + } + + public func startMessageTimeoutTimer() { + // NSTimer uses the runloop of the current thread. Only the main thread guarantees a runloop, so make sure we dispatch it to main! + // This is mainly a problem for the "hello message", because it's send from a NSURL delegate and the timer sometimes fails to run + DispatchQueue.main.async { + self.timeoutTimer = Timer.scheduledTimer(withTimeInterval: self.sendMessageTimeoutInterval, repeats: false, block: { _ in + self.executeCompletionBlock(withStatus: .socketError) + }) + } + } + + public func ignoreCompletionBlock() { + DispatchQueue.main.async { + self.completionBlock = nil + self.timeoutTimer?.invalidate() + } + } + + public func executeCompletionBlock(withStatus status: NCExternalSignalingSendMessageStatus) { + // As the timer was create on the main thread, it needs to be invalidated on the main thread as well + DispatchQueue.main.async { + if let completionBlock = self.completionBlock { + completionBlock(self.webSocketTask, status) + self.completionBlock = nil + } + + self.timeoutTimer?.invalidate() + } + } + + private func webSocketMessageString() -> String? { + guard let jsonData = try? JSONSerialization.data(withJSONObject: self.message) + else { + print("Error creating websocket message") + return nil + } + + return String(data: jsonData, encoding: .utf8) + } + + public func send(withWebSocket webSocket: URLSessionWebSocketTask) { + guard let webSocketMessageString = self.webSocketMessageString() + else { + print("Error creating websocket message") + self.executeCompletionBlock(withStatus: .applicationError) + return + } + + self.webSocketTask = webSocket + + if self.completionBlock != nil { + self.startMessageTimeoutTimer() + } + + let webSocketMessage = URLSessionWebSocketTask.Message.string(webSocketMessageString) + + webSocket.send(webSocketMessage) { error in + if error != nil, self.completionBlock != nil { + self.executeCompletionBlock(withStatus: .socketError) + } + } + } +}