mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1435 lines
64 KiB
Swift
1435 lines
64 KiB
Swift
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
|
|
}
|
|
}
|
|
|
|
enum CallSessionInternalState {
|
|
case ringing(id: Int64, accessHash: Int64, gAHash: Data, b: Data, versions: [String])
|
|
case accepting(id: Int64, accessHash: Int64, gAHash: Data, b: Data, disposable: Disposable)
|
|
case awaitingConfirmation(id: Int64, accessHash: Int64, gAHash: Data, b: Data, config: SecretChatEncryptionConfig)
|
|
case requesting(a: Data, disposable: Disposable)
|
|
case requested(id: Int64, accessHash: Int64, a: Data, gA: Data, config: SecretChatEncryptionConfig, remoteConfirmationTimestamp: Int32?)
|
|
case confirming(id: Int64, accessHash: Int64, key: Data, keyId: Int64, keyVisualHash: Data, 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)
|
|
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 .dropping:
|
|
return nil
|
|
case let .terminated(id, _, _, _, _):
|
|
return id
|
|
}
|
|
}
|
|
}
|
|
|
|
public typealias CallSessionInternalId = UUID
|
|
typealias CallSessionStableId = Int64
|
|
|
|
private final class StableIncomingUUIDs {
|
|
static let shared = Atomic<StableIncomingUUIDs>(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 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)
|
|
case active(id: CallId, key: Data, keyVisualHash: Data, connections: CallSessionConnectionSet, maxLayer: Int32, version: String, customParameters: String?, allowsP2P: Bool)
|
|
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 .requesting:
|
|
self = .requesting(ringing: false)
|
|
case .confirming:
|
|
self = .requesting(ringing: true)
|
|
case let .requested(_, _, _, _, _, remoteConfirmationTimestamp):
|
|
self = .requesting(ringing: remoteConfirmationTimestamp != nil)
|
|
case let .active(id, accessHash, _, key, _, keyVisualHash, connections, maxLayer, version, customParameters, allowsP2P):
|
|
self = .active(id: CallId(id: id, accessHash: accessHash), key: key, keyVisualHash: keyVisualHash, connections: connections, maxLayer: maxLayer, version: version, customParameters: customParameters, allowsP2P: allowsP2P)
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
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
|
|
var type: CallSession.CallType
|
|
var isVideoPossible: Bool
|
|
var state: CallSessionInternalState
|
|
let subscribers = Bag<(CallSession) -> Void>()
|
|
var signalingReceiver: (([Data]) -> Void)?
|
|
|
|
let signalingDisposables = DisposableSet()
|
|
|
|
let acknowledgeIncomingCallDisposable = MetaDisposable()
|
|
|
|
var isEmpty: Bool {
|
|
if case .terminated = self.state {
|
|
return self.subscribers.isEmpty
|
|
} else {
|
|
return false
|
|
}
|
|
}
|
|
|
|
init(peerId: PeerId, isOutgoing: Bool, type: CallSession.CallType, isVideoPossible: Bool, state: CallSessionInternalState) {
|
|
self.peerId = peerId
|
|
self.isOutgoing = isOutgoing
|
|
self.type = type
|
|
self.isVideoPossible = isVideoPossible
|
|
self.state = state
|
|
}
|
|
|
|
deinit {
|
|
self.acknowledgeIncomingCallDisposable.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<CallSession, NoError> {
|
|
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))
|
|
}
|
|
}
|
|
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) -> 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, type: isVideo ? .video : .audio, isVideoPossible: isVideoPossible, state: .ringing(id: stableId, accessHash: accessHash, gAHash: gAHash, b: b, versions: versions))
|
|
self.contexts[internalId] = context
|
|
let queue = self.queue
|
|
|
|
let requestSignal: Signal<Api.Bool, MTRpcError> = 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<String?, NoError>) {
|
|
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 .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)
|
|
}
|
|
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 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, _):
|
|
let acceptVersions = self.versions.map({ $0.version })
|
|
context.state = .accepting(id: id, accessHash: accessHash, gAHash: gAHash, b: b, 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):
|
|
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)
|
|
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 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):
|
|
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, 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 })
|
|
} 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, _):
|
|
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)
|
|
}
|
|
} 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, _, _, _, _, _, _, _, _, _):
|
|
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:
|
|
break
|
|
}
|
|
} else {
|
|
//assertionFailure()
|
|
}
|
|
}
|
|
case let .phoneCall(flags, id, _, _, _, _, gAOrB, keyFingerprint, callProtocol, connections, startDate, customParameters):
|
|
let allowsP2P = (flags & (1 << 5)) != 0
|
|
if let internalId = self.contextIdByStableId[id] {
|
|
if let context = self.contexts[internalId] {
|
|
switch context.state {
|
|
case .accepting, .active, .dropping, .requesting, .ringing, .terminated, .requested:
|
|
break
|
|
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)
|
|
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)
|
|
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):
|
|
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)
|
|
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):
|
|
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)
|
|
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) -> 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, type: isVideo ? .video : .audio, isVideoPossible: enableVideo || isVideo, state: .requesting(a: a, disposable: (requestCallSession(postbox: self.postbox, network: self.network, peerId: peerId, a: a, maxLayer: self.maxLayer, versions: self.filteredVersions(enableVideo: true), isVideo: isVideo) |> 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)
|
|
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<CallSessionManagerContext>?
|
|
|
|
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<String?, NoError>) {
|
|
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, internalId: CallSessionInternalId = CallSessionInternalId()) -> Signal<CallSessionInternalId, NoError> {
|
|
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) {
|
|
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 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<CallSession, NoError> {
|
|
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)
|
|
}
|
|
|
|
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<AcceptCallResult, NoError> {
|
|
return validatedEncryptionConfig(postbox: postbox, network: network)
|
|
|> mapToSignal { config -> Signal<AcceptCallResult, NoError> 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<Api.phone.PhoneCall?, NoError> in
|
|
return .single(nil)
|
|
}
|
|
|> mapToSignal { call -> Signal<AcceptCallResult, NoError> 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):
|
|
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))
|
|
} 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) -> Signal<RequestCallSessionResult, NoError> {
|
|
return validatedEncryptionConfig(postbox: postbox, network: network)
|
|
|> mapToSignal { config -> Signal<RequestCallSessionResult, NoError> in
|
|
return postbox.transaction { transaction -> Signal<RequestCallSessionResult, NoError> 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
|
|
}
|
|
|
|
return network.request(Api.functions.phone.requestCall(flags: callFlags, userId: inputUser, 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<RequestCallSessionResult, NoError> 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<Api.PhoneCall?, NoError> {
|
|
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<Api.phone.PhoneCall?, NoError> 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
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
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<Api.Updates?, NoError> 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))
|
|
}
|
|
}
|