import Foundation import Postbox import SwiftSignalKit import TelegramApi import MtProtoKit public enum AuthorizationCodeRequestError { case invalidPhoneNumber case limitExceeded case generic(info: (Int, String)?) case phoneLimitExceeded case phoneBanned case timeout case appOutdated } func switchToAuthorizedAccount(transaction: AccountManagerModifier, account: UnauthorizedAccount, isSupportUser: Bool) { let nextSortOrder = (transaction.getRecords().map({ record -> Int32 in for attribute in record.attributes { if case let .sortOrder(sortOrder) = attribute { return sortOrder.order } } return 0 }).max() ?? 0) + 1 transaction.updateRecord(account.id, { _ in var attributes: [TelegramAccountManagerTypes.Attribute] = [ .environment(AccountEnvironmentAttribute(environment: account.testingEnvironment ? .test : .production)), .sortOrder(AccountSortOrderAttribute(order: nextSortOrder)) ] if isSupportUser { attributes.append(.supportUserInfo(AccountSupportUserInfo())) } return AccountRecord(id: account.id, attributes: attributes, temporarySessionId: nil) }) transaction.setCurrentId(account.id) transaction.removeAuth() } private struct Regex { let pattern: String let options: NSRegularExpression.Options! private var matcher: NSRegularExpression { return try! NSRegularExpression(pattern: self.pattern, options: self.options) } init(_ pattern: String) { self.pattern = pattern self.options = [] } func match(_ string: String, options: NSRegularExpression.MatchingOptions = []) -> Bool { return self.matcher.numberOfMatches(in: string, options: options, range: NSMakeRange(0, string.utf16.count)) != 0 } } private protocol RegularExpressionMatchable { func match(_ regex: Regex) -> Bool } private struct MatchString: RegularExpressionMatchable { private let string: String init(_ string: String) { self.string = string } func match(_ regex: Regex) -> Bool { return regex.match(self.string) } } private func ~=(pattern: Regex, matchable: T) -> Bool { return matchable.match(pattern) } public enum SendAuthorizationCodeResult { case sentCode(UnauthorizedAccount) case loggedIn } func storeFutureLoginToken(accountManager: AccountManager, token: Data) { let _ = (accountManager.transaction { transaction -> Void in var tokens = transaction.getStoredLoginTokens() #if DEBUG tokens.removeAll() #endif var cloudValue: [Data] = [] if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { cloudValue = list.compactMap { string -> Data? in guard let stringData = string.data(using: .utf8) else { return nil } return Data(base64Encoded: stringData) } } for data in cloudValue { if !tokens.contains(data) { tokens.insert(data, at: 0) } } tokens.insert(token, at: 0) if tokens.count > 20 { tokens.removeLast(tokens.count - 20) } NSUbiquitousKeyValueStore.default.set(tokens.map { $0.base64EncodedString() }, forKey: "T_SLTokens") NSUbiquitousKeyValueStore.default.synchronize() transaction.setStoredLoginTokens(tokens) }).start() } public struct AuthorizationCodePushNotificationConfiguration { public var token: String public var isSandbox: Bool public init(token: String, isSandbox: Bool) { self.token = token self.isSandbox = isSandbox } } enum SendFirebaseAuthorizationCodeError { case generic } private func sendFirebaseAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, phoneCodeHash: String, timeout: Int32?, firebaseSecret: String, syncContacts: Bool) -> Signal { //auth.requestFirebaseSms#89464b50 flags:# phone_number:string phone_code_hash:string safety_net_token:flags.0?string ios_push_secret:flags.1?string = Bool; var flags: Int32 = 0 flags |= 1 << 1 return account.network.request(Api.functions.auth.requestFirebaseSms(flags: flags, phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, playIntegrityToken: nil, iosPushSecret: firebaseSecret)) |> mapError { _ -> SendFirebaseAuthorizationCodeError in return .generic } |> mapToSignal { result -> Signal in return .single(true) } } public func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>, syncContacts: Bool, disableAuthTokens: Bool = false, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { var cloudValue: [Data] = [] if let list = NSUbiquitousKeyValueStore.default.object(forKey: "T_SLTokens") as? [String] { cloudValue = list.compactMap { string -> Data? in guard let stringData = string.data(using: .utf8) else { return nil } return Data(base64Encoded: stringData) } } return accountManager.transaction { transaction -> [Data] in return transaction.getStoredLoginTokens() } |> castError(AuthorizationCodeRequestError.self) |> mapToSignal { localAuthTokens -> Signal in var authTokens = localAuthTokens for data in cloudValue { if !authTokens.contains(data) { authTokens.insert(data, at: 0) } } if disableAuthTokens { authTokens.removeAll() } #if DEBUG authTokens.removeAll() #endif var flags: Int32 = 0 flags |= 1 << 5 //allowMissedCall flags |= 1 << 6 //tokens var token: String? var appSandbox: Api.Bool? if let pushNotificationConfiguration = pushNotificationConfiguration { flags |= 1 << 7 flags |= 1 << 8 token = pushNotificationConfiguration.token appSandbox = pushNotificationConfiguration.isSandbox ? .boolTrue : .boolFalse } let sendCode = Api.functions.auth.sendCode(phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, settings: .codeSettings(flags: flags, logoutTokens: authTokens.map { Buffer(data: $0) }, token: token, appSandbox: appSandbox)) enum SendCodeResult { case password(hint: String?) case sentCode(Api.auth.SentCode) } let codeAndAccount = account.network.request(sendCode, automaticFloodWait: false) |> map { result -> (SendCodeResult, UnauthorizedAccount) in return (.sentCode(result), account) } |> `catch` { error -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in switch MatchString(error.errorDescription ?? "") { case Regex("(PHONE_|USER_|NETWORK_)MIGRATE_(\\d+)"): let range = error.errorDescription.range(of: "MIGRATE_")! let updatedMasterDatacenterId = Int32(error.errorDescription[range.upperBound ..< error.errorDescription.endIndex])! let updatedAccount = account.changedMasterDatacenterId(accountManager: accountManager, masterDatacenterId: updatedMasterDatacenterId) return updatedAccount |> mapToSignalPromotingError { updatedAccount -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in return updatedAccount.network.request(sendCode, automaticFloodWait: false) |> map { sentCode in return (.sentCode(sentCode), updatedAccount) } |> `catch` { error -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in if error.errorDescription == "SESSION_PASSWORD_NEEDED" { return updatedAccount.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) |> mapToSignal { result -> Signal<(SendCodeResult, UnauthorizedAccount), MTRpcError> in switch result { case let .password(_, _, _, _, hint, _, _, _, _, _, _): return .single((.password(hint: hint), updatedAccount)) } } } else { return .fail(error) } } } case _: return .fail(error) } } |> `catch` { error -> Signal<(SendCodeResult, UnauthorizedAccount), AuthorizationCodeRequestError> in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if error.errorDescription == "PHONE_NUMBER_INVALID" { return .fail(.invalidPhoneNumber) } else if error.errorDescription == "PHONE_NUMBER_FLOOD" { return .fail(.phoneLimitExceeded) } else if error.errorDescription == "PHONE_NUMBER_BANNED" { return .fail(.phoneBanned) } else if error.errorDescription == "UPDATE_APP_TO_LOGIN" { return .fail(.appOutdated) } else if error.errorDescription == "SESSION_PASSWORD_NEEDED" { return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else { return .generic(info: (Int(error.errorCode), error.errorDescription)) } } |> mapToSignal { result -> Signal<(SendCodeResult, UnauthorizedAccount), AuthorizationCodeRequestError> in switch result { case let .password(_, _, _, _, hint, _, _, _, _, _, _): return .single((.password(hint: hint), account)) } } } else { return .fail(.generic(info: (Int(error.errorCode), error.errorDescription))) } } |> timeout(20.0, queue: Queue.concurrentDefaultQueue(), alternate: .fail(.timeout)) return codeAndAccount |> mapToSignal { result, account -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { case let .password(hint): transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint ?? "", number: nil, code: nil, suggestReset: false, syncContacts: syncContacts))) return .single(.sentCode(account)) case let .sentCode(sentCode): switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): let parsedType = SentAuthorizationCodeType(apiType: type) var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = type { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { return nil } if let value = mapping[receipt] { return value } if receipt == "" && mapping.count == 1 { return mapping.first?.value } return nil } |> filter { $0 != nil } |> take(1) |> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil)) |> castError(AuthorizationCodeRequestError.self) |> mapToSignal { firebaseSecret -> Signal in guard let firebaseSecret = firebaseSecret else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: phoneNumber, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) |> `catch` { _ -> Signal in return .single(false) } |> mapError { _ -> AuthorizationCodeRequestError in return .generic(info: nil) } |> mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in var previousCodeEntry: UnauthorizedAccountStateContents? if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { if let previousCodeEntryValue { previousCodeEntry = previousCodeEntryValue } else { switch type { case .word, .phrase: previousCodeEntry = state.contents default: switch parsedType { case .word, .phrase: previousCodeEntry = state.contents default: break } } } } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } |> castError(AuthorizationCodeRequestError.self) } else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: phoneNumber, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } } } } var previousCodeEntry: UnauthorizedAccountStateContents? if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { if let previousCodeEntryValue { previousCodeEntry = previousCodeEntryValue } else { switch type { case .word, .phrase: previousCodeEntry = state.contents default: switch parsedType { case .word, .phrase: previousCodeEntry = state.contents default: break } } } } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) case let .sentCodeSuccess(authorization): switch authorization { case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user): if let futureAuthToken = futureAuthToken { storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) } let user = TelegramUser(user: user) var isSupportUser = false if let phone = user.phone, phone.hasPrefix("42") { isSupportUser = true } let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) { transaction.setNoticeEntry(key: value.0, value: value.1) } return accountManager.transaction { transaction -> SendAuthorizationCodeResult in switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) return .loggedIn } |> castError(AuthorizationCodeRequestError.self) case .authorizationSignUpRequired: return .never() } } return .single(.sentCode(account)) } } |> mapError { _ -> AuthorizationCodeRequestError in } |> switchToLatest } } } private func internalResendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, number: String, apiId: Int32, apiHash: String, hash: String, syncContacts: Bool, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal { return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription == "PHONE_NUMBER_INVALID" { return .invalidPhoneNumber } else if error.errorDescription == "PHONE_NUMBER_FLOOD" { return .phoneLimitExceeded } else if error.errorDescription == "PHONE_NUMBER_BANNED" { return .phoneBanned } else if error.errorDescription == "UPDATE_APP_TO_LOGIN" { return .appOutdated } else { return .generic(info: (Int(error.errorCode), error.errorDescription)) } } |> mapToSignal { sentCode -> Signal in return account.postbox.transaction { transaction -> Signal in switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): let parsedType = SentAuthorizationCodeType(apiType: type) var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = type { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { return nil } if let value = mapping[receipt] { return value } if receipt == "" && mapping.count == 1 { return mapping.first?.value } return nil } |> filter { $0 != nil } |> take(1) |> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil)) |> castError(AuthorizationCodeRequestError.self) |> mapToSignal { firebaseSecret -> Signal in guard let firebaseSecret = firebaseSecret else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: number, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) |> `catch` { _ -> Signal in return .single(false) } |> mapError { _ -> AuthorizationCodeRequestError in return .generic(info: nil) } |> mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in var previousCodeEntry: UnauthorizedAccountStateContents? if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { if let previousCodeEntryValue { previousCodeEntry = previousCodeEntryValue } else { switch type { case .word, .phrase: previousCodeEntry = state.contents default: switch parsedType { case .word, .phrase: previousCodeEntry = state.contents default: break } } } } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } |> castError(AuthorizationCodeRequestError.self) } else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } } } } var previousCodeEntry: UnauthorizedAccountStateContents? if let state = transaction.getState() as? UnauthorizedAccountState, case let .confirmationCodeEntry(_, type, _, _, _, _, previousCodeEntryValue, _) = state.contents { if let previousCodeEntryValue { previousCodeEntry = previousCodeEntryValue } else { switch type { case .word, .phrase: previousCodeEntry = state.contents default: switch parsedType { case .word, .phrase: previousCodeEntry = state.contents default: break } } } } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .single(.sentCode(account)) case .sentCodeSuccess: return .single(.loggedIn) } } |> mapError { _ -> AuthorizationCodeRequestError in } |> switchToLatest } } public func resendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(number, type, hash, _, nextType, syncContacts, previousCodeEntryValue, _): if nextType != nil { return account.network.request(Api.functions.auth.resendCode(phoneNumber: number, phoneCodeHash: hash), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeRequestError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription == "PHONE_NUMBER_INVALID" { return .invalidPhoneNumber } else if error.errorDescription == "PHONE_NUMBER_FLOOD" { return .phoneLimitExceeded } else if error.errorDescription == "PHONE_NUMBER_BANNED" { return .phoneBanned } else if error.errorDescription == "UPDATE_APP_TO_LOGIN" { return .appOutdated } else { return .generic(info: (Int(error.errorCode), error.errorDescription)) } } |> mapToSignal { sentCode -> Signal in return account.postbox.transaction { transaction -> Signal in switch sentCode { case let .sentCode(_, newType, phoneCodeHash, nextType, codeTimeout): let parsedType = SentAuthorizationCodeType(apiType: newType) var previousCodeEntry: UnauthorizedAccountStateContents? if let previousCodeEntryValue { previousCodeEntry = previousCodeEntryValue } else { switch type { case .word, .phrase: previousCodeEntry = state.contents default: switch parsedType { case .word, .phrase: previousCodeEntry = state.contents default: break } } } var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } if case let .sentCodeTypeFirebaseSms(_, _, receipt, pushTimeout, _) = newType { return firebaseSecretStream |> map { mapping -> String? in guard let receipt = receipt else { return nil } if let value = mapping[receipt] { return value } if receipt == "" && mapping.count == 1 { return mapping.first?.value } return nil } |> filter { $0 != nil } |> take(1) |> timeout(Double(pushTimeout ?? 15), queue: .mainQueue(), alternate: .single(nil)) |> castError(AuthorizationCodeRequestError.self) |> mapToSignal { firebaseSecret -> Signal in guard let firebaseSecret = firebaseSecret else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } return sendFirebaseAuthorizationCode(accountManager: accountManager, account: account, phoneNumber: number, apiId: apiId, apiHash: apiHash, phoneCodeHash: phoneCodeHash, timeout: codeTimeout, firebaseSecret: firebaseSecret, syncContacts: syncContacts) |> `catch` { _ -> Signal in return .single(false) } |> mapError { _ -> AuthorizationCodeRequestError in return .generic(info: nil) } |> mapToSignal { success -> Signal in if success { return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) return .sentCode(account) } |> castError(AuthorizationCodeRequestError.self) } else { return internalResendAuthorizationCode(accountManager: accountManager, account: account, number: number, apiId: apiId, apiHash: apiHash, hash: phoneCodeHash, syncContacts: syncContacts, firebaseSecretStream: firebaseSecretStream) } } } |> map { _ -> Void in return Void() } } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: parsedType, hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: false))) case .sentCodeSuccess: break } return .single(Void()) } |> mapError { _ -> AuthorizationCodeRequestError in } |> switchToLatest } } else { return .fail(.generic(info: nil)) } default: return .complete() } } else { return .fail(.generic(info: nil)) } } |> mapError { _ -> AuthorizationCodeRequestError in } |> switchToLatest } public enum AuthorizationCodeVerificationError { case invalidCode case limitExceeded case generic case codeExpired case invalidEmailToken case invalidEmailAddress } private enum AuthorizationCodeResult { case authorization(Api.auth.Authorization) case password(hint: String) case signUp } public enum AuthorizationCode: PostboxCoding, Equatable { private enum CodeType: Int32 { case phoneCode = 0 case emailCode = 1 case appleToken = 2 case googleToken = 3 } public enum EmailVerification: Equatable { case emailCode(String) case appleToken(String) case googleToken(String) } case phoneCode(String) case emailVerification(EmailVerification) public init(decoder: PostboxDecoder) { let type = decoder.decodeInt32ForKey("t", orElse: 0) switch type { case CodeType.phoneCode.rawValue: self = .phoneCode(decoder.decodeStringForKey("c", orElse: "")) case CodeType.emailCode.rawValue: self = .emailVerification(.emailCode(decoder.decodeStringForKey("c", orElse: ""))) case CodeType.appleToken.rawValue: self = .emailVerification(.appleToken(decoder.decodeStringForKey("c", orElse: ""))) case CodeType.googleToken.rawValue: self = .emailVerification(.googleToken(decoder.decodeStringForKey("c", orElse: ""))) default: assertionFailure() self = .phoneCode("") } } public func encode(_ encoder: PostboxEncoder) { switch self { case let .phoneCode(code): encoder.encodeInt32(CodeType.phoneCode.rawValue, forKey: "t") encoder.encodeString(code, forKey: "c") case let .emailVerification(verification): switch verification { case let .emailCode(code): encoder.encodeInt32(CodeType.emailCode.rawValue, forKey: "t") encoder.encodeString(code, forKey: "c") case let .appleToken(token): encoder.encodeInt32(CodeType.appleToken.rawValue, forKey: "t") encoder.encodeString(token, forKey: "c") case let .googleToken(token): encoder.encodeInt32(CodeType.googleToken.rawValue, forKey: "t") encoder.encodeString(token, forKey: "c") } } } } public struct AuthorizationSignUpData { let number: String let codeHash: String let code: AuthorizationCode let termsOfService: UnauthorizedAccountTermsOfService? let syncContacts: Bool } public enum AuthorizeWithCodeResult { case signUp(AuthorizationSignUpData) case loggedIn } public enum AuthorizationSendEmailCodeError { case generic case limitExceeded case codeExpired case timeout case invalidEmail case emailNotAllowed } public enum AuthorizationEmailVerificationError { case generic case limitExceeded case codeExpired case invalidCode case timeout case invalidEmailToken case emailNotAllowed } public struct ChangeLoginEmailData: Equatable { public let email: String public let length: Int32 } public func sendLoginEmailChangeCode(account: Account, email: String) -> Signal { return account.network.request(Api.functions.account.sendVerifyEmailCode(purpose: .emailVerifyPurposeLoginChange, email: email), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { return .fail(.codeExpired) } else if errorDescription.hasPrefix("EMAIL_INVALID") { return .fail(.invalidEmail) } else if errorDescription.hasPrefix("EMAIL_NOT_ALLOWED") { return .fail(.emailNotAllowed) } else { return .fail(.generic) } } |> map { result -> ChangeLoginEmailData in switch result { case let .sentEmailCode(_, length): return ChangeLoginEmailData(email: email, length: length) } } } public func sendLoginEmailCode(account: UnauthorizedAccount, email: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.account.sendVerifyEmailCode(purpose: .emailVerifyPurposeLoginSetup(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), email: email), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { return .fail(.codeExpired) } else if errorDescription.hasPrefix("EMAIL_INVALID") { return .fail(.invalidEmail) } else if errorDescription.hasPrefix("EMAIL_NOT_ALLOWED") { return .fail(.emailNotAllowed) } else { return .fail(.generic) } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { case let .sentEmailCode(emailPattern, length): transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: .email(emailPattern: emailPattern, length: length, resetAvailablePeriod: nil, resetPendingDate: nil, appleSignInAllowed: false, setup: true), hash: phoneCodeHash, timeout: nil, nextType: nil, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) } return .complete() } |> switchToLatest |> mapError { _ -> AuthorizationSendEmailCodeError in } |> ignoreValues } default: return .fail(.generic) } } else { return .fail(.generic) } } |> mapError { _ -> AuthorizationSendEmailCodeError in } |> switchToLatest |> ignoreValues } public func verifyLoginEmailChange(account: Account, code: AuthorizationCode.EmailVerification) -> Signal { let verification: Api.EmailVerification switch code { case let .emailCode(code): verification = .emailVerificationCode(code: code) case let .appleToken(token): verification = .emailVerificationApple(token: token) case let .googleToken(token): verification = .emailVerificationGoogle(token: token) } return account.network.request(Api.functions.account.verifyEmail(purpose: .emailVerifyPurposeLoginChange, verification: verification), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" || errorDescription == "EMAIL_VERIFY_EXPIRED" { return .fail(.codeExpired) } else if errorDescription == "CODE_INVALID" { return .fail(.invalidCode) } else if errorDescription == "EMAIL_TOKEN_INVALID" { return .fail(.invalidEmailToken) } else if errorDescription == "EMAIL_NOT_ALLOWED" { return .fail(.emailNotAllowed) } else { return .fail(.generic) } } |> mapToSignal { _ -> Signal in return .complete() } } public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: AuthorizationCode.EmailVerification) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): let verification: Api.EmailVerification switch code { case let .emailCode(code): verification = .emailVerificationCode(code: code) case let .appleToken(token): verification = .emailVerificationApple(token: token) case let .googleToken(token): verification = .emailVerificationGoogle(token: token) } return account.network.request(Api.functions.account.verifyEmail(purpose: .emailVerifyPurposeLoginSetup(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), verification: verification), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" || errorDescription == "EMAIL_VERIFY_EXPIRED" { return .fail(.codeExpired) } else if errorDescription == "CODE_INVALID" { return .fail(.invalidCode) } else if errorDescription == "EMAIL_TOKEN_INVALID" { return .fail(.invalidEmailToken) } else if errorDescription == "EMAIL_NOT_ALLOWED" { return .fail(.emailNotAllowed) } else { return .fail(.generic) } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { case let .emailVerifiedLogin(_, sentCode): switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, timeout): var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) case .sentCodeSuccess: break } case .emailVerified: break } return .complete() } |> switchToLatest |> mapError { _ -> AuthorizationEmailVerificationError in } |> ignoreValues } default: return .fail(.generic) } } else { return .fail(.generic) } } |> mapError { _ -> AuthorizationEmailVerificationError in } |> switchToLatest |> ignoreValues } public enum AuthorizationEmailResetError { case generic case limitExceeded case codeExpired case alreadyInProgress } public func resetLoginEmail(account: UnauthorizedAccount, phoneNumber: String, phoneCodeHash: String) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(phoneNumber, _, phoneCodeHash, _, _, syncContacts, _, _): return account.network.request(Api.functions.auth.resetLoginEmail(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash), automaticFloodWait: false) |> `catch` { error -> Signal in let errorDescription = error.errorDescription ?? "" if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { return .fail(.codeExpired) } else if errorDescription == "TASK_ALREADY_EXISTS" { return .fail(.alreadyInProgress) } else { return .fail(.generic) } } |> mapToSignal { sentCode -> Signal in return account.postbox.transaction { transaction -> Signal in switch sentCode { case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout): var parsedNextType: AuthorizationCodeNextType? if let nextType = nextType { parsedNextType = AuthorizationCodeNextType(apiType: nextType) } transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .confirmationCodeEntry(number: phoneNumber, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts, previousCodeEntry: nil, usePrevious: false))) return .complete() case .sentCodeSuccess: return .complete() } } |> switchToLatest |> mapError { _ -> AuthorizationEmailResetError in } |> ignoreValues } default: return .fail(.generic) } } else { return .fail(.generic) } } |> mapError { _ -> AuthorizationEmailResetError in } |> switchToLatest |> ignoreValues } public func authorizeWithCode(accountManager: AccountManager, account: UnauthorizedAccount, code: AuthorizationCode, termsOfService: UnauthorizedAccountTermsOfService?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts, _, _): var flags: Int32 = 0 var phoneCode: String? var emailVerification: Api.EmailVerification? switch code { case let .phoneCode(code): flags = 1 << 0 phoneCode = code case let .emailVerification(verification): flags = 1 << 1 switch verification { case let .emailCode(code): emailVerification = .emailVerificationCode(code: code) case let .appleToken(token): emailVerification = .emailVerificationApple(token: token) case let .googleToken(token): emailVerification = .emailVerificationGoogle(token: token) } } return account.network.request(Api.functions.auth.signIn(flags: flags, phoneNumber: number, phoneCodeHash: hash, phoneCode: phoneCode, emailVerification: emailVerification), automaticFloodWait: false) |> map { authorization in return .authorization(authorization) } |> `catch` { error -> Signal in switch (error.errorCode, error.errorDescription ?? "") { case (401, "SESSION_PASSWORD_NEEDED"): return account.network.request(Api.functions.account.getPassword(), automaticFloodWait: false) |> mapError { error -> AuthorizationCodeVerificationError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else { return .generic } } |> mapToSignal { result -> Signal in switch result { case let .password(_, _, _, _, hint, _, _, _, _, _, _): return .single(.password(hint: hint ?? "")) } } case let (_, errorDescription): if errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if errorDescription == "PHONE_CODE_INVALID" || errorDescription == "EMAIL_CODE_INVALID" { return .fail(.invalidCode) } else if errorDescription == "CODE_HASH_EXPIRED" || errorDescription == "PHONE_CODE_EXPIRED" { return .fail(.codeExpired) } else if errorDescription == "PHONE_NUMBER_UNOCCUPIED" { return .single(.signUp) } else if errorDescription == "EMAIL_TOKEN_INVALID" { return .fail(.invalidEmailToken) } else if errorDescription == "EMAIL_ADDRESS_INVALID" { return .fail(.invalidEmailAddress) } else { return .fail(.generic) } } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { case .signUp: return .single(.signUp(AuthorizationSignUpData(number: number, codeHash: hash, code: code, termsOfService: termsOfService, syncContacts: syncContacts))) case let .password(hint): transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .passwordEntry(hint: hint, number: number, code: code, suggestReset: false, syncContacts: syncContacts))) return .single(.loggedIn) case let .authorization(authorization): switch authorization { case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user): if let futureAuthToken = futureAuthToken { storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) } let user = TelegramUser(user: user) var isSupportUser = false if let phone = user.phone, phone.hasPrefix("42") { isSupportUser = true } let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) { transaction.setNoticeEntry(key: value.0, value: value.1) } return accountManager.transaction { transaction -> AuthorizeWithCodeResult in switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) return .loggedIn } case let .authorizationSignUpRequired(_, termsOfService): return .single(.signUp(AuthorizationSignUpData(number: number, codeHash: hash, code: code, termsOfService: termsOfService.flatMap(UnauthorizedAccountTermsOfService.init(apiTermsOfService:)), syncContacts: syncContacts))) } } } |> switchToLatest |> mapError { _ -> AuthorizationCodeVerificationError in } } default: return .fail(.generic) } } else { return .fail(.generic) } } |> mapError { _ -> AuthorizationCodeVerificationError in } |> switchToLatest } public func beginSignUp(account: UnauthorizedAccount, data: AuthorizationSignUpData) -> Signal { return account.postbox.transaction { transaction -> Void in transaction.setState(UnauthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, contents: .signUp(number: data.number, codeHash: data.codeHash, firstName: "", lastName: "", termsOfService: data.termsOfService, syncContacts: data.syncContacts))) } |> ignoreValues } public enum AuthorizationPasswordVerificationError { case limitExceeded case invalidPassword case generic } public func authorizeWithPassword(accountManager: AccountManager, account: UnauthorizedAccount, password: String, syncContacts: Bool) -> Signal { return verifyPassword(account, password: password) |> `catch` { error -> Signal in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .fail(.limitExceeded) } else if error.errorDescription == "PASSWORD_HASH_INVALID" { return .fail(.invalidPassword) } else { return .fail(.generic) } } |> mapToSignal { result -> Signal in return account.postbox.transaction { transaction -> Signal in switch result { case let .authorization(_, _, _, futureAuthToken, user): if let futureAuthToken = futureAuthToken { storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) } let user = TelegramUser(user: user) let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) /*transaction.updatePeersInternal([user], update: { current, peer -> Peer? in return peer })*/ var isSupportUser = false if let phone = user.phone, phone.hasPrefix("42") { isSupportUser = true } initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) return accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) } case .authorizationSignUpRequired: return .complete() } } |> switchToLatest |> mapError { _ -> AuthorizationPasswordVerificationError in } } } public enum PasswordRecoveryRequestError { case limitExceeded case generic } public enum PasswordRecoveryOption { case none case email(pattern: String) } public enum PasswordRecoveryError { case invalidCode case limitExceeded case expired case generic } func _internal_checkPasswordRecoveryCode(network: Network, code: String) -> Signal { return network.request(Api.functions.auth.checkRecoveryPassword(code: code), automaticFloodWait: false) |> mapError { error -> PasswordRecoveryError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_EXPIRED") { return .expired } else { return .invalidCode } } |> mapToSignal { result -> Signal in return .complete() } } public final class RecoveredAccountData { let authorization: Api.auth.Authorization init(authorization: Api.auth.Authorization) { self.authorization = authorization } } public func loginWithRecoveredAccountData(accountManager: AccountManager, account: UnauthorizedAccount, recoveredAccountData: RecoveredAccountData, syncContacts: Bool) -> Signal { return account.postbox.transaction { transaction -> Signal in switch recoveredAccountData.authorization { case let .authorization(_, _, _, futureAuthToken, user): if let futureAuthToken = futureAuthToken { storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) } let user = TelegramUser(user: user) var isSupportUser = false if let phone = user.phone, phone.hasPrefix("42") { isSupportUser = true } let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) return accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) } case .authorizationSignUpRequired: return .complete() } } |> switchToLatest |> ignoreValues } func _internal_performPasswordRecovery(network: Network, code: String, updatedPassword: UpdatedTwoStepVerificationPassword) -> Signal { return _internal_twoStepAuthData(network) |> mapError { _ -> PasswordRecoveryError in return .generic } |> mapToSignal { authData -> Signal in let newSettings: Api.account.PasswordInputSettings? switch updatedPassword { case .none: newSettings = nil case let .password(password, hint, email): var flags: Int32 = 1 << 0 if email != nil { flags |= (1 << 1) } guard let (updatedPasswordHash, updatedPasswordDerivation) = passwordUpdateKDF(encryptionProvider: network.encryptionProvider, password: password, derivation: authData.nextPasswordDerivation) else { return .fail(.invalidCode) } newSettings = Api.account.PasswordInputSettings.passwordInputSettings(flags: flags, newAlgo: updatedPasswordDerivation.apiAlgo, newPasswordHash: Buffer(data: updatedPasswordHash), hint: hint, email: email, newSecureSettings: nil) } var flags: Int32 = 0 if newSettings != nil { flags |= 1 << 0 } return network.request(Api.functions.auth.recoverPassword(flags: flags, code: code, newSettings: newSettings), automaticFloodWait: false) |> mapError { error -> PasswordRecoveryError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription.hasPrefix("PASSWORD_RECOVERY_EXPIRED") { return .expired } else { return .invalidCode } } |> mapToSignal { result -> Signal in return .single(RecoveredAccountData(authorization: result)) } } } func _internal_invalidateLoginCodes(network: Network, codes: [String]) -> Signal { return network.request(Api.functions.account.invalidateSignInCodes(codes: codes)) |> ignoreValues |> `catch` { _ -> Signal in return .never() } } public enum AccountResetError { case generic case limitExceeded } public func performAccountReset(account: UnauthorizedAccount) -> Signal { return account.network.request(Api.functions.account.deleteAccount(flags: 0, reason: "", password: nil)) |> map { _ -> Int32? in return nil } |> `catch` { error -> Signal in if error.errorDescription.hasPrefix("2FA_CONFIRM_WAIT_") { let timeout = String(error.errorDescription[error.errorDescription.index(error.errorDescription.startIndex, offsetBy: "2FA_CONFIRM_WAIT_".count)...]) if let value = Int32(timeout) { return .single(value) } else { return .fail(.generic) } } else if error.errorDescription == "2FA_RECENT_CONFIRM" { return .fail(.limitExceeded) } else { return .fail(.generic) } } |> mapToSignal { timeout -> Signal in return account.postbox.transaction { transaction -> Void in guard let state = transaction.getState() as? UnauthorizedAccountState else { return } var number: String? var syncContacts: Bool? if case let .passwordEntry(_, numberValue, _, _, syncContactsValue) = state.contents { number = numberValue syncContacts = syncContactsValue } else if case let .awaitingAccountReset(_, numberValue, syncContactsValue) = state.contents { number = numberValue syncContacts = syncContactsValue } if let number = number, let syncContacts = syncContacts { if let timeout = timeout { let timestamp = Int32(CFAbsoluteTimeGetCurrent() + NSTimeIntervalSince1970) transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .awaitingAccountReset(protectedUntil: timestamp + timeout, number: number, syncContacts: syncContacts))) } else { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .empty)) } } } |> mapError { _ -> AccountResetError in } } } public enum SignUpError { case generic case limitExceeded case codeExpired case invalidFirstName case invalidLastName } public func signUpWithName(accountManager: AccountManager, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal?, videoStartTimestamp: Double?, disableJoinNotifications: Bool = false, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState, case let .signUp(number, codeHash, _, _, _, syncContacts) = state.contents { var flags: Int32 = 0 if disableJoinNotifications { flags |= (1 << 0) } return account.network.request(Api.functions.auth.signUp(flags: flags, phoneNumber: number, phoneCodeHash: codeHash, firstName: firstName, lastName: lastName)) |> mapError { error -> SignUpError in if error.errorDescription.hasPrefix("FLOOD_WAIT") { return .limitExceeded } else if error.errorDescription == "PHONE_CODE_EXPIRED" { return .codeExpired } else if error.errorDescription == "FIRSTNAME_INVALID" { return .invalidFirstName } else if error.errorDescription == "LASTNAME_INVALID" { return .invalidLastName } else { return .generic } } |> mapToSignal { result -> Signal in switch result { case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user): if let futureAuthToken = futureAuthToken { storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData()) } let user = TelegramUser(user: user) var isSupportUser = false if let phone = user.phone, phone.hasPrefix("42") { isSupportUser = true } let appliedState = account.postbox.transaction { transaction -> Void in let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil, invalidatedChannels: []) if let hole = account.postbox.seedConfiguration.initializeChatListWithHole.topLevel { transaction.replaceChatListHole(groupId: .root, index: hole.index, hole: nil) } initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) if let otherwiseReloginDays = otherwiseReloginDays, let value = forcedPasswordSetupNotice(otherwiseReloginDays) { transaction.setNoticeEntry(key: value.0, value: value.1) } } |> castError(SignUpError.self) let switchedAccounts = accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account, isSupportUser: isSupportUser) } |> castError(SignUpError.self) if let avatarData = avatarData { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) account.postbox.mediaBox.storeResourceData(resource.id, data: avatarData) return _internal_updatePeerPhotoInternal(postbox: account.postbox, network: account.network, stateManager: nil, accountPeerId: user.id, peer: .single(user), photo: _internal_uploadedPeerPhoto(postbox: account.postbox, network: account.network, resource: resource), video: avatarVideo, videoStartTimestamp: videoStartTimestamp, mapResourceToAvatarSizes: { _, _ in .single([:]) }) |> `catch` { _ -> Signal in return .complete() } |> mapToSignal { result -> Signal in switch result { case .complete: return .complete() case .progress: return .never() } } |> then(appliedState) |> then(switchedAccounts) } else { return appliedState |> then(switchedAccounts) } case .authorizationSignUpRequired: return .fail(.generic) } } } else { return .fail(.generic) } } |> mapError { _ -> SignUpError in } |> switchToLatest } public enum AuthorizationStateReset { case empty } public func resetAuthorizationState(account: UnauthorizedAccount, to value: AuthorizationStateReset) -> Signal { return account.postbox.transaction { transaction -> Void in if let state = transaction.getState() as? UnauthorizedAccountState { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .empty)) } } } public func togglePreviousCodeEntry(account: UnauthorizedAccount) -> Signal { return account.postbox.transaction { transaction -> Void in if let state = transaction.getState() as? UnauthorizedAccountState { if case let .confirmationCodeEntry(number, type, hash, timeout, nextType, syncContacts, previousCodeEntry, usePrevious) = state.contents, let previousCodeEntry { transaction.setState(UnauthorizedAccountState(isTestingEnvironment: state.isTestingEnvironment, masterDatacenterId: state.masterDatacenterId, contents: .confirmationCodeEntry(number: number, type: type, hash: hash, timeout: timeout, nextType: nextType, syncContacts: syncContacts, previousCodeEntry: previousCodeEntry, usePrevious: !usePrevious))) } } } |> ignoreValues } public enum ReportMissingCodeError { case generic } func _internal_reportMissingCode(network: Network, phoneNumber: String, phoneCodeHash: String, mnc: String) -> Signal { return network.request(Api.functions.auth.reportMissingCode(phoneNumber: phoneNumber, phoneCodeHash: phoneCodeHash, mnc: mnc)) |> mapError { error -> ReportMissingCodeError in return .generic } |> mapToSignal { result -> Signal in return .complete() } }