// C o p y r i g h t © 2 0 2 2 R a n g e p r o o f P t y L t d . A l l r i g h t s r e s e r v e d .
import Foundation
import AVFAudio
import GRDB
import WebRTC
import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver {
public static func handleCallMessage (
_ db : Database ,
threadId : String ,
threadVariant : SessionThread . Variant ,
message : CallMessage ,
using dependencies : Dependencies
) throws {
// O n l y s u p p o r t c a l l s f r o m c o n t a c t t h r e a d s
guard threadVariant = = . contact else { return }
switch message . kind {
case . preOffer : try MessageReceiver . handleNewCallMessage ( db , message : message , using : dependencies )
case . offer : MessageReceiver . handleOfferCallMessage ( db , message : message )
case . answer : MessageReceiver . handleAnswerCallMessage ( db , message : message )
case . provisionalAnswer : break // TODO: I m p l e m e n t
case let . iceCandidates ( sdpMLineIndexes , sdpMids ) :
Singleton . callManager . handleICECandidates (
message : message ,
sdpMLineIndexes : sdpMLineIndexes ,
sdpMids : sdpMids
)
case . endCall : MessageReceiver . handleEndCallMessage ( db , message : message )
}
}
// MARK: - S p e c i f i c H a n d l i n g
private static func handleNewCallMessage ( _ db : Database , message : CallMessage , using dependencies : Dependencies ) throws {
SNLog ( " [Calls] Received pre-offer message with uuid: \( message . uuid ) . " )
// D e t e r m i n e w h e t h e r t h e a p p i s a c t i v e b a s e d o n t h e p r e f s r a t h e r t h a n t h e U I A p p l i c a t i o n s t a t e t o a v o i d
// r e q u i r i n g m a i n - t h r e a d e x e c u t i o n
let isMainAppActive : Bool = ( UserDefaults . sharedLokiProject ? [ . isMainAppActive ] ) . defaulting ( to : false )
// I t i s e n o u g h j u s t i g n o r i n g t h e p r e o f f e r s , o t h e r c a l l m e s s a g e s
// f o r t h i s c a l l w o u l d b e d r o p p e d b e c a u s e o f n o S e s s i o n c a l l i n s t a n c e
guard
Singleton . hasAppContext ,
Singleton . appContext . 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 ( timestampMs : timestamp ) else {
// A d d m i s s e d c a l l m e s s a g e f o r c a l l o f f e r m e s s a g e s f r o m m o r e t h a n o n e m i n u t e
SNLog ( " [Calls] Got an expired call offer message with uuid: \( message . uuid ) . Sent at \( message . sentTimestamp ) , now is \( Date ( ) . timeIntervalSince1970 * 1000 ) " )
if let interaction : Interaction = try MessageReceiver . insertCallInfoMessage ( db , for : message , state : . missed , using : dependencies ) {
let thread : SessionThread = try SessionThread . upsert (
db ,
id : sender ,
variant : . contact ,
values : . existingOrDefault ,
using : dependencies
)
if ! interaction . wasRead {
SessionEnvironment . shared ? . notificationsManager ?
. notifyUser (
db ,
forIncomingCall : interaction ,
in : thread ,
applicationState : ( isMainAppActive ? . active : . background )
)
}
}
return
}
guard db [ . areCallsEnabled ] && Permissions . microphone = = . granted else {
let state : CallMessage . MessageInfo . State = ( db [ . areCallsEnabled ] ? . permissionDeniedMicrophone : . permissionDenied )
SNLog ( " [MessageReceiver+Calls] Microphone permission is \( AVAudioSession . sharedInstance ( ) . recordPermission ) " )
if let interaction : Interaction = try MessageReceiver . insertCallInfoMessage ( db , for : message , state : state , using : dependencies ) {
let thread : SessionThread = try SessionThread . upsert (
db ,
id : sender ,
variant : . contact ,
values : . existingOrDefault ,
using : dependencies
)
if ! interaction . wasRead {
SessionEnvironment . shared ? . notificationsManager ?
. notifyUser (
db ,
forIncomingCall : interaction ,
in : thread ,
applicationState : ( isMainAppActive ? . active : . background )
)
}
// T r i g g e r t h e m i s s e d c a l l U I i f n e e d e d
NotificationCenter . default . post (
name : . missedCall ,
object : nil ,
userInfo : [ Notification . Key . senderId . rawValue : sender ]
)
}
return
}
// I g n o r e p r e o f f e r m e s s a g e a f t e r t h e s a m e c a l l i n s t a n c e h a s b e e n g e n e r a t e d
if let currentCall : CurrentCallProtocol = Singleton . callManager . currentCall , currentCall . uuid = = message . uuid {
SNLog ( " [MessageReceiver+Calls] Ignoring pre-offer message for call[ \( currentCall . uuid ) ] instance because it is already active. " )
return
}
guard Singleton . callManager . currentCall = = nil else {
try MessageReceiver . handleIncomingCallOfferInBusyState ( db , message : message )
return
}
let interaction : Interaction ? = try MessageReceiver . insertCallInfoMessage ( db , for : message , using : dependencies )
// H a n d l e U I
Singleton . 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. " )
// E n s u r e w e h a v e a c a l l m a n a g e r b e f o r e c o n t i n u i n g
guard
let currentCall : CurrentCallProtocol = Singleton . 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
Singleton . callManager . currentWebRTCSessionMatches ( callId : message . uuid ) ,
var currentCall : CurrentCallProtocol = Singleton . callManager . currentCall ,
currentCall . uuid = = message . uuid ,
let sender : String = message . sender
else { return }
guard sender != getUserHexEncodedPublicKey ( db ) else {
guard ! currentCall . hasStartedConnecting else { return }
Singleton . callManager . dismissAllCallUI ( )
Singleton . 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 )
Singleton . callManager . handleAnswerMessage ( message )
}
private static func handleEndCallMessage ( _ db : Database , message : CallMessage ) {
SNLog ( " [Calls] Received end call message. " )
guard
Singleton . callManager . currentWebRTCSessionMatches ( callId : message . uuid ) ,
let currentCall : CurrentCallProtocol = Singleton . callManager . currentCall ,
currentCall . uuid = = message . uuid ,
let sender : String = message . sender
else { return }
Singleton . callManager . dismissAllCallUI ( )
Singleton . callManager . reportCurrentCallEnded (
reason : ( sender = = getUserHexEncodedPublicKey ( db ) ?
. declinedElsewhere :
. remoteEnded
)
)
}
// MARK: - C o n v e n i e n c e
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 ,
threadVariant : thread . variant ,
authorId : caller ,
variant : . infoCall ,
body : String ( data : messageInfoData , encoding : . utf8 ) ,
timestampMs : messageSentTimestamp ,
wasRead : LibSession . timestampAlreadyRead (
threadId : thread . id ,
threadVariant : thread . variant ,
timestampMs : ( messageSentTimestamp * 1000 ) ,
userPublicKey : getUserHexEncodedPublicKey ( db ) ,
openGroup : nil ,
using : dependencies
) ,
expiresInSeconds : message . expiresInSeconds ,
expiresStartedAtMs : message . expiresStartedAtMs
)
. inserted ( db )
MessageSender . sendImmediate (
data : try MessageSender
. preparedSendData (
db ,
message : CallMessage (
uuid : message . uuid ,
kind : . endCall ,
sdps : [ ] ,
sentTimestampMs : nil // E x p l i c i t l y n i l a s i t ' s a s e p a r a t e m e s s a g e f r o m a b o v e
)
. with ( try ? thread . disappearingMessagesConfiguration
. fetchOne ( db ) ?
. forcedWithDisappearAfterReadIfNeeded ( )
) ,
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 , // E x p l i c i t l y n i l a s i t ' s a s e p a r a t e m e s s a g e f r o m a b o v e
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 ,
using dependencies : Dependencies
) throws -> Interaction ? {
guard (
try ? Interaction
. filter ( Interaction . Columns . variant = = Interaction . Variant . infoCall )
. filter ( Interaction . Columns . messageUuid = = message . uuid )
. isEmpty ( db )
) . defaulting ( to : false )
else { throw MessageReceiverError . duplicatedCall }
guard
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 ,
threadVariant : thread . variant ,
authorId : sender ,
variant : . infoCall ,
body : String ( data : messageInfoData , encoding : . utf8 ) ,
timestampMs : timestampMs ,
wasRead : LibSession . timestampAlreadyRead (
threadId : thread . id ,
threadVariant : thread . variant ,
timestampMs : ( timestampMs * 1000 ) ,
userPublicKey : currentUserPublicKey ,
openGroup : nil ,
using : dependencies
) ,
expiresInSeconds : message . expiresInSeconds ,
expiresStartedAtMs : message . expiresStartedAtMs
)
. inserted ( db )
}
}