import Foundation import Postbox import MtProtoKit import SwiftSignalKit import TelegramApi private let minLayer: Int32 = 65 public enum CallSessionError: Equatable { case generic case privacyRestricted case notSupportedByPeer(isVideo: Bool) case serverProvided(text: String) case disconnected } public enum CallSessionEndedType { case hungUp case busy case missed } public enum CallSessionTerminationReason: Equatable { case ended(CallSessionEndedType) case error(CallSessionError) public static func ==(lhs: CallSessionTerminationReason, rhs: CallSessionTerminationReason) -> Bool { switch lhs { case let .ended(type): if case .ended(type) = rhs { return true } else { return false } case let .error(error): if case .error(error) = rhs { return true } else { return false } } } } public struct CallId: Equatable { public let id: Int64 public let accessHash: Int64 public init(id: Int64, accessHash: Int64) { self.id = id self.accessHash = accessHash } } public struct GroupCallReference: Equatable { public var id: Int64 public var accessHash: Int64 public init(id: Int64, accessHash: Int64) { self.id = id self.accessHash = accessHash } } extension GroupCallReference { init(_ apiGroupCall: Api.InputGroupCall) { switch apiGroupCall { case let .inputGroupCall(id, accessHash): self.init(id: id, accessHash: accessHash) } } } enum CallSessionInternalState { case ringing(id: Int64, accessHash: Int64, gAHash: Data, b: Data, versions: [String], conferenceCall: GroupCallReference?) case accepting(id: Int64, accessHash: Int64, gAHash: Data, b: Data, conferenceCall: GroupCallReference?, disposable: Disposable) case awaitingConfirmation(id: Int64, accessHash: Int64, gAHash: Data, b: Data, config: SecretChatEncryptionConfig) case requesting(a: Data, conferenceCall: GroupCallReference?, disposable: Disposable) case requested(id: Int64, accessHash: Int64, a: Data, gA: Data, config: SecretChatEncryptionConfig, remoteConfirmationTimestamp: Int32?, conferenceCall: GroupCallReference?) case confirming(id: Int64, accessHash: Int64, key: Data, keyId: Int64, keyVisualHash: Data, conferenceCall: GroupCallReference?, disposable: Disposable) case active(id: Int64, accessHash: Int64, beginTimestamp: Int32, key: Data, keyId: Int64, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, conferenceCall: GroupCallReference?) case switchedToConference(key: Data, keyVisualHash: Data, conferenceCall: GroupCallReference) case dropping(reason: CallSessionTerminationReason, disposable: Disposable) case terminated(id: Int64?, accessHash: Int64?, reason: CallSessionTerminationReason, reportRating: Bool, sendDebugLogs: Bool) var stableId: Int64? { switch self { case let .ringing(id, _, _, _, _, _): return id case let .accepting(id, _, _, _, _, _): return id case let .awaitingConfirmation(id, _, _, _, _): return id case .requesting: return nil case let .requested(id, _, _, _, _, _, _): return id case let .confirming(id, _, _, _, _, _, _): return id case let .active(id, _, _, _, _, _, _, _, _, _, _, _): return id case .switchedToConference: return nil case .dropping: return nil case let .terminated(id, _, _, _, _): return id } } } public typealias CallSessionInternalId = UUID typealias CallSessionStableId = Int64 private final class StableIncomingUUIDs { static let shared = Atomic(value: StableIncomingUUIDs()) private var dict: [Int64: UUID] = [:] private init() { } func get(id: Int64) -> UUID { if let value = self.dict[id] { return value } else { let value = UUID() self.dict[id] = value return value } } } public struct CallSessionRingingState: Equatable { public let id: CallSessionInternalId public let peerId: PeerId public let isVideo: Bool public let isVideoPossible: Bool public let isConference: Bool } public enum DropCallReason { case hangUp case busy case disconnect case missed } public struct CallTerminationOptions: OptionSet { public let rawValue: Int public init(rawValue: Int) { self.rawValue = rawValue } public init() { self.rawValue = 0 } public static let reportRating = CallTerminationOptions(rawValue: 1 << 0) public static let sendDebugLogs = CallTerminationOptions(rawValue: 1 << 1) } public enum CallSessionState { case ringing case accepting case requesting(ringing: Bool, conferenceCall: GroupCallReference?) case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, conferenceCall: GroupCallReference?) case switchedToConference(key: Data, keyVisualHash: Data, conferenceCall: GroupCallReference) case dropping(reason: CallSessionTerminationReason) case terminated(id: CallId?, reason: CallSessionTerminationReason, options: CallTerminationOptions) fileprivate init(_ context: CallSessionContext) { switch context.state { case .ringing: self = .ringing case .accepting, .awaitingConfirmation: self = .accepting case let .requesting(_, conferenceCall, _): self = .requesting(ringing: false, conferenceCall: conferenceCall) case let .confirming(_, _, _, _, _, conferenceCall, _): self = .requesting(ringing: true, conferenceCall: conferenceCall) case let .requested(_, _, _, _, _, remoteConfirmationTimestamp, conferenceCall): self = .requesting(ringing: remoteConfirmationTimestamp != nil, conferenceCall: conferenceCall) case let .active(id, accessHash, _, key, _, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall): self = .active(id: CallId(id: id, accessHash: accessHash), key: key, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: conferenceCall) case let .dropping(reason, _): self = .dropping(reason: reason) case let .terminated(id, accessHash, reason, reportRating, sendDebugLogs): var options = CallTerminationOptions() if reportRating { options.insert(.reportRating) } if sendDebugLogs { options.insert(.sendDebugLogs) } let callId: CallId? if let id = id, let accessHash = accessHash { callId = CallId(id: id, accessHash: accessHash) } else { callId = nil } self = .terminated(id: callId, reason: reason, options: options) case let .switchedToConference(key, keyVisualHash, conferenceCall): self = .switchedToConference(key: key, keyVisualHash: keyVisualHash, conferenceCall: conferenceCall) } } } public struct CallSession { public enum CallType { case audio case video } public let id: CallSessionInternalId public let stableId: Int64? public let isOutgoing: Bool public let type: CallType public let state: CallSessionState public let isVideoPossible: Bool init( id: CallSessionInternalId, stableId: Int64?, isOutgoing: Bool, type: CallType, state: CallSessionState, isVideoPossible: Bool ) { self.id = id self.stableId = stableId self.isOutgoing = isOutgoing self.type = type self.state = state self.isVideoPossible = isVideoPossible } } public enum CallSessionConnection: Equatable { public struct Reflector: Equatable { public let id: Int64 public let ip: String public let ipv6: String public let isTcp: Bool public let port: Int32 public let peerTag: Data public init( id: Int64, ip: String, ipv6: String, isTcp: Bool, port: Int32, peerTag: Data ) { self.id = id self.ip = ip self.ipv6 = ipv6 self.isTcp = isTcp self.port = port self.peerTag = peerTag } } public struct WebRtcReflector: Equatable { public let id: Int64 public let hasStun: Bool public let hasTurn: Bool public let ip: String public let ipv6: String public let port: Int32 public let username: String public let password: String public init( id: Int64, hasStun: Bool, hasTurn: Bool, ip: String, ipv6: String, port: Int32, username: String, password: String ) { self.id = id self.hasStun = hasStun self.hasTurn = hasTurn self.ip = ip self.ipv6 = ipv6 self.port = port self.username = username self.password = password } } case reflector(Reflector) case webRtcReflector(WebRtcReflector) } private func parseConnection(_ apiConnection: Api.PhoneConnection) -> CallSessionConnection { switch apiConnection { case let .phoneConnection(flags, id, ip, ipv6, port, peerTag): let isTcp = (flags & (1 << 0)) != 0 return .reflector(CallSessionConnection.Reflector(id: id, ip: ip, ipv6: ipv6, isTcp: isTcp, port: port, peerTag: peerTag.makeData())) case let .phoneConnectionWebrtc(flags, id, ip, ipv6, port, username, password): return .webRtcReflector(CallSessionConnection.WebRtcReflector( id: id, hasStun: (flags & (1 << 1)) != 0, hasTurn: (flags & (1 << 0)) != 0, ip: ip, ipv6: ipv6, port: port, username: username, password: password )) } } public struct CallSessionConnectionSet { public let primary: CallSessionConnection public let alternatives: [CallSessionConnection] public init(primary: CallSessionConnection, alternatives: [CallSessionConnection]) { self.primary = primary self.alternatives = alternatives } } private func parseConnectionSet(primary: Api.PhoneConnection, alternative: [Api.PhoneConnection]) -> CallSessionConnectionSet { return CallSessionConnectionSet(primary: parseConnection(primary), alternatives: alternative.map { parseConnection($0) }) } private final class CallSessionContext { let peerId: PeerId let isOutgoing: Bool let isConference: Bool var type: CallSession.CallType var isVideoPossible: Bool let pendingConference: (conference: GroupCallReference, encryptionKey: Data)? var state: CallSessionInternalState let subscribers = Bag<(CallSession) -> Void>() var signalingReceiver: (([Data]) -> Void)? let signalingDisposables = DisposableSet() var createConferenceCallDisposable: Disposable? let acknowledgeIncomingCallDisposable = MetaDisposable() var isEmpty: Bool { if case .terminated = self.state { return self.subscribers.isEmpty } else { return false } } init(peerId: PeerId, isOutgoing: Bool, isConference: Bool, type: CallSession.CallType, isVideoPossible: Bool, pendingConference: (conference: GroupCallReference, encryptionKey: Data)?, state: CallSessionInternalState) { self.peerId = peerId self.isOutgoing = isOutgoing self.isConference = isConference self.type = type self.isVideoPossible = isVideoPossible self.pendingConference = pendingConference self.state = state } deinit { self.signalingDisposables.dispose() self.acknowledgeIncomingCallDisposable.dispose() self.createConferenceCallDisposable?.dispose() } } private func selectVersionOnAccept(localVersions: [CallSessionManagerImplementationVersion], remoteVersions: [String]) -> [String]? { let filteredVersions = localVersions.map({ $0.version }).filter(remoteVersions.contains) if filteredVersions.isEmpty { return nil } else { return [filteredVersions[0]] } } public struct CallSessionManagerImplementationVersion: Hashable { public var version: String public var supportsVideo: Bool public init(version: String, supportsVideo: Bool) { self.version = version self.supportsVideo = supportsVideo } } private final class CallSessionManagerContext { private let queue: Queue private let postbox: Postbox private let network: Network private let accountPeerId: PeerId private let maxLayer: Int32 private var versions: [CallSessionManagerImplementationVersion] private let addUpdates: (Api.Updates) -> Void private let ringingSubscribers = Bag<([CallSessionRingingState]) -> Void>() private var contexts: [CallSessionInternalId: CallSessionContext] = [:] private var futureContextSubscribers: [CallSessionInternalId: Bag<(CallSession) -> Void>] = [:] private var contextIdByStableId: [CallSessionStableId: CallSessionInternalId] = [:] private var enqueuedSignalingData: [Int64: [Data]] = [:] private let disposables = DisposableSet() init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, maxLayer: Int32, versions: [CallSessionManagerImplementationVersion], addUpdates: @escaping (Api.Updates) -> Void) { self.queue = queue self.postbox = postbox self.network = network self.accountPeerId = accountPeerId self.maxLayer = maxLayer self.versions = versions.reversed() self.addUpdates = addUpdates } deinit { assert(self.queue.isCurrent()) self.disposables.dispose() } func updateVersions(versions: [CallSessionManagerImplementationVersion]) { self.versions = versions.reversed() } func filteredVersions(enableVideo: Bool) -> [String] { return self.versions.compactMap { version -> String? in if enableVideo { return version.version } else if !version.supportsVideo { return version.version } else { return nil } } } func videoVersions() -> [String] { return self.versions.compactMap { version -> String? in if version.supportsVideo { return version.version } else { return nil } } } func ringingStates() -> Signal<[CallSessionRingingState], NoError> { let queue = self.queue return Signal { [weak self] subscriber in let disposable = MetaDisposable() queue.async { if let strongSelf = self { let index = strongSelf.ringingSubscribers.add { next in subscriber.putNext(next) } subscriber.putNext(strongSelf.ringingStatesValue()) disposable.set(ActionDisposable { queue.async { if let strongSelf = self { strongSelf.ringingSubscribers.remove(index) } } }) } } return disposable } } func callState(internalId: CallSessionInternalId) -> Signal { let queue = self.queue return Signal { [weak self] subscriber in let disposable = MetaDisposable() queue.async { guard let strongSelf = self else { return } if let context = strongSelf.contexts[internalId] { let index = context.subscribers.add { next in subscriber.putNext(next) } subscriber.putNext(CallSession(id: internalId, stableId: context.state.stableId, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context), isVideoPossible: context.isVideoPossible)) disposable.set(ActionDisposable { queue.async { if let strongSelf = self, let context = strongSelf.contexts[internalId] { context.subscribers.remove(index) if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) } } } }) } else { if strongSelf.futureContextSubscribers[internalId] == nil { strongSelf.futureContextSubscribers[internalId] = Bag() } let index = strongSelf.futureContextSubscribers[internalId]?.add({ session in subscriber.putNext(session) }) if let index = index { disposable.set(ActionDisposable { queue.async { guard let strongSelf = self else { return } strongSelf.futureContextSubscribers[internalId]?.remove(index) } }) } } } return disposable } } func beginReceivingCallSignalingData(internalId: CallSessionInternalId, _ receiver: @escaping ([Data]) -> Void) -> Disposable { let queue = self.queue let disposable = MetaDisposable() queue.async { [weak self] in if let strongSelf = self, let context = strongSelf.contexts[internalId] { context.signalingReceiver = receiver for (listStableId, listInternalId) in strongSelf.contextIdByStableId { if listInternalId == internalId { strongSelf.deliverCallSignalingData(id: listStableId) break } } disposable.set(ActionDisposable { queue.async { if let strongSelf = self, let context = strongSelf.contexts[internalId] { context.signalingReceiver = nil } } }) } } return disposable } private func ringingStatesValue() -> [CallSessionRingingState] { var ringingContexts: [CallSessionRingingState] = [] for (id, context) in self.contexts { if case .ringing = context.state { ringingContexts.append(CallSessionRingingState( id: id, peerId: context.peerId, isVideo: context.type == .video, isVideoPossible: context.isVideoPossible, isConference: context.isConference )) } } return ringingContexts } private func ringingStatesUpdated() { let states = self.ringingStatesValue() for subscriber in self.ringingSubscribers.copyItems() { subscriber(states) } } private func contextUpdated(internalId: CallSessionInternalId) { if let context = self.contexts[internalId] { let session = CallSession(id: internalId, stableId: context.state.stableId, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context), isVideoPossible: context.isVideoPossible) for subscriber in context.subscribers.copyItems() { subscriber(session) } if let futureSubscribers = self.futureContextSubscribers[internalId] { for subscriber in futureSubscribers.copyItems() { subscriber(session) } } } } private func addIncoming(peerId: PeerId, stableId: CallSessionStableId, accessHash: Int64, timestamp: Int32, gAHash: Data, versions: [String], isVideo: Bool, conferenceCall: GroupCallReference?) -> CallSessionInternalId? { if self.contextIdByStableId[stableId] != nil { return nil } let bBytes = malloc(256)! let randomStatus = SecRandomCopyBytes(nil, 256, bBytes.assumingMemoryBound(to: UInt8.self)) let b = Data(bytesNoCopy: bBytes, count: 256, deallocator: .free) if randomStatus == 0 { var isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) //#if DEBUG isVideoPossible = true //#endif let internalId = CallSessionManager.getStableIncomingUUID(stableId: stableId) let context = CallSessionContext(peerId: peerId, isOutgoing: false, isConference: conferenceCall != nil, type: isVideo ? .video : .audio, isVideoPossible: isVideoPossible, pendingConference: nil, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions, conferenceCall: conferenceCall)) self.contexts[internalId] = context let queue = self.queue let requestSignal: Signal = self.network.request(Api.functions.phone.receivedCall(peer: .inputPhoneCall(id: stableId, accessHash: accessHash))) context.acknowledgeIncomingCallDisposable.set(requestSignal.start(error: { [weak self] _ in queue.async { guard let strongSelf = self else { return } context.state = .terminated(id: nil, accessHash: nil, reason: .ended(.missed), reportRating: false, sendDebugLogs: false) strongSelf.contextUpdated(internalId: internalId) strongSelf.ringingStatesUpdated() if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) } strongSelf.contextIdByStableId.removeValue(forKey: stableId) } })) self.contextIdByStableId[stableId] = internalId self.contextUpdated(internalId: internalId) self.deliverCallSignalingData(id: stableId) self.ringingStatesUpdated() return internalId } else { return nil } } func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { if let context = self.contexts[internalId] { var dropData: (CallSessionStableId, Int64, DropCallSessionReason)? var wasRinging = false let isVideo = context.type == .video switch context.state { case let .ringing(id, accessHash, _, _, _, _): wasRinging = true let internalReason: DropCallSessionReason switch reason { case .busy: internalReason = .busy case .hangUp: internalReason = .hangUp(0) case .disconnect: internalReason = .disconnect case .missed: internalReason = .missed } dropData = (id, accessHash, internalReason) case let .accepting(id, accessHash, _, _, _, disposable): dropData = (id, accessHash, .abort) disposable.dispose() case let .active(id, accessHash, beginTimestamp, _, _, _, _, _, _, _, _, _): let duration = max(0, Int32(CFAbsoluteTimeGetCurrent()) - beginTimestamp) let internalReason: DropCallSessionReason switch reason { case .busy, .hangUp: internalReason = .hangUp(duration) case .disconnect: internalReason = .disconnect case .missed: internalReason = .missed } dropData = (id, accessHash, internalReason) case .switchedToConference: break case .dropping, .terminated: break case let .awaitingConfirmation(id, accessHash, _, _, _): dropData = (id, accessHash, .abort) case let .confirming(id, accessHash, _, _, _, _, disposable): disposable.dispose() dropData = (id, accessHash, .abort) case let .requested(id, accessHash, _, _, _, _, _): let internalReason: DropCallSessionReason switch reason { case .busy, .hangUp: internalReason = .missed case .disconnect: internalReason = .disconnect case .missed: internalReason = .missed } dropData = (id, accessHash, internalReason) case let .requesting(_, _, disposable): disposable.dispose() context.state = .terminated(id: nil, accessHash: nil, reason: .ended(.hungUp), reportRating: false, sendDebugLogs: false) self.contextUpdated(internalId: internalId) if context.isEmpty { self.contexts.removeValue(forKey: internalId) } } if let (id, accessHash, reason) = dropData { self.contextIdByStableId.removeValue(forKey: id) let mappedReason: CallSessionTerminationReason switch reason { case .abort: mappedReason = .ended(.hungUp) case .busy: mappedReason = .ended(.busy) case .disconnect: mappedReason = .error(.disconnected) case .hangUp: mappedReason = .ended(.hungUp) case .missed: mappedReason = .ended(.missed) case .switchToConference: mappedReason = .ended(.hungUp) } context.state = .dropping(reason: mappedReason, disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: reason) |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in if let strongSelf = self { if let context = strongSelf.contexts[internalId] { context.state = .terminated(id: id, accessHash: accessHash, reason: mappedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) /*if sendDebugLogs { let network = strongSelf.network let _ = (debugLog |> timeout(5.0, queue: strongSelf.queue, alternate: .single(nil)) |> deliverOnMainQueue).start(next: { debugLog in if let debugLog = debugLog { let _ = _internal_saveCallDebugLog(network: network, callId: CallId(id: id, accessHash: accessHash), log: debugLog).start() } }) }*/ strongSelf.contextUpdated(internalId: internalId) if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) } } } })) self.contextUpdated(internalId: internalId) if wasRinging { self.ringingStatesUpdated() } } } else { self.contextUpdated(internalId: internalId) } } func drop(stableId: CallSessionStableId, reason: DropCallReason) { if let internalId = self.contextIdByStableId[stableId] { self.contextIdByStableId.removeValue(forKey: stableId) self.drop(internalId: internalId, reason: reason, debugLog: .single(nil)) } } func dropToConference(internalId: CallSessionInternalId, encryptedGroupKey: Data) { if let context = self.contexts[internalId] { var dropData: (CallSessionStableId, Int64)? let isVideo = context.type == .video switch context.state { case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): dropData = (id, accessHash) default: break } if let (id, accessHash) = dropData { self.contextIdByStableId.removeValue(forKey: id) context.state = .dropping(reason: .ended(.hungUp), disposable: (dropCallSession(network: self.network, addUpdates: self.addUpdates, stableId: id, accessHash: accessHash, isVideo: isVideo, reason: .switchToConference(encryptedGroupKey: encryptedGroupKey)) |> deliverOn(self.queue)).start(next: { [weak self] reportRating, sendDebugLogs in if let strongSelf = self { if let context = strongSelf.contexts[internalId] { context.state = .terminated(id: id, accessHash: accessHash, reason: .ended(.hungUp), reportRating: reportRating, sendDebugLogs: sendDebugLogs) strongSelf.contextUpdated(internalId: internalId) if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) } } } })) self.contextUpdated(internalId: internalId) } } else { self.contextUpdated(internalId: internalId) } } func dropAll() { let contexts = self.contexts for (internalId, _) in contexts { self.drop(internalId: internalId, reason: .hangUp, debugLog: .single(nil)) } } func accept(internalId: CallSessionInternalId) { if let context = self.contexts[internalId] { switch context.state { case let .ringing(id, accessHash, gAHash, b, _, conferenceCall): let acceptVersions = self.versions.map({ $0.version }) context.state = .accepting(id: id, accessHash: accessHash, gAHash: gAHash, b: b, conferenceCall: conferenceCall, disposable: (acceptCallSession(accountPeerId: self.accountPeerId, postbox: self.postbox, network: self.network, stableId: id, accessHash: accessHash, b: b, maxLayer: self.maxLayer, versions: acceptVersions) |> deliverOn(self.queue)).start(next: { [weak self] result in if let strongSelf = self, let context = strongSelf.contexts[internalId] { if case .accepting = context.state { switch result { case .failed: strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) case let .success(call): switch call { case let .waiting(config): context.state = .awaitingConfirmation(id: id, accessHash: accessHash, gAHash: gAHash, b: b, config: config) strongSelf.contextUpdated(internalId: internalId) case let .call(config, gA, timestamp, connections, maxLayer, version, customParameters, allowsP2P, conferenceCall): if let (key, keyId, keyVisualHash) = strongSelf.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gA) { context.state = .active(id: id, accessHash: accessHash, beginTimestamp: timestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: conferenceCall) strongSelf.contextUpdated(internalId: internalId) } else { strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } } } } })) self.contextUpdated(internalId: internalId) self.ringingStatesUpdated() default: break } } } func sendSignalingData(internalId: CallSessionInternalId, data: Data) { if let context = self.contexts[internalId] { switch context.state { case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, _): context.signalingDisposables.add(self.network.request(Api.functions.phone.sendSignalingData(peer: .inputPhoneCall(id: id, accessHash: accessHash), data: Buffer(data: data))).start()) default: break } } } func createConferenceIfNecessary(internalId: CallSessionInternalId) { if let context = self.contexts[internalId] { if context.createConferenceCallDisposable != nil { return } var idAndAccessHash: (id: Int64, accessHash: Int64)? switch context.state { case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, conferenceCall): if conferenceCall != nil { return } idAndAccessHash = (id, accessHash) default: break } if let (id, accessHash) = idAndAccessHash { context.createConferenceCallDisposable = (createConferenceCall(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId, callId: CallId(id: id, accessHash: accessHash)) |> deliverOn(self.queue)).startStrict(next: { [weak self] result in guard let self else { return } guard let context = self.contexts[internalId] else { return } if let result { switch context.state { case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, _): context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: result) self.contextUpdated(internalId: internalId) default: break } } }) } } } func updateCallType(internalId: CallSessionInternalId, type: CallSession.CallType) { if let context = self.contexts[internalId] { context.type = type } } func updateSession(_ call: Api.PhoneCall, completion: @escaping ((CallSessionRingingState, CallSession)?) -> Void) { var resultRingingState: (CallSessionRingingState, CallSession)? switch call { case .phoneCallEmpty: break case let .phoneCallAccepted(_, id, _, _, _, _, gB, remoteProtocol, conferenceCall): let remoteVersions: [String] switch remoteProtocol { case let .phoneCallProtocol(_, _, _, versions): remoteVersions = versions } if let internalId = self.contextIdByStableId[id] { guard let selectedVersions = selectVersionOnAccept(localVersions: self.versions, remoteVersions: remoteVersions) else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) return } if let context = self.contexts[internalId] { switch context.state { case let .requested(_, accessHash, a, gA, config, _, _): let p = config.p.makeData() if !MTCheckIsSafeGAOrB(self.network.encryptionProvider, gA, p) { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } var key = MTExp(self.network.encryptionProvider, gB.makeData(), a, p)! if key.count > 256 { key.count = 256 } else { while key.count < 256 { key.insert(0, at: 0) } } let keyHash = MTSha1(key) var keyId: Int64 = 0 keyHash.withUnsafeBytes { rawBytes -> Void in let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) memcpy(&keyId, bytes.advanced(by: keyHash.count - 8), 8) } let keyVisualHash = MTSha256(key + gA) context.state = .confirming(id: id, accessHash: accessHash, key: key, keyId: keyId, keyVisualHash: keyVisualHash, conferenceCall: conferenceCall.flatMap(GroupCallReference.init), disposable: (confirmCallSession(network: self.network, stableId: id, accessHash: accessHash, gA: gA, keyFingerprint: keyId, maxLayer: self.maxLayer, versions: selectedVersions) |> deliverOnMainQueue).start(next: { [weak self] updatedCall in if let strongSelf = self, let context = strongSelf.contexts[internalId], case .confirming = context.state { if let updatedCall = updatedCall { strongSelf.updateSession(updatedCall, completion: { _ in }) if let pendingConference = context.pendingConference { strongSelf.dropToConference(internalId: internalId, encryptedGroupKey: pendingConference.encryptionKey) } } else { strongSelf.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } })) self.contextUpdated(internalId: internalId) default: self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } else { assertionFailure() } } case let .phoneCallDiscarded(flags, id, reason, _, conferenceCall): let _ = conferenceCall let reportRating = (flags & (1 << 2)) != 0 let sendDebugLogs = (flags & (1 << 3)) != 0 if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { let parsedReason: CallSessionTerminationReason if let reason = reason { switch reason { case .phoneCallDiscardReasonBusy: parsedReason = .ended(.busy) case .phoneCallDiscardReasonDisconnect: parsedReason = .error(.disconnected) case .phoneCallDiscardReasonHangup: parsedReason = .ended(.hungUp) case .phoneCallDiscardReasonMissed: parsedReason = .ended(.missed) case .phoneCallDiscardReasonAllowGroupCall: parsedReason = .ended(.hungUp) } } else { parsedReason = .ended(.hungUp) } switch context.state { case let .accepting(id, accessHash, _, _, _, disposable): disposable.dispose() context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) case let .active(id, accessHash, _, _, _, _, _, _, _, _, _, conferenceCall): if let conferenceCall, case let .phoneCallDiscardReasonAllowGroupCall(encryptedGroupKey) = reason { context.state = .switchedToConference(key: encryptedGroupKey.makeData(), keyVisualHash: MTSha256(encryptedGroupKey.makeData()), conferenceCall: conferenceCall) } else { context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) } self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(id, accessHash, _, _, _): context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) case let .requested(id, accessHash, _, _, _, _, _): context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) case let .confirming(id, accessHash, _, _, _, _, disposable): disposable.dispose() context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.contextUpdated(internalId: internalId) case let .requesting(_, _, disposable): disposable.dispose() context.state = .terminated(id: nil, accessHash: nil, reason: parsedReason, reportRating: false, sendDebugLogs: false) self.contextUpdated(internalId: internalId) case let .ringing(id, accessHash, _, _, _, _): context.state = .terminated(id: id, accessHash: accessHash, reason: parsedReason, reportRating: reportRating, sendDebugLogs: sendDebugLogs) self.ringingStatesUpdated() self.contextUpdated(internalId: internalId) case .dropping, .terminated, .switchedToConference: break } } else { //assertionFailure() } } case let .phoneCall(flags, id, _, _, _, _, gAOrB, keyFingerprint, callProtocol, connections, startDate, customParameters, conferenceCall): let allowsP2P = (flags & (1 << 5)) != 0 if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { switch context.state { case .accepting, .dropping, .requesting, .ringing, .terminated, .requested, .switchedToConference: break case let .active(id, accessHash, beginTimestamp, key, keyId, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P, _): context.state = .active(id: id, accessHash: accessHash, beginTimestamp: beginTimestamp, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) self.contextUpdated(internalId: internalId) case let .awaitingConfirmation(_, accessHash, gAHash, b, config): if let (key, calculatedKeyId, keyVisualHash) = self.makeSessionEncryptionKey(config: config, gAHash: gAHash, b: b, gA: gAOrB.makeData()) { if keyFingerprint == calculatedKeyId { switch callProtocol { case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { var customParametersValue: String? switch customParameters { case .none: break case let .dataJSON(data): customParametersValue = data } let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: calculatedKeyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } case let .confirming(id, accessHash, key, keyId, keyVisualHash, _, _): switch callProtocol { case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { var customParametersValue: String? switch customParameters { case .none: break case let .dataJSON(data): customParametersValue = data } let isVideoPossible = self.videoVersions().contains(where: { versions.contains($0) }) context.isVideoPossible = isVideoPossible context.state = .active(id: id, accessHash: accessHash, beginTimestamp: startDate, key: key, keyId: keyId, keyVisualHash: keyVisualHash, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: allowsP2P, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) self.contextUpdated(internalId: internalId) } else { self.drop(internalId: internalId, reason: .disconnect, debugLog: .single(nil)) } } } } else { assertionFailure() } } case let .phoneCallRequested(flags, id, accessHash, date, adminId, _, gAHash, requestedProtocol, conferenceCall): let isVideo = (flags & (1 << 6)) != 0 let versions: [String] switch requestedProtocol { case let .phoneCallProtocol(_, _, _, libraryVersions): versions = libraryVersions } if self.contextIdByStableId[id] == nil { let internalId = self.addIncoming(peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(adminId)), stableId: id, accessHash: accessHash, timestamp: date, gAHash: gAHash.makeData(), versions: versions, isVideo: isVideo, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) if let internalId = internalId { var resultRingingStateValue: CallSessionRingingState? for ringingState in self.ringingStatesValue() { if ringingState.id == internalId { resultRingingStateValue = ringingState break } } if let context = self.contexts[internalId] { let callSession = CallSession(id: internalId, stableId: id, isOutgoing: context.isOutgoing, type: context.type, state: CallSessionState(context), isVideoPossible: context.isVideoPossible) if let resultRingingStateValue = resultRingingStateValue { resultRingingState = (resultRingingStateValue, callSession) } } } } case let .phoneCallWaiting(_, id, _, _, _, _, _, receiveDate, conferenceCall): if let internalId = self.contextIdByStableId[id] { if let context = self.contexts[internalId] { switch context.state { case let .requested(id, accessHash, a, gA, config, remoteConfirmationTimestamp, _): if let receiveDate = receiveDate, remoteConfirmationTimestamp == nil { context.state = .requested(id: id, accessHash: accessHash, a: a, gA: gA, config: config, remoteConfirmationTimestamp: receiveDate, conferenceCall: conferenceCall.flatMap(GroupCallReference.init)) self.contextUpdated(internalId: internalId) } default: break } } else { assertionFailure() } } } completion(resultRingingState) } func addCallSignalingData(id: Int64, data: Data) { if self.enqueuedSignalingData[id] == nil { self.enqueuedSignalingData[id] = [] } self.enqueuedSignalingData[id]?.append(data) self.deliverCallSignalingData(id: id) } private func deliverCallSignalingData(id: Int64) { guard let internalId = self.contextIdByStableId[id], let context = self.contexts[internalId] else { return } if let signalingReceiver = context.signalingReceiver { if let data = self.enqueuedSignalingData.removeValue(forKey: id) { signalingReceiver(data) } } } private func makeSessionEncryptionKey(config: SecretChatEncryptionConfig, gAHash: Data, b: Data, gA: Data) -> (key: Data, keyId: Int64, keyVisualHash: Data)? { var key = MTExp(self.network.encryptionProvider, gA, b, config.p.makeData())! if key.count > 256 { key.count = 256 } else { while key.count < 256 { key.insert(0, at: 0) } } let keyHash = MTSha1(key) var keyId: Int64 = 0 keyHash.withUnsafeBytes { rawBytes -> Void in let bytes = rawBytes.baseAddress!.assumingMemoryBound(to: UInt8.self) memcpy(&keyId, bytes.advanced(by: keyHash.count - 8), 8) } if MTSha256(gA) != gAHash { return nil } let keyVisualHash = MTSha256(key + gA) return (key, keyId, keyVisualHash) } func request(peerId: PeerId, internalId: CallSessionInternalId, isVideo: Bool, enableVideo: Bool, conferenceCall: (conference: GroupCallReference, encryptionKey: Data)?) -> CallSessionInternalId? { let aBytes = malloc(256)! let randomStatus = SecRandomCopyBytes(nil, 256, aBytes.assumingMemoryBound(to: UInt8.self)) let a = Data(bytesNoCopy: aBytes, count: 256, deallocator: .free) if randomStatus == 0 { self.contexts[internalId] = CallSessionContext(peerId: peerId, isOutgoing: true, isConference: conferenceCall != nil, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, pendingConference: conferenceCall, state: .requesting(a: a, conferenceCall: conferenceCall?.conference, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: true), isVideo: isVideo, conferenceCall: conferenceCall?.conference) |> deliverOn(queue)).start(next: { [weak self] result in if let strongSelf = self, let context = strongSelf.contexts[internalId] { if case .requesting = context.state { switch result { case let .success(id, accessHash, config, gA, remoteConfirmationTimestamp): context.state = .requested(id: id, accessHash: accessHash, a: a, gA: gA, config: config, remoteConfirmationTimestamp: remoteConfirmationTimestamp, conferenceCall: conferenceCall?.conference) strongSelf.contextIdByStableId[id] = internalId strongSelf.contextUpdated(internalId: internalId) strongSelf.deliverCallSignalingData(id: id) case let .failed(error): context.state = .terminated(id: nil, accessHash: nil, reason: .error(error), reportRating: false, sendDebugLogs: false) strongSelf.contextUpdated(internalId: internalId) if context.isEmpty { strongSelf.contexts.removeValue(forKey: internalId) } } } } }))) self.contextUpdated(internalId: internalId) return internalId } else { return nil } } } public enum CallRequestError { case generic } public final class CallSessionManager { public static func getStableIncomingUUID(stableId: Int64) -> UUID { return StableIncomingUUIDs.shared.with { impl in return impl.get(id: stableId) } } private let queue = Queue() private var contextRef: Unmanaged? init(postbox: Postbox, network: Network, accountPeerId: PeerId, maxLayer: Int32, versions: [CallSessionManagerImplementationVersion], addUpdates: @escaping (Api.Updates) -> Void) { self.queue.async { let context = CallSessionManagerContext(queue: self.queue, postbox: postbox, network: network, accountPeerId: accountPeerId, maxLayer: maxLayer, versions: versions, addUpdates: addUpdates) self.contextRef = Unmanaged.passRetained(context) } } deinit { let contextRef = self.contextRef self.queue.async { contextRef?.release() } } private func withContext(_ f: @escaping (CallSessionManagerContext) -> Void) { self.queue.async { if let contextRef = self.contextRef { let context = contextRef.takeUnretainedValue() f(context) } } } func updateSession(_ call: Api.PhoneCall, completion: @escaping ((CallSessionRingingState, CallSession)?) -> Void) { self.withContext { context in context.updateSession(call, completion: completion) } } func addCallSignalingData(id: Int64, data: Data) { self.withContext { context in context.addCallSignalingData(id: id, data: data) } } public func drop(internalId: CallSessionInternalId, reason: DropCallReason, debugLog: Signal) { self.withContext { context in context.drop(internalId: internalId, reason: reason, debugLog: debugLog) } } func drop(stableId: CallSessionStableId, reason: DropCallReason) { self.withContext { context in context.drop(stableId: stableId, reason: reason) } } func dropAll() { self.withContext { context in context.dropAll() } } public func accept(internalId: CallSessionInternalId) { self.withContext { context in context.accept(internalId: internalId) } } public func request(peerId: PeerId, isVideo: Bool, enableVideo: Bool, conferenceCall: (conference: GroupCallReference, encryptionKey: Data)?, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal { return Signal { [weak self] subscriber in let disposable = MetaDisposable() self?.withContext { context in if let internalId = context.request(peerId: peerId, internalId: internalId, isVideo: isVideo, enableVideo: enableVideo, conferenceCall: conferenceCall) { subscriber.putNext(internalId) subscriber.putCompletion() } } return disposable } } public func sendSignalingData(internalId: CallSessionInternalId, data: Data) { self.withContext { context in context.sendSignalingData(internalId: internalId, data: data) } } public func createConferenceIfNecessary(internalId: CallSessionInternalId) { self.withContext { context in context.createConferenceIfNecessary(internalId: internalId) } } public func updateCallType(internalId: CallSessionInternalId, type: CallSession.CallType) { self.withContext { context in context.updateCallType(internalId: internalId, type: type) } } public func updateVersions(versions: [CallSessionManagerImplementationVersion]) { self.withContext { context in context.updateVersions(versions: versions) } } public func ringingStates() -> Signal<[CallSessionRingingState], NoError> { return Signal { [weak self] subscriber in let disposable = MetaDisposable() self?.withContext { context in disposable.set(context.ringingStates().start(next: { next in subscriber.putNext(next) })) } return disposable } } public func callState(internalId: CallSessionInternalId) -> Signal { return Signal { [weak self] subscriber in let disposable = MetaDisposable() self?.withContext { context in disposable.set(context.callState(internalId: internalId).start(next: { next in subscriber.putNext(next) })) } return disposable } } public func beginReceivingCallSignalingData(internalId: CallSessionInternalId, _ receiver: @escaping ([Data]) -> Void) -> Disposable { let disposable = MetaDisposable() self.withContext { context in disposable.set(context.beginReceivingCallSignalingData(internalId: internalId, receiver)) } return disposable } } private enum AcceptedCall { case waiting(config: SecretChatEncryptionConfig) case call(config: SecretChatEncryptionConfig, gA: Data, timestamp: Int32, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool, conferenceCall: GroupCallReference?) } private enum AcceptCallResult { case failed case success(AcceptedCall) } private func acceptCallSession(accountPeerId: PeerId, postbox: Postbox, network: Network, stableId: CallSessionStableId, accessHash: Int64, b: Data, maxLayer: Int32, versions: [String]) -> Signal { return validatedEncryptionConfig(postbox: postbox, network: network) |> mapToSignal { config -> Signal in var gValue: Int32 = config.g.byteSwapped let g = Data(bytes: &gValue, count: 4) let p = config.p.makeData() let bData = b let gb = MTExp(network.encryptionProvider, g, bData, p)! if !MTCheckIsSafeGAOrB(network.encryptionProvider, gb, p) { return .single(.failed) } return network.request(Api.functions.phone.acceptCall(peer: .inputPhoneCall(id: stableId, accessHash: accessHash), gB: Buffer(data: gb), protocol: .phoneCallProtocol(flags: (1 << 0) | (1 << 1), minLayer: minLayer, maxLayer: maxLayer, libraryVersions: versions))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { call -> Signal in if let call = call { return postbox.transaction { transaction -> AcceptCallResult in switch call { case let .phoneCall(phoneCall, users): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) switch phoneCall { case .phoneCallEmpty, .phoneCallRequested, .phoneCallAccepted, .phoneCallDiscarded: return .failed case .phoneCallWaiting: return .success(.waiting(config: config)) case let .phoneCall(flags, id, _, _, _, _, gAOrB, _, callProtocol, connections, startDate, customParameters, conferenceCall): if id == stableId { switch callProtocol{ case let .phoneCallProtocol(_, _, maxLayer, versions): if !versions.isEmpty { var customParametersValue: String? switch customParameters { case .none: break case let .dataJSON(data): customParametersValue = data } return .success(.call(config: config, gA: gAOrB.makeData(), timestamp: startDate, connections: parseConnectionSet(primary: connections.first!, alternative: Array(connections[1...])), maxLayer: maxLayer, version: versions[0], customParameters: customParametersValue, allowsP2P: (flags & (1 << 5)) != 0, conferenceCall: conferenceCall.flatMap(GroupCallReference.init))) } else { return .failed } } } else { return .failed } } } } } else { return .single(.failed) } } } } private enum RequestCallSessionResult { case success(id: CallSessionStableId, accessHash: Int64, config: SecretChatEncryptionConfig, gA: Data, remoteConfirmationTimestamp: Int32?) case failed(CallSessionError) } private func requestCallSession(postbox: Postbox, network: Network, peerId: PeerId, a: Data, maxLayer: Int32, versions: [String], isVideo: Bool, conferenceCall: GroupCallReference?) -> Signal { return validatedEncryptionConfig(postbox: postbox, network: network) |> mapToSignal { config -> Signal in return postbox.transaction { transaction -> Signal in if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) { var gValue: Int32 = config.g.byteSwapped let g = Data(bytes: &gValue, count: 4) let p = config.p.makeData() let ga = MTExp(network.encryptionProvider, g, a, p)! if !MTCheckIsSafeGAOrB(network.encryptionProvider, ga, p) { return .single(.failed(.generic)) } let gAHash = MTSha256(ga) var callFlags: Int32 = 0 if isVideo { callFlags |= 1 << 0 } if conferenceCall != nil { callFlags |= 1 << 1 } return network.request(Api.functions.phone.requestCall(flags: callFlags, userId: inputUser, conferenceCall: conferenceCall.flatMap { Api.InputGroupCall.inputGroupCall(id: $0.id, accessHash: $0.accessHash) }, randomId: Int32(bitPattern: arc4random()), gAHash: Buffer(data: gAHash), protocol: .phoneCallProtocol(flags: (1 << 0) | (1 << 1), minLayer: minLayer, maxLayer: maxLayer, libraryVersions: versions))) |> map { result -> RequestCallSessionResult in switch result { case let .phoneCall(phoneCall, _): switch phoneCall { case let .phoneCallRequested(_, id, accessHash, _, _, _, _, _, _): return .success(id: id, accessHash: accessHash, config: config, gA: ga, remoteConfirmationTimestamp: nil) case let .phoneCallWaiting(_, id, accessHash, _, _, _, _, receiveDate, _): return .success(id: id, accessHash: accessHash, config: config, gA: ga, remoteConfirmationTimestamp: receiveDate) default: return .failed(.generic) } } } |> `catch` { error -> Signal in switch error.errorDescription { case "PARTICIPANT_VERSION_OUTDATED": return .single(.failed(.notSupportedByPeer(isVideo: isVideo))) case "USER_PRIVACY_RESTRICTED": return .single(.failed(.privacyRestricted)) default: if error.errorCode == 406 { return .single(.failed(.serverProvided(text: error.errorDescription))) } else { return .single(.failed(.generic)) } } } } else { return .single(.failed(.generic)) } } |> switchToLatest } } private func confirmCallSession(network: Network, stableId: CallSessionStableId, accessHash: Int64, gA: Data, keyFingerprint: Int64, maxLayer: Int32, versions: [String]) -> Signal { return network.request(Api.functions.phone.confirmCall(peer: Api.InputPhoneCall.inputPhoneCall(id: stableId, accessHash: accessHash), gA: Buffer(data: gA), keyFingerprint: keyFingerprint, protocol: .phoneCallProtocol(flags: (1 << 0) | (1 << 1), minLayer: minLayer, maxLayer: maxLayer, libraryVersions: versions))) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> map { result -> Api.PhoneCall? in if let result = result { switch result { case let .phoneCall(phoneCall, _): return phoneCall } } else { return nil } } } private enum DropCallSessionReason { case abort case hangUp(Int32) case busy case disconnect case missed case switchToConference(encryptedGroupKey: Data) } private func dropCallSession(network: Network, addUpdates: @escaping (Api.Updates) -> Void, stableId: CallSessionStableId, accessHash: Int64, isVideo: Bool, reason: DropCallSessionReason) -> Signal<(Bool, Bool), NoError> { var mappedReason: Api.PhoneCallDiscardReason var duration: Int32 = 0 switch reason { case .abort: mappedReason = .phoneCallDiscardReasonHangup case let .hangUp(value): duration = value mappedReason = .phoneCallDiscardReasonHangup case .busy: mappedReason = .phoneCallDiscardReasonBusy case .disconnect: mappedReason = .phoneCallDiscardReasonDisconnect case .missed: mappedReason = .phoneCallDiscardReasonMissed case let .switchToConference(encryptedGroupKey): mappedReason = .phoneCallDiscardReasonAllowGroupCall(encryptedKey: Buffer(data: encryptedGroupKey)) } var callFlags: Int32 = 0 if isVideo { callFlags |= 1 << 0 } return network.request(Api.functions.phone.discardCall(flags: callFlags, peer: Api.InputPhoneCall.inputPhoneCall(id: stableId, accessHash: accessHash), duration: duration, reason: mappedReason, connectionId: 0)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { updates -> Signal<(Bool, Bool), NoError> in var reportRating: Bool = false var sendDebugLogs: Bool = false if let updates = updates { switch updates { case .updates(let updates, _, _, _, _): for update in updates { switch update { case .updatePhoneCall(let phoneCall): switch phoneCall { case let .phoneCallDiscarded(flags, _, _, _, _): reportRating = (flags & (1 << 2)) != 0 sendDebugLogs = (flags & (1 << 3)) != 0 default: break } break default: break } } default: break } addUpdates(updates) } return .single((reportRating, sendDebugLogs)) } } private func createConferenceCall(postbox: Postbox, network: Network, accountPeerId: PeerId, callId: CallId) -> Signal { return network.request(Api.functions.phone.createConferenceCall(peer: .inputPhoneCall(id: callId.id, accessHash: callId.accessHash), keyFingerprint: 1)) |> map(Optional.init) |> `catch` { _ -> Signal in return .single(nil) } |> mapToSignal { result -> Signal in guard let result else { return .single(nil) } return postbox.transaction { transaction -> GroupCallReference? in switch result { case let .phoneCall(phoneCall, users): updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users)) switch phoneCall { case .phoneCallEmpty, .phoneCallRequested, .phoneCallAccepted, .phoneCallDiscarded: return nil case .phoneCallWaiting: return nil case let .phoneCall(_, _, _, _, _, _, _, _, _, _, _, _, conferenceCall): if let conferenceCall { return GroupCallReference(conferenceCall) } else { return nil } } } } } }