mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-06-16 05:55:20 +00:00
1292 lines
70 KiB
Swift
1292 lines
70 KiB
Swift
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<TelegramAccountManagerTypes>, 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 ~=<T: RegularExpressionMatchable>(pattern: Regex, matchable: T) -> Bool {
|
|
return matchable.match(pattern)
|
|
}
|
|
|
|
public enum SendAuthorizationCodeResult {
|
|
case sentCode(UnauthorizedAccount)
|
|
case loggedIn
|
|
}
|
|
|
|
func storeFutureLoginToken(accountManager: AccountManager<TelegramAccountManagerTypes>, 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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, phoneCodeHash: String, timeout: Int32?, firebaseSecret: String, syncContacts: Bool) -> Signal<Bool, SendFirebaseAuthorizationCodeError> {
|
|
//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, safetyNetToken: nil, iosPushSecret: firebaseSecret))
|
|
|> mapError { _ -> SendFirebaseAuthorizationCodeError in
|
|
return .generic
|
|
}
|
|
|> mapToSignal { result -> Signal<Bool, SendFirebaseAuthorizationCodeError> in
|
|
return .single(true)
|
|
}
|
|
}
|
|
|
|
public func sendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, phoneNumber: String, apiId: Int32, apiHash: String, pushNotificationConfiguration: AuthorizationCodePushNotificationConfiguration?, firebaseSecretStream: Signal<[String: String], NoError>, syncContacts: Bool, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> {
|
|
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)
|
|
}
|
|
}
|
|
#if DEBUG
|
|
cloudValue.removeAll()
|
|
#endif
|
|
return accountManager.transaction { transaction -> [Data] in
|
|
return transaction.getStoredLoginTokens()
|
|
}
|
|
|> castError(AuthorizationCodeRequestError.self)
|
|
|> mapToSignal { localAuthTokens -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
var authTokens = localAuthTokens
|
|
|
|
#if DEBUG
|
|
authTokens.removeAll()
|
|
#endif
|
|
|
|
for data in cloudValue {
|
|
if !authTokens.contains(data) {
|
|
authTokens.insert(data, at: 0)
|
|
}
|
|
}
|
|
|
|
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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
return account.postbox.transaction { transaction -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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):
|
|
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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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<Bool, SendFirebaseAuthorizationCodeError> in
|
|
return .single(false)
|
|
}
|
|
|> mapError { _ -> AuthorizationCodeRequestError in
|
|
return .generic(info: nil)
|
|
}
|
|
|> mapToSignal { success -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
if success {
|
|
return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in
|
|
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)))
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)))
|
|
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)
|
|
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)
|
|
return .loggedIn
|
|
}
|
|
|> castError(AuthorizationCodeRequestError.self)
|
|
case .authorizationSignUpRequired:
|
|
return .never()
|
|
}
|
|
}
|
|
return .single(.sentCode(account))
|
|
}
|
|
}
|
|
|> mapError { _ -> AuthorizationCodeRequestError in
|
|
}
|
|
|> switchToLatest
|
|
}
|
|
}
|
|
}
|
|
|
|
private func internalResendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, number: String, apiId: Int32, apiHash: String, hash: String, syncContacts: Bool, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> {
|
|
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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
return account.postbox.transaction { transaction -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
switch sentCode {
|
|
case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
|
|
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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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<Bool, SendFirebaseAuthorizationCodeError> in
|
|
return .single(false)
|
|
}
|
|
|> mapError { _ -> AuthorizationCodeRequestError in
|
|
return .generic(info: nil)
|
|
}
|
|
|> mapToSignal { success -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
if success {
|
|
return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in
|
|
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)))
|
|
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)))
|
|
|
|
return .single(.sentCode(account))
|
|
case .sentCodeSuccess:
|
|
return .single(.loggedIn)
|
|
}
|
|
}
|
|
|> mapError { _ -> AuthorizationCodeRequestError in }
|
|
|> switchToLatest
|
|
}
|
|
}
|
|
|
|
public func resendAuthorizationCode(accountManager: AccountManager<TelegramAccountManagerTypes>, account: UnauthorizedAccount, apiId: Int32, apiHash: String, firebaseSecretStream: Signal<[String: String], NoError>) -> Signal<Void, AuthorizationCodeRequestError> {
|
|
return account.postbox.transaction { transaction -> Signal<Void, AuthorizationCodeRequestError> 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 if error.errorDescription == "UPDATE_APP_TO_LOGIN" {
|
|
return .appOutdated
|
|
} else {
|
|
return .generic(info: (Int(error.errorCode), error.errorDescription))
|
|
}
|
|
}
|
|
|> mapToSignal { sentCode -> Signal<Void, AuthorizationCodeRequestError> in
|
|
return account.postbox.transaction { transaction -> Signal<Void, AuthorizationCodeRequestError> in
|
|
switch sentCode {
|
|
case let .sentCode(_, type, phoneCodeHash, nextType, codeTimeout):
|
|
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<SendAuthorizationCodeResult, AuthorizationCodeRequestError> 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<Bool, SendFirebaseAuthorizationCodeError> in
|
|
return .single(false)
|
|
}
|
|
|> mapError { _ -> AuthorizationCodeRequestError in
|
|
return .generic(info: nil)
|
|
}
|
|
|> mapToSignal { success -> Signal<SendAuthorizationCodeResult, AuthorizationCodeRequestError> in
|
|
if success {
|
|
return account.postbox.transaction { transaction -> SendAuthorizationCodeResult in
|
|
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)))
|
|
|
|
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: SentAuthorizationCodeType(apiType: type), hash: phoneCodeHash, timeout: codeTimeout, nextType: parsedNextType, syncContacts: syncContacts)))
|
|
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<ChangeLoginEmailData, AuthorizationSendEmailCodeError> {
|
|
return account.network.request(Api.functions.account.sendVerifyEmailCode(purpose: .emailVerifyPurposeLoginChange, email: email), automaticFloodWait: false)
|
|
|> `catch` { error -> Signal<Api.account.SentEmailCode, AuthorizationSendEmailCodeError> 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<Never, AuthorizationSendEmailCodeError> {
|
|
return account.postbox.transaction { transaction -> Signal<Never, AuthorizationSendEmailCodeError> 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<Api.account.SentEmailCode, AuthorizationSendEmailCodeError> 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<Never, AuthorizationSendEmailCodeError> in
|
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> 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)))
|
|
}
|
|
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<Never, AuthorizationEmailVerificationError> {
|
|
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<Api.account.EmailVerified, AuthorizationEmailVerificationError> 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<Never, AuthorizationEmailVerificationError> in
|
|
return .complete()
|
|
}
|
|
}
|
|
|
|
public func verifyLoginEmailSetup(account: UnauthorizedAccount, code: AuthorizationCode.EmailVerification) -> Signal<Never, AuthorizationEmailVerificationError> {
|
|
return account.postbox.transaction { transaction -> Signal<Never, AuthorizationEmailVerificationError> 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<Api.account.EmailVerified, AuthorizationEmailVerificationError> 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<Never, AuthorizationEmailVerificationError> in
|
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> 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)))
|
|
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<Never, AuthorizationEmailResetError> {
|
|
return account.postbox.transaction { transaction -> Signal<Never, AuthorizationEmailResetError> 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<Api.auth.SentCode, AuthorizationEmailResetError> 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<Never, AuthorizationEmailResetError> in
|
|
return account.postbox.transaction { transaction -> Signal<Never, NoError> 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)))
|
|
|
|
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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, code: AuthorizationCode, termsOfService: UnauthorizedAccountTermsOfService?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> {
|
|
return account.postbox.transaction { transaction -> Signal<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<AuthorizationCodeResult, AuthorizationCodeVerificationError> 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<AuthorizeWithCodeResult, AuthorizationCodeVerificationError> in
|
|
return account.postbox.transaction { transaction -> Signal<AuthorizeWithCodeResult, NoError> 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)
|
|
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)
|
|
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<Never, NoError> {
|
|
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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, password: String, syncContacts: Bool) -> Signal<Void, AuthorizationPasswordVerificationError> {
|
|
return verifyPassword(account, password: password)
|
|
|> `catch` { error -> Signal<Api.auth.Authorization, AuthorizationPasswordVerificationError> 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<Void, AuthorizationPasswordVerificationError> in
|
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> 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
|
|
})*/
|
|
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<Never, PasswordRecoveryError> {
|
|
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<Never, PasswordRecoveryError> 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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, recoveredAccountData: RecoveredAccountData, syncContacts: Bool) -> Signal<Never, NoError> {
|
|
return account.postbox.transaction { transaction -> Signal<Void, NoError> in
|
|
switch recoveredAccountData.authorization {
|
|
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: [])
|
|
|
|
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<RecoveredAccountData, PasswordRecoveryError> {
|
|
return _internal_twoStepAuthData(network)
|
|
|> mapError { _ -> PasswordRecoveryError in
|
|
return .generic
|
|
}
|
|
|> mapToSignal { authData -> Signal<RecoveredAccountData, PasswordRecoveryError> 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<RecoveredAccountData, PasswordRecoveryError> in
|
|
return .single(RecoveredAccountData(authorization: result))
|
|
}
|
|
}
|
|
}
|
|
|
|
public enum AccountResetError {
|
|
case generic
|
|
case limitExceeded
|
|
}
|
|
|
|
public func performAccountReset(account: UnauthorizedAccount) -> Signal<Void, AccountResetError> {
|
|
return account.network.request(Api.functions.account.deleteAccount(flags: 0, reason: "", password: nil))
|
|
|> map { _ -> Int32? in return nil }
|
|
|> `catch` { error -> Signal<Int32?, AccountResetError> 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<Void, AccountResetError> 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<TelegramAccountManagerTypes>, account: UnauthorizedAccount, firstName: String, lastName: String, avatarData: Data?, avatarVideo: Signal<UploadedPeerPhotoData?, NoError>?, videoStartTimestamp: Double?, forcedPasswordSetupNotice: @escaping (Int32) -> (NoticeEntryKey, CodableEntry)?) -> Signal<Void, SignUpError> {
|
|
return account.postbox.transaction { transaction -> Signal<Void, SignUpError> 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<Void, SignUpError> in
|
|
switch result {
|
|
case let .authorization(_, otherwiseReloginDays, _, futureAuthToken, user):
|
|
if let futureAuthToken = futureAuthToken {
|
|
storeFutureLoginToken(accountManager: accountManager, token: futureAuthToken.makeData())
|
|
}
|
|
|
|
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, 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)
|
|
}
|
|
|> 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<UpdatePeerPhotoStatus, SignUpError> in
|
|
return .complete()
|
|
}
|
|
|> mapToSignal { result -> Signal<Void, SignUpError> 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<Void, NoError> {
|
|
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))
|
|
}
|
|
}
|
|
}
|