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.
		
		
		
		
		
			
		
			
				
	
	
		
			275 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Swift
		
	
			
		
		
	
	
			275 lines
		
	
	
		
			11 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.")
 | 
						|
        
 | 
						|
        // 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)
 | 
						|
                
 | 
						|
                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, shouldBeVisible: nil)
 | 
						|
                
 | 
						|
                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) } ??
 | 
						|
                SnodeAPI.currentOffsetTimestampMs()
 | 
						|
            )
 | 
						|
        )
 | 
						|
        .inserted(db)
 | 
						|
        
 | 
						|
        MessageSender.sendImmediate(
 | 
						|
            preparedSendData: 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
 | 
						|
                )
 | 
						|
        )
 | 
						|
        .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 messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
 | 
						|
            state: state.defaulting(
 | 
						|
                to: (sender == getUserHexEncodedPublicKey(db) ?
 | 
						|
                    .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
 | 
						|
        ).inserted(db)
 | 
						|
    }
 | 
						|
}
 |