mirror of
https://github.com/oxen-io/session-ios.git
synced 2023-12-13 21:30:14 +01:00
Cleaned up the Dependencies so that tests can run synchronously without having to custom set queues as much Sorted out the crypto and network dependencies to avoid needing weird dependency inheritance Fixed the flaky tests so they are no longer flaky Fixed some unexpected JobRunner behaviours Updated the CI config to use a local build directory for derivedData (now works with build tweaks)
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)
|
|
}
|
|
}
|