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 } func switchToAuthorizedAccount(transaction: AccountManagerModifier, account: UnauthorizedAccount) { 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 return AccountRecord(id: account.id, attributes: [ .environment(AccountEnvironmentAttribute(environment: account.testingEnvironment ? .test : .production)), .sortOrder(AccountSortOrderAttribute(order: nextSortOrder)) ], 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 func sendAuthorizationCode(accountManager: AccountManager, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, syncContacts: Bool) -> Signal { let sendCode = Api.functions.auth.sendCode(phoneNumber: phoneNumber, apiId: apiId, apiHash: apiHash, settings: .codeSettings(flags: 0)) let codeAndAccount = account.network.request(sendCode, automaticFloodWait: false) |> map { result in return (result, account) } |> `catch` { error -> Signal<(Api.auth.SentCode, 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<(Api.auth.SentCode, UnauthorizedAccount), MTRpcError> in return updatedAccount.network.request(sendCode, automaticFloodWait: false) |> map { sentCode in return (sentCode, updatedAccount) } } case _: return .fail(error) } } |> 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 { return .generic(info: (Int(error.errorCode), error.errorDescription)) } } |> timeout(20.0, queue: Queue.concurrentDefaultQueue(), alternate: .fail(.timeout)) return codeAndAccount |> mapToSignal { (sentCode, account) -> Signal in return account.postbox.transaction { transaction -> UnauthorizedAccount in 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))) } return account } |> mapError { _ -> AuthorizationCodeRequestError in } } } public func resendAuthorizationCode(account: UnauthorizedAccount) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(number, _, hash, _, nextType, syncContacts): 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 { return .generic(info: (Int(error.errorCode), error.errorDescription)) } } |> mapToSignal { sentCode -> Signal in return account.postbox.transaction { transaction -> Void in 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: number, type: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: timeout, nextType: parsedNextType, syncContacts: syncContacts))) } } |> mapError { _ -> AuthorizationCodeRequestError in } } } 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 } private enum AuthorizationCodeResult { case authorization(Api.auth.Authorization) case password(hint: String) case signUp } public struct AuthorizationSignUpData { let number: String let codeHash: String let code: String let termsOfService: UnauthorizedAccountTermsOfService? let syncContacts: Bool } public enum AuthorizeWithCodeResult { case signUp(AuthorizationSignUpData) case loggedIn } public func authorizeWithCode(accountManager: AccountManager, account: UnauthorizedAccount, code: String, termsOfService: UnauthorizedAccountTermsOfService?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState { switch state.contents { case let .confirmationCodeEntry(number, _, hash, _, _, syncContacts): return account.network.request(Api.functions.auth.signIn(phoneNumber: number, phoneCodeHash: hash, phoneCode: code), 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" { 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 { 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(_, _, user): let user = TelegramUser(user: user) let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) return accountManager.transaction { transaction -> AuthorizeWithCodeResult in switchToAuthorizedAccount(transaction: transaction, account: account) 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(_, _, user): let user = TelegramUser(user: user) let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) /*transaction.updatePeersInternal([user], update: { current, peer -> Peer? in return peer })*/ initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) return accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account) } 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(_, _, user): let user = TelegramUser(user: user) let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) initializedAppSettingsAfterLogin(transaction: transaction, appVersion: account.networkArguments.appVersion, syncContacts: syncContacts) transaction.setState(state) return accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account) } 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)) } } } public enum AccountResetError { case generic case limitExceeded } public func performAccountReset(account: UnauthorizedAccount) -> Signal { return account.network.request(Api.functions.account.deleteAccount(reason: "")) |> 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?) -> Signal { return account.postbox.transaction { transaction -> Signal in if let state = transaction.getState() as? UnauthorizedAccountState, case let .signUp(number, codeHash, _, _, _, syncContacts) = state.contents { return account.network.request(Api.functions.auth.signUp(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(_, _, user): let user = TelegramUser(user: user) let appliedState = account.postbox.transaction { transaction -> Void in let state = AuthorizedAccountState(isTestingEnvironment: account.testingEnvironment, masterDatacenterId: account.masterDatacenterId, peerId: user.id, state: nil) 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) } |> castError(SignUpError.self) let switchedAccounts = accountManager.transaction { transaction -> Void in switchToAuthorizedAccount(transaction: transaction, account: account) } |> 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)) } } }