mirror of https://github.com/oxen-io/session-ios
				
				
				
			
			You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			308 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			308 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import GRDB
 | |
| import WebRTC
 | |
| import SessionUtilitiesKit
 | |
| import SessionSnodeKit
 | |
| 
 | |
| extension MessageReceiver {
 | |
|     public static func handleCallMessage(
 | |
|         _ db: Database,
 | |
|         threadId: String,
 | |
|         threadVariant: SessionThread.Variant,
 | |
|         message: CallMessage
 | |
|     ) throws {
 | |
|         // Only support calls from contact threads
 | |
|         guard threadVariant == .contact else { return }
 | |
|         
 | |
|         switch message.kind {
 | |
|             case .preOffer: try MessageReceiver.handleNewCallMessage(db, message: message)
 | |
|             case .offer: MessageReceiver.handleOfferCallMessage(db, message: message)
 | |
|             case .answer: MessageReceiver.handleAnswerCallMessage(db, message: message)
 | |
|             case .provisionalAnswer: break // TODO: Implement
 | |
|                 
 | |
|             case let .iceCandidates(sdpMLineIndexes, sdpMids):
 | |
|                 guard let currentWebRTCSession = WebRTCSession.current, currentWebRTCSession.uuid == message.uuid else {
 | |
|                     return
 | |
|                 }
 | |
|                 var candidates: [RTCIceCandidate] = []
 | |
|                 let sdps = message.sdps
 | |
|                 for i in 0..<sdps.count {
 | |
|                     let sdp = sdps[i]
 | |
|                     let sdpMLineIndex = sdpMLineIndexes[i]
 | |
|                     let sdpMid = sdpMids[i]
 | |
|                     let candidate = RTCIceCandidate(sdp: sdp, sdpMLineIndex: Int32(sdpMLineIndex), sdpMid: sdpMid)
 | |
|                     candidates.append(candidate)
 | |
|                 }
 | |
|                 currentWebRTCSession.handleICECandidates(candidates)
 | |
|                 
 | |
|             case .endCall: MessageReceiver.handleEndCallMessage(db, message: message)
 | |
|         }
 | |
|     }
 | |
|     
 | |
|     // MARK: - Specific Handling
 | |
|     
 | |
|     private static func handleNewCallMessage(_ db: Database, message: CallMessage) throws {
 | |
|         SNLog("[Calls] Received pre-offer message.")
 | |
|         
 | |
|         // Determine whether the app is active based on the prefs rather than the UIApplication state to avoid
 | |
|         // requiring main-thread execution
 | |
|         let isMainAppActive: Bool = (UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false)
 | |
|         
 | |
|         // It is enough just ignoring the pre offers, other call messages
 | |
|         // for this call would be dropped because of no Session call instance
 | |
|         guard
 | |
|             CurrentAppContext().isMainApp,
 | |
|             let sender: String = message.sender,
 | |
|             (try? Contact
 | |
|                 .filter(id: sender)
 | |
|                 .select(.isApproved)
 | |
|                 .asRequest(of: Bool.self)
 | |
|                 .fetchOne(db))
 | |
|                 .defaulting(to: false)
 | |
|         else { return }
 | |
|         guard let timestamp = message.sentTimestamp, TimestampUtils.isWithinOneMinute(timestamp: timestamp) else {
 | |
|             // Add missed call message for call offer messages from more than one minute
 | |
|             if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .missed) {
 | |
|                 let thread: SessionThread = try SessionThread
 | |
|                     .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
 | |
|                 
 | |
|                 if !interaction.wasRead {
 | |
|                     Environment.shared?.notificationsManager.wrappedValue?
 | |
|                         .notifyUser(
 | |
|                             db,
 | |
|                             forIncomingCall: interaction,
 | |
|                             in: thread,
 | |
|                             applicationState: (isMainAppActive ? .active : .background)
 | |
|                         )
 | |
|                 }
 | |
|             }
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         guard db[.areCallsEnabled] else {
 | |
|             if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .permissionDenied) {
 | |
|                 let thread: SessionThread = try SessionThread
 | |
|                     .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil)
 | |
|                 
 | |
|                 if !interaction.wasRead {
 | |
|                     Environment.shared?.notificationsManager.wrappedValue?
 | |
|                         .notifyUser(
 | |
|                             db,
 | |
|                             forIncomingCall: interaction,
 | |
|                             in: thread,
 | |
|                             applicationState: (isMainAppActive ? .active : .background)
 | |
|                         )
 | |
|                 }
 | |
|                 
 | |
|                 // Trigger the missed call UI if needed
 | |
|                 NotificationCenter.default.post(
 | |
|                     name: .missedCall,
 | |
|                     object: nil,
 | |
|                     userInfo: [ Notification.Key.senderId.rawValue: sender ]
 | |
|                 )
 | |
|             }
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         // Ensure we have a call manager before continuing
 | |
|         guard let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue else { return }
 | |
|         
 | |
|         // Ignore pre offer message after the same call instance has been generated
 | |
|         if let currentCall: CurrentCallProtocol = callManager.currentCall, currentCall.uuid == message.uuid {
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         guard callManager.currentCall == nil else {
 | |
|             try MessageReceiver.handleIncomingCallOfferInBusyState(db, message: message)
 | |
|             return
 | |
|         }
 | |
|         
 | |
|         let interaction: Interaction? = try MessageReceiver.insertCallInfoMessage(db, for: message)
 | |
|         
 | |
|         // Handle UI
 | |
|         callManager.showCallUIForCall(
 | |
|             caller: sender,
 | |
|             uuid: message.uuid,
 | |
|             mode: .answer,
 | |
|             interactionId: interaction?.id
 | |
|         )
 | |
|     }
 | |
|     
 | |
|     private static func handleOfferCallMessage(_ db: Database, message: CallMessage) {
 | |
|         SNLog("[Calls] Received offer message.")
 | |
|         
 | |
|         // Ensure we have a call manager before continuing
 | |
|         guard
 | |
|             let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
 | |
|             let currentCall: CurrentCallProtocol = callManager.currentCall,
 | |
|             currentCall.uuid == message.uuid,
 | |
|             let sdp: String = message.sdps.first
 | |
|         else { return }
 | |
|         
 | |
|         let sdpDescription: RTCSessionDescription = RTCSessionDescription(type: .offer, sdp: sdp)
 | |
|         currentCall.didReceiveRemoteSDP(sdp: sdpDescription)
 | |
|     }
 | |
|     
 | |
|     private static func handleAnswerCallMessage(_ db: Database, message: CallMessage) {
 | |
|         SNLog("[Calls] Received answer message.")
 | |
|         
 | |
|         guard
 | |
|             let currentWebRTCSession: WebRTCSession = WebRTCSession.current,
 | |
|             currentWebRTCSession.uuid == message.uuid,
 | |
|             let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
 | |
|             var currentCall: CurrentCallProtocol = callManager.currentCall,
 | |
|             currentCall.uuid == message.uuid,
 | |
|             let sender: String = message.sender
 | |
|         else { return }
 | |
|         
 | |
|         guard sender != getUserHexEncodedPublicKey(db) else {
 | |
|             guard !currentCall.hasStartedConnecting else { return }
 | |
|             
 | |
|             callManager.dismissAllCallUI()
 | |
|             callManager.reportCurrentCallEnded(reason: .answeredElsewhere)
 | |
|             return
 | |
|         }
 | |
|         guard let sdp: String = message.sdps.first else { return }
 | |
|         
 | |
|         let sdpDescription: RTCSessionDescription = RTCSessionDescription(type: .answer, sdp: sdp)
 | |
|         currentCall.hasStartedConnecting = true
 | |
|         currentCall.didReceiveRemoteSDP(sdp: sdpDescription)
 | |
|         callManager.handleAnswerMessage(message)
 | |
|     }
 | |
|     
 | |
|     private static func handleEndCallMessage(_ db: Database, message: CallMessage) {
 | |
|         SNLog("[Calls] Received end call message.")
 | |
|         
 | |
|         guard
 | |
|             WebRTCSession.current?.uuid == message.uuid,
 | |
|             let callManager: CallManagerProtocol = Environment.shared?.callManager.wrappedValue,
 | |
|             let currentCall: CurrentCallProtocol = callManager.currentCall,
 | |
|             currentCall.uuid == message.uuid,
 | |
|             let sender: String = message.sender
 | |
|         else { return }
 | |
|         
 | |
|         callManager.dismissAllCallUI()
 | |
|         callManager.reportCurrentCallEnded(
 | |
|             reason: (sender == getUserHexEncodedPublicKey(db) ?
 | |
|                 .declinedElsewhere :
 | |
|                 .remoteEnded
 | |
|             )
 | |
|         )
 | |
|     }
 | |
|     
 | |
|     // MARK: - Convenience
 | |
|     
 | |
|     public static func handleIncomingCallOfferInBusyState(
 | |
|         _ db: Database,
 | |
|         message: CallMessage,
 | |
|         using dependencies: Dependencies = Dependencies()
 | |
|     ) throws {
 | |
|         let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(state: .missed)
 | |
|         
 | |
|         guard
 | |
|             let caller: String = message.sender,
 | |
|             let messageInfoData: Data = try? JSONEncoder().encode(messageInfo),
 | |
|             let thread: SessionThread = try SessionThread.fetchOne(db, id: caller),
 | |
|             !thread.isMessageRequest(db)
 | |
|         else { return }
 | |
|         
 | |
|         SNLog("[Calls] Sending end call message because there is an ongoing call.")
 | |
|         
 | |
|         let messageSentTimestamp: Int64 = (
 | |
|             message.sentTimestamp.map { Int64($0) } ??
 | |
|             SnodeAPI.currentOffsetTimestampMs()
 | |
|         )
 | |
|         _ = try Interaction(
 | |
|             serverHash: message.serverHash,
 | |
|             messageUuid: message.uuid,
 | |
|             threadId: thread.id,
 | |
|             authorId: caller,
 | |
|             variant: .infoCall,
 | |
|             body: String(data: messageInfoData, encoding: .utf8),
 | |
|             timestampMs: messageSentTimestamp,
 | |
|             wasRead: SessionUtil.timestampAlreadyRead(
 | |
|                 threadId: thread.id,
 | |
|                 threadVariant: thread.variant,
 | |
|                 timestampMs: (messageSentTimestamp * 1000),
 | |
|                 userPublicKey: getUserHexEncodedPublicKey(db),
 | |
|                 openGroup: nil
 | |
|             )
 | |
|         )
 | |
|         .inserted(db)
 | |
|         
 | |
|         MessageSender.sendImmediate(
 | |
|             data: try MessageSender
 | |
|                 .preparedSendData(
 | |
|                     db,
 | |
|                     message: CallMessage(
 | |
|                         uuid: message.uuid,
 | |
|                         kind: .endCall,
 | |
|                         sdps: [],
 | |
|                         sentTimestampMs: nil // Explicitly nil as it's a separate message from above
 | |
|                     ),
 | |
|                     to: try Message.Destination.from(db, threadId: thread.id, threadVariant: thread.variant),
 | |
|                     namespace: try Message.Destination
 | |
|                         .from(db, threadId: thread.id, threadVariant: thread.variant)
 | |
|                         .defaultNamespace,
 | |
|                     interactionId: nil,      // Explicitly nil as it's a separate message from above
 | |
|                     using: dependencies
 | |
|                 ),
 | |
|             using: dependencies
 | |
|         )
 | |
|         .subscribe(on: DispatchQueue.global(qos: .userInitiated))
 | |
|         .sinkUntilComplete()
 | |
|     }
 | |
|     
 | |
|     @discardableResult public static func insertCallInfoMessage(
 | |
|         _ db: Database,
 | |
|         for message: CallMessage,
 | |
|         state: CallMessage.MessageInfo.State? = nil
 | |
|     ) throws -> Interaction? {
 | |
|         guard
 | |
|             (try? Interaction
 | |
|                 .filter(Interaction.Columns.variant == Interaction.Variant.infoCall)
 | |
|                 .filter(Interaction.Columns.messageUuid == message.uuid)
 | |
|                 .isEmpty(db))
 | |
|                 .defaulting(to: false),
 | |
|             let sender: String = message.sender,
 | |
|             let thread: SessionThread = try SessionThread.fetchOne(db, id: sender),
 | |
|             !thread.isMessageRequest(db)
 | |
|         else { return nil }
 | |
|         
 | |
|         let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
 | |
|         let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
 | |
|             state: state.defaulting(
 | |
|                 to: (sender == currentUserPublicKey ?
 | |
|                     .outgoing :
 | |
|                     .incoming
 | |
|                 )
 | |
|             )
 | |
|         )
 | |
|         let timestampMs: Int64 = (
 | |
|             message.sentTimestamp.map { Int64($0) } ??
 | |
|             SnodeAPI.currentOffsetTimestampMs()
 | |
|         )
 | |
|         
 | |
|         guard let messageInfoData: Data = try? JSONEncoder().encode(messageInfo) else { return nil }
 | |
|         
 | |
|         return try Interaction(
 | |
|             serverHash: message.serverHash,
 | |
|             messageUuid: message.uuid,
 | |
|             threadId: thread.id,
 | |
|             authorId: sender,
 | |
|             variant: .infoCall,
 | |
|             body: String(data: messageInfoData, encoding: .utf8),
 | |
|             timestampMs: timestampMs,
 | |
|             wasRead: SessionUtil.timestampAlreadyRead(
 | |
|                 threadId: thread.id,
 | |
|                 threadVariant: thread.variant,
 | |
|                 timestampMs: (timestampMs * 1000),
 | |
|                 userPublicKey: currentUserPublicKey,
 | |
|                 openGroup: nil
 | |
|             )
 | |
|         ).inserted(db)
 | |
|     }
 | |
| }
 |