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.
		
		
		
		
		
			
		
			
				
	
	
		
			254 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			254 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Swift
		
	
| // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
 | |
| 
 | |
| import Foundation
 | |
| import GRDB
 | |
| import WebRTC
 | |
| import SessionUtilitiesKit
 | |
| 
 | |
| extension MessageReceiver {
 | |
|     public static func handleCallMessage(_ db: Database, message: CallMessage) throws {
 | |
|         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.")
 | |
|         
 | |
|         // 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.fetchOne(db, id: sender))?.isApproved == true
 | |
|         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)
 | |
|                 
 | |
|                 Environment.shared?.notificationsManager.wrappedValue?
 | |
|                     .notifyUser(
 | |
|                         db,
 | |
|                         forIncomingCall: interaction,
 | |
|                         in: thread
 | |
|                     )
 | |
|             }
 | |
|             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)
 | |
|                 
 | |
|                 Environment.shared?.notificationsManager.wrappedValue?
 | |
|                     .notifyUser(
 | |
|                         db,
 | |
|                         forIncomingCall: interaction,
 | |
|                         in: thread
 | |
|                     )
 | |
|                 
 | |
|                 // 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) 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.")
 | |
|         
 | |
|         _ = try Interaction(
 | |
|             serverHash: message.serverHash,
 | |
|             messageUuid: message.uuid,
 | |
|             threadId: thread.id,
 | |
|             authorId: caller,
 | |
|             variant: .infoCall,
 | |
|             body: String(data: messageInfoData, encoding: .utf8),
 | |
|             timestampMs: (
 | |
|                 message.sentTimestamp.map { Int64($0) } ??
 | |
|                 Int64(floor(Date().timeIntervalSince1970 * 1000))
 | |
|             )
 | |
|         )
 | |
|         .inserted(db)
 | |
|         
 | |
|         try MessageSender
 | |
|             .sendNonDurably(
 | |
|                 db,
 | |
|                 message: CallMessage(
 | |
|                     uuid: message.uuid,
 | |
|                     kind: .endCall,
 | |
|                     sdps: [],
 | |
|                     sentTimestampMs: nil // Explicitly nil as it's a separate message from above
 | |
|                 ),
 | |
|                 interactionId: nil,      // Explicitly nil as it's a separate message from above
 | |
|                 in: thread
 | |
|             )
 | |
|             .retainUntilComplete()
 | |
|     }
 | |
|     
 | |
|     @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 messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
 | |
|             state: state.defaulting(
 | |
|                 to: (sender == getUserHexEncodedPublicKey(db) ?
 | |
|                     .outgoing :
 | |
|                     .incoming
 | |
|                 )
 | |
|             )
 | |
|         )
 | |
|         let timestampMs: Int64 = (
 | |
|             message.sentTimestamp.map { Int64($0) } ??
 | |
|             Int64(floor(Date().timeIntervalSince1970 * 1000))
 | |
|         )
 | |
|         
 | |
|         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
 | |
|         ).inserted(db)
 | |
|     }
 | |
| }
 |