import Foundation import Intents import TelegramCore import SyncCore import Postbox import SwiftSignalKit import BuildConfig import Contacts import OpenSSLEncryptionProvider import AppLockState import UIKit import GeneratedSources private var accountCache: Account? private var installedSharedLogger = false private func setupSharedLogger(rootPath: String, path: String) { if !installedSharedLogger { installedSharedLogger = true Logger.setSharedLogger(Logger(rootPath: rootPath, basePath: path)) } } private let accountAuxiliaryMethods = AccountAuxiliaryMethods(updatePeerChatInputState: { interfaceState, inputState -> PeerChatInterfaceState? in return interfaceState }, fetchResource: { account, resource, ranges, _ in return nil }, fetchResourceMediaReferenceHash: { resource in return .single(nil) }, prepareSecretThumbnailData: { _ in return nil }) private struct ApplicationSettings { let logging: LoggingSettings } private func applicationSettings(accountManager: AccountManager) -> Signal { return accountManager.transaction { transaction -> ApplicationSettings in let loggingSettings: LoggingSettings if let value = transaction.getSharedData(SharedDataKeys.loggingSettings) as? LoggingSettings { loggingSettings = value } else { loggingSettings = LoggingSettings.defaultSettings } return ApplicationSettings(logging: loggingSettings) } } enum IntentHandlingError { case generic } @available(iOSApplicationExtension 10.0, iOS 10.0, *) @objc(IntentHandler) class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling, INStartAudioCallIntentHandling, INSearchCallHistoryIntentHandling, SelectFriendsIntentHandling { private let accountPromise = Promise() private let allAccounts = Promise<[(AccountRecordId, PeerId)]>() private let resolvePersonsDisposable = MetaDisposable() private let actionDisposable = MetaDisposable() private let searchDisposable = MetaDisposable() private var rootPath: String? private var accountManager: AccountManager? private var encryptionParameters: ValueBoxEncryptionParameters? private var appGroupUrl: URL? override init() { super.init() guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { return } let baseAppBundleId = String(appBundleIdentifier[.. take(1) |> map { view -> [(AccountRecordId, PeerId)] in var result: [(AccountRecordId, Int, PeerId)] = [] for record in view.records { let isLoggedOut = record.attributes.contains(where: { attribute in return attribute is LoggedOutAccountAttribute }) if isLoggedOut { continue } /*let isTestingEnvironment = record.attributes.contains(where: { attribute in if let attribute = attribute as? AccountEnvironmentAttribute, case .test = attribute.environment { return true } else { return false } })*/ var backupData: AccountBackupData? var sortIndex: Int32 = 0 for attribute in record.attributes { if let attribute = attribute as? AccountSortOrderAttribute { sortIndex = attribute.order } else if let attribute = attribute as? AccountBackupDataAttribute { backupData = attribute.data } } if let backupData = backupData { result.append((record.id, Int(sortIndex), PeerId(backupData.peerId))) } } result.sort(by: { lhs, rhs in if lhs.1 != rhs.1 { return lhs.1 < rhs.1 } else { return lhs.0 < rhs.0 } }) return result.map { record -> (AccountRecordId, PeerId) in return (record.0, record.2) } }) let account: Signal if let accountCache = accountCache { account = .single(accountCache) } else { account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) |> mapToSignal { account -> Signal in if let account = account { switch account { case .upgrading: return .complete() case let .authorized(account): return applicationSettings(accountManager: accountManager) |> deliverOnMainQueue |> map { settings -> Account in accountCache = account Logger.shared.logToFile = settings.logging.logToFile Logger.shared.logToConsole = settings.logging.logToConsole Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData return account } case .unauthorized: return .complete() } } else { return .single(nil) } } |> take(1) } self.accountPromise.set(account) } deinit { self.resolvePersonsDisposable.dispose() self.actionDisposable.dispose() self.searchDisposable.dispose() } override public func handler(for intent: INIntent) -> Any { if #available(iOSApplicationExtension 12.0, iOS 12.0, *) { if intent is SelectAvatarFriendsIntent { return AvatarsIntentHandler() } else { return self } } else { return self } } enum ResolveResult { case success(INPerson) case disambiguation([INPerson]) case needsValue case noResult case skip @available(iOSApplicationExtension 11.0, iOS 11.0, *) var sendMessageRecipientResulutionResult: INSendMessageRecipientResolutionResult { switch self { case let .success(person): return .success(with: person) case let .disambiguation(persons): return .disambiguation(with: persons) case .needsValue: return .needsValue() case .noResult: return .unsupported() case .skip: return .notRequired() } } var personResolutionResult: INPersonResolutionResult { switch self { case let .success(person): return .success(with: person) case let .disambiguation(persons): return .disambiguation(with: persons) case .needsValue: return .needsValue() case .noResult: return .unsupported() case .skip: return .notRequired() } } } private func resolve(persons: [INPerson]?, with completion: @escaping ([ResolveResult]) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { completion([.skip]) return } } let account = self.accountPromise.get() guard let initialPersons = persons, !initialPersons.isEmpty else { completion([.needsValue]) return } var filteredPersons: [INPerson] = [] for person in initialPersons { if let contactIdentifier = person.contactIdentifier, !contactIdentifier.isEmpty { filteredPersons.append(person) } if #available(iOSApplicationExtension 10.3, iOS 10.3, *) { if let siriMatches = person.siriMatches { for match in siriMatches { if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty { filteredPersons.append(match) } } } } } if filteredPersons.isEmpty { completion([.noResult]) return } var allPersonsAlreadyMatched = true for person in filteredPersons { if !(person.customIdentifier ?? "").hasPrefix("tg") { allPersonsAlreadyMatched = false break } } if allPersonsAlreadyMatched && filteredPersons.count == 1 { completion([.success(filteredPersons[0])]) return } let stableIds = filteredPersons.compactMap({ person -> String? in if let contactIdentifier = person.contactIdentifier { return contactIdentifier } if #available(iOSApplicationExtension 10.3, iOS 10.3, *) { if let siriMatches = person.siriMatches { for match in siriMatches { if let contactIdentifier = match.contactIdentifier, !contactIdentifier.isEmpty { return contactIdentifier } } } } return nil }) let signal = matchingDeviceContacts(stableIds: stableIds) |> take(1) |> mapToSignal { matchedContacts in return account |> castError(IntentContactsError.self) |> mapToSignal { account -> Signal<[(String, TelegramUser)], IntentContactsError> in if let account = account { return matchingCloudContacts(postbox: account.postbox, contacts: matchedContacts) |> castError(IntentContactsError.self) } else { return .fail(.generic) } } } self.resolvePersonsDisposable.set((signal |> deliverOnMainQueue).start(next: { peers in if peers.isEmpty { completion([.noResult]) } else if peers.count == 1 { completion(peers.map { .success(personWithUser(stableId: $0, user: $1)) }) } else { completion([.disambiguation(peers.map { (personWithUser(stableId: $0, user: $1)) })]) } }, error: { error in completion([.skip]) })) } // MARK: - INSendMessageIntentHandling public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) { guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { completion([INPersonResolutionResult.notRequired()]) return } self.resolve(persons: intent.recipients, with: { result in completion(result.map { $0.personResolutionResult }) }) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { completion([INSendMessageRecipientResolutionResult.notRequired()]) return } } if let peerId = intent.conversationIdentifier.flatMap(Int64.init) { let account = self.accountPromise.get() let signal = account |> castError(IntentHandlingError.self) |> mapToSignal { account -> Signal in if let account = account { return matchingCloudContact(postbox: account.postbox, peerId: PeerId(peerId)) |> castError(IntentHandlingError.self) |> map { user -> INPerson? in if let user = user { return personWithUser(stableId: "tg\(peerId)", user: user) } else { return nil } } } else { return .fail(.generic) } } self.resolvePersonsDisposable.set((signal |> deliverOnMainQueue).start(next: { person in if let person = person { completion([INSendMessageRecipientResolutionResult.success(with: person)]) } else { completion([INSendMessageRecipientResolutionResult.needsValue()]) } }, error: { error in completion([INSendMessageRecipientResolutionResult.unsupported(forReason: .noAccount)]) })) } else { guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { completion([INSendMessageRecipientResolutionResult.notRequired()]) return } self.resolve(persons: intent.recipients, with: { result in completion(result.map { $0.sendMessageRecipientResulutionResult }) }) } } public func resolveContent(for intent: INSendMessageIntent, with completion: @escaping (INStringResolutionResult) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { completion(INStringResolutionResult.notRequired()) return } } guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { completion(INStringResolutionResult.notRequired()) return } if let text = intent.content, !text.isEmpty { completion(INStringResolutionResult.success(with: text)) } else { completion(INStringResolutionResult.needsValue()) } } public func confirm(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } } let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } let response = INSendMessageIntentResponse(code: .ready, userActivity: userActivity) completion(response) } public func handle(intent: INSendMessageIntent, completion: @escaping (INSendMessageIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } } self.actionDisposable.set((self.accountPromise.get() |> castError(IntentHandlingError.self) |> take(1) |> mapToSignal { account -> Signal in guard let account = account else { return .fail(.generic) } guard let recipient = intent.recipients?.first, let customIdentifier = recipient.customIdentifier, customIdentifier.hasPrefix("tg") else { return .fail(.generic) } guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else { return .fail(.generic) } let peerId = PeerId(peerIdValue) if peerId.namespace != Namespaces.Peer.CloudUser { return .fail(.generic) } account.shouldBeServiceTaskMaster.set(.single(.now)) return standaloneSendMessage(account: account, peerId: peerId, text: intent.content ?? "", attributes: [], media: nil, replyToMessageId: nil) |> mapError { _ -> IntentHandlingError in return .generic } |> mapToSignal { _ -> Signal in return .complete() } |> afterDisposed { account.shouldBeServiceTaskMaster.set(.single(.never)) } } |> deliverOnMainQueue).start(error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) }, completed: { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSendMessageIntent.self)) let response = INSendMessageIntentResponse(code: .success, userActivity: userActivity) completion(response) })) } // MARK: - INSearchForMessagesIntentHandling public func resolveAttributes(for intent: INSearchForMessagesIntent, with completion: @escaping (INMessageAttributeOptionsResolutionResult) -> Void) { completion(.success(with: .unread)) } public func handle(intent: INSearchForMessagesIntent, completion: @escaping (INSearchForMessagesIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) let response = INSearchForMessagesIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } } self.actionDisposable.set((self.accountPromise.get() |> take(1) |> castError(IntentHandlingError.self) |> mapToSignal { account -> Signal<[INMessage], IntentHandlingError> in guard let account = account else { return .fail(.generic) } account.shouldBeServiceTaskMaster.set(.single(.now)) account.resetStateManagement() let completion: Signal = account.stateManager.pollStateUpdateCompletion() |> map { _ in return Void() } return (completion |> timeout(4.0, queue: Queue.mainQueue(), alternate: .single(Void()))) |> castError(IntentHandlingError.self) |> take(1) |> mapToSignal { _ -> Signal<[INMessage], IntentHandlingError> in let messages: Signal<[INMessage], NoError> if let identifiers = intent.identifiers, !identifiers.isEmpty { messages = getMessages(account: account, ids: identifiers.compactMap(MessageId.init(string:))) } else { messages = unreadMessages(account: account) } return messages |> castError(IntentHandlingError.self) |> afterDisposed { account.shouldBeServiceTaskMaster.set(.single(.never)) } } } |> deliverOnMainQueue).start(next: { messages in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) let response = INSearchForMessagesIntentResponse(code: .success, userActivity: userActivity) response.messages = messages completion(response) }, error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchForMessagesIntent.self)) let response = INSearchForMessagesIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) })) } // MARK: - INSetMessageAttributeIntentHandling public func resolveAttribute(for intent: INSetMessageAttributeIntent, with completion: @escaping (INMessageAttributeResolutionResult) -> Void) { let supportedAttributes: [INMessageAttribute] = [.read, .unread] var attribute = intent.attribute if attribute == .flagged { attribute = .unread } if supportedAttributes.contains(attribute) { completion(.success(with: attribute)) } else { completion(.confirmationRequired(with: intent.attribute)) } } public func handle(intent: INSetMessageAttributeIntent, completion: @escaping (INSetMessageAttributeIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .failure, userActivity: userActivity) completion(response) return } } self.actionDisposable.set((self.accountPromise.get() |> castError(IntentHandlingError.self) |> take(1) |> mapToSignal { account -> Signal in guard let account = account else { return .fail(.generic) } var signals: [Signal] = [] var maxMessageIdsToApply: [PeerId: MessageId] = [:] if let identifiers = intent.identifiers { for identifier in identifiers { let components = identifier.components(separatedBy: "_") if let first = components.first, let peerId = Int64(first), let namespace = Int32(components[1]), let id = Int32(components[2]) { let peerId = PeerId(peerId) let messageId = MessageId(peerId: peerId, namespace: namespace, id: id) if let currentMessageId = maxMessageIdsToApply[peerId] { if currentMessageId < messageId { maxMessageIdsToApply[peerId] = messageId } } else { maxMessageIdsToApply[peerId] = messageId } } } } for (_, messageId) in maxMessageIdsToApply { signals.append(applyMaxReadIndexInteractively(postbox: account.postbox, stateManager: account.stateManager, index: MessageIndex(id: messageId, timestamp: 0)) |> castError(IntentHandlingError.self)) } if signals.isEmpty { return .complete() } else { account.shouldBeServiceTaskMaster.set(.single(.now)) return combineLatest(signals) |> mapToSignal { _ -> Signal in return .complete() } |> afterDisposed { account.shouldBeServiceTaskMaster.set(.single(.never)) } } } |> deliverOnMainQueue).start(error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .failure, userActivity: userActivity) completion(response) }, completed: { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSetMessageAttributeIntent.self)) let response = INSetMessageAttributeIntentResponse(code: .success, userActivity: userActivity) completion(response) })) } // MARK: - INStartAudioCallIntentHandling public func resolveContacts(for intent: INStartAudioCallIntent, with completion: @escaping ([INPersonResolutionResult]) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { completion([INPersonResolutionResult.notRequired()]) return } } guard CNContactStore.authorizationStatus(for: .contacts) == .authorized else { completion([INPersonResolutionResult.notRequired()]) return } self.resolve(persons: intent.contacts, with: { result in completion(result.map { $0.personResolutionResult }) }) } @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func resolveDestinationType(for intent: INStartAudioCallIntent, with completion: @escaping (INCallDestinationTypeResolutionResult) -> Void) { completion(.success(with: .normal)) } public func handle(intent: INStartAudioCallIntent, completion: @escaping (INStartAudioCallIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self)) let response = INStartAudioCallIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } } self.actionDisposable.set((self.accountPromise.get() |> castError(IntentHandlingError.self) |> take(1) |> mapToSignal { account -> Signal in guard let contact = intent.contacts?.first, let customIdentifier = contact.customIdentifier, customIdentifier.hasPrefix("tg") else { return .fail(.generic) } guard let peerIdValue = Int64(String(customIdentifier[customIdentifier.index(customIdentifier.startIndex, offsetBy: 2)...])) else { return .fail(.generic) } let peerId = PeerId(peerIdValue) if peerId.namespace != Namespaces.Peer.CloudUser { return .fail(.generic) } return .single(peerId) } |> deliverOnMainQueue).start(next: { peerId in let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self)) userActivity.userInfo = ["handle": "TGCA\(peerId.toInt64())"] let response = INStartAudioCallIntentResponse(code: .continueInApp, userActivity: userActivity) completion(response) }, error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INStartAudioCallIntent.self)) let response = INStartAudioCallIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) })) } // MARK: - INSearchCallHistoryIntentHandling @available(iOSApplicationExtension 11.0, iOS 11.0, *) public func resolveCallTypes(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeOptionsResolutionResult) -> Void) { completion(.success(with: .missed)) } /*public func resolveCallType(for intent: INSearchCallHistoryIntent, with completion: @escaping (INCallRecordTypeResolutionResult) -> Void) { completion(.success(with: .missed)) }*/ public func handle(intent: INSearchCallHistoryIntent, completion: @escaping (INSearchCallHistoryIntentResponse) -> Void) { if let appGroupUrl = self.appGroupUrl { let rootPath = rootPathForBasePath(appGroupUrl.path) if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data), isAppLocked(state: state) { let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self)) let response = INSearchCallHistoryIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) return } } self.actionDisposable.set((self.accountPromise.get() |> take(1) |> castError(IntentHandlingError.self) |> mapToSignal { account -> Signal<[CallRecord], IntentHandlingError> in guard let account = account else { return .fail(.generic) } account.shouldBeServiceTaskMaster.set(.single(.now)) return missedCalls(account: account) |> castError(IntentHandlingError.self) |> afterDisposed { account.shouldBeServiceTaskMaster.set(.single(.never)) } } |> deliverOnMainQueue).start(next: { calls in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self)) let response: INSearchCallHistoryIntentResponse if #available(iOSApplicationExtension 11.0, iOS 11.0, *) { response = INSearchCallHistoryIntentResponse(code: .success, userActivity: userActivity) response.callRecords = calls.map { $0.intentCall } } else { response = INSearchCallHistoryIntentResponse(code: .continueInApp, userActivity: userActivity) } completion(response) }, error: { _ in let userActivity = NSUserActivity(activityType: NSStringFromClass(INSearchCallHistoryIntent.self)) let response = INSearchCallHistoryIntentResponse(code: .failureRequiringAppLaunch, userActivity: userActivity) completion(response) })) } @available(iOSApplicationExtension 14.0, iOS 14.0, *) func provideFriendsOptionsCollection(for intent: SelectFriendsIntent, searchTerm: String?, with completion: @escaping (INObjectCollection?, Error?) -> Void) { guard let rootPath = self.rootPath, let _ = self.accountManager, let encryptionParameters = self.encryptionParameters else { completion(nil, nil) return } if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data) { if state.isManuallyLocked || state.autolockTimeout != nil { let error = NSError(domain: "Locked", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Open Telegram and enter passcode to edit widget." ]) completion(nil, error) return } } self.searchDisposable.set((self.allAccounts.get() |> castError(Error.self) |> take(1) |> mapToSignal { accounts -> Signal, Error> in var accountResults: [Signal, Error>] = [] for (accountId, accountPeerId) in accounts { accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> INObjectSection in var accountTitle: String = "" if let peer = transaction.getPeer(accountPeerId) as? TelegramUser { if let username = peer.username, !username.isEmpty { accountTitle = "@\(username)" } else { accountTitle = peer.debugDisplayTitle } } var peers: [Peer] = [] if let searchTerm = searchTerm { if !searchTerm.isEmpty { for renderedPeer in transaction.searchPeers(query: searchTerm) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } } if peers.count > 30 { peers = Array(peers.dropLast(peers.count - 30)) } } } else { for renderedPeer in transaction.getTopChatListEntries(groupId: .root, count: 50) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } } } var items: [Friend] = [] for peer in peers { let path = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in return postbox.mediaBox.resourcePath(representation.resource) } let profileImage: INImage? let image = avatarImage(path: path, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { profileImage = INImage(imageData: data) } else { profileImage = nil } items.append(Friend(identifier: "\(accountId.int64):\(peer.id.toInt64())", display: peer.debugDisplayTitle, subtitle: nil, image: profileImage)) } return INObjectSection(title: accountTitle, items: items) }) |> castError(Error.self)) } return combineLatest(accountResults) |> map { accountResults -> INObjectCollection in let filteredSections = accountResults.filter { section in return !section.items.isEmpty } if filteredSections.count == 1 { return INObjectCollection(items: filteredSections[0].items) } else { return INObjectCollection(sections: filteredSections) } } }).start(next: { result in completion(result, nil) }, error: { error in completion(nil, error) })) } } @available(iOSApplicationExtension 10.0, iOS 10.0, *) @objc(AvatarsIntentHandler) class AvatarsIntentHandler: NSObject, SelectAvatarFriendsIntentHandling { private let accountPromise = Promise() private let allAccounts = Promise<[(AccountRecordId, PeerId)]>() private let resolvePersonsDisposable = MetaDisposable() private let actionDisposable = MetaDisposable() private let searchDisposable = MetaDisposable() private var rootPath: String? private var accountManager: AccountManager? private var encryptionParameters: ValueBoxEncryptionParameters? private var appGroupUrl: URL? override init() { super.init() guard let appBundleIdentifier = Bundle.main.bundleIdentifier, let lastDotRange = appBundleIdentifier.range(of: ".", options: [.backwards]) else { return } let baseAppBundleId = String(appBundleIdentifier[.. take(1) |> map { view -> [(AccountRecordId, PeerId)] in var result: [(AccountRecordId, Int, PeerId)] = [] for record in view.records { let isLoggedOut = record.attributes.contains(where: { attribute in return attribute is LoggedOutAccountAttribute }) if isLoggedOut { continue } /*let isTestingEnvironment = record.attributes.contains(where: { attribute in if let attribute = attribute as? AccountEnvironmentAttribute, case .test = attribute.environment { return true } else { return false } })*/ var backupData: AccountBackupData? var sortIndex: Int32 = 0 for attribute in record.attributes { if let attribute = attribute as? AccountSortOrderAttribute { sortIndex = attribute.order } else if let attribute = attribute as? AccountBackupDataAttribute { backupData = attribute.data } } if let backupData = backupData { result.append((record.id, Int(sortIndex), PeerId(backupData.peerId))) } } result.sort(by: { lhs, rhs in if lhs.1 != rhs.1 { return lhs.1 < rhs.1 } else { return lhs.0 < rhs.0 } }) return result.map { record -> (AccountRecordId, PeerId) in return (record.0, record.2) } }) let account: Signal if let accountCache = accountCache { account = .single(accountCache) } else { account = currentAccount(allocateIfNotExists: false, networkArguments: NetworkInitializationArguments(apiId: apiId, apiHash: apiHash, languagesCategory: languagesCategory, appVersion: appVersion, voipMaxLayer: 0, voipVersions: [], appData: .single(buildConfig.bundleData(withAppToken: nil, signatureDict: nil)), autolockDeadine: .single(nil), encryptionProvider: OpenSSLEncryptionProvider()), supplementary: true, manager: accountManager, rootPath: rootPath, auxiliaryMethods: accountAuxiliaryMethods, encryptionParameters: encryptionParameters) |> mapToSignal { account -> Signal in if let account = account { switch account { case .upgrading: return .complete() case let .authorized(account): return applicationSettings(accountManager: accountManager) |> deliverOnMainQueue |> map { settings -> Account in accountCache = account Logger.shared.logToFile = settings.logging.logToFile Logger.shared.logToConsole = settings.logging.logToConsole Logger.shared.redactSensitiveData = settings.logging.redactSensitiveData return account } case .unauthorized: return .complete() } } else { return .single(nil) } } |> take(1) } self.accountPromise.set(account) } deinit { self.resolvePersonsDisposable.dispose() self.actionDisposable.dispose() self.searchDisposable.dispose() } @available(iOSApplicationExtension 14.0, iOS 14.0, *) func provideFriendsOptionsCollection(for intent: SelectAvatarFriendsIntent, searchTerm: String?, with completion: @escaping (INObjectCollection?, Error?) -> Void) { guard let rootPath = self.rootPath, let _ = self.accountManager, let encryptionParameters = self.encryptionParameters else { completion(nil, nil) return } if let data = try? Data(contentsOf: URL(fileURLWithPath: appLockStatePath(rootPath: rootPath))), let state = try? JSONDecoder().decode(LockState.self, from: data) { if state.isManuallyLocked || state.autolockTimeout != nil { let error = NSError(domain: "Locked", code: 1, userInfo: [ NSLocalizedDescriptionKey: "Open Telegram and enter passcode to edit widget." ]) completion(nil, error) return } } self.searchDisposable.set((self.allAccounts.get() |> castError(Error.self) |> take(1) |> mapToSignal { accounts -> Signal, Error> in var accountResults: [Signal, Error>] = [] for (accountId, accountPeerId) in accounts { accountResults.append(accountTransaction(rootPath: rootPath, id: accountId, encryptionParameters: encryptionParameters, transaction: { postbox, transaction -> INObjectSection in var accountTitle: String = "" if let peer = transaction.getPeer(accountPeerId) as? TelegramUser { if let username = peer.username, !username.isEmpty { accountTitle = "@\(username)" } else { accountTitle = peer.debugDisplayTitle } } var peers: [Peer] = [] if let searchTerm = searchTerm { if !searchTerm.isEmpty { for renderedPeer in transaction.searchPeers(query: searchTerm) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } } if peers.count > 30 { peers = Array(peers.dropLast(peers.count - 30)) } } } else { for renderedPeer in transaction.getTopChatListEntries(groupId: .root, count: 50) { if let peer = renderedPeer.peer, !(peer is TelegramSecretChat), !peer.isDeleted { peers.append(peer) } } } var items: [Friend] = [] for peer in peers { let path = smallestImageRepresentation(peer.profileImageRepresentations).flatMap { representation in return postbox.mediaBox.resourcePath(representation.resource) } let profileImage: INImage? let image = avatarImage(path: path, peerId: peer.id.toInt64(), accountPeerId: accountPeerId.toInt64(), letters: peer.displayLetters, size: CGSize(width: 50.0, height: 50.0)) if let data = image.pngData() { profileImage = INImage(imageData: data) } else { profileImage = nil } items.append(Friend(identifier: "\(accountId.int64):\(peer.id.toInt64())", display: peer.debugDisplayTitle, subtitle: nil, image: profileImage)) } return INObjectSection(title: accountTitle, items: items) }) |> castError(Error.self)) } return combineLatest(accountResults) |> map { accountResults -> INObjectCollection in let filteredSections = accountResults.filter { section in return !section.items.isEmpty } if filteredSections.count == 1 { return INObjectCollection(items: filteredSections[0].items) } else { return INObjectCollection(sections: filteredSections) } } }).start(next: { result in completion(result, nil) }, error: { error in completion(nil, error) })) } } private func avatarRoundImage(size: CGSize, source: UIImage) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) let context = UIGraphicsGetCurrentContext() context?.beginPath() context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context?.clip() source.draw(in: CGRect(origin: CGPoint(), size: size)) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } private let deviceColorSpace: CGColorSpace = { if #available(iOSApplicationExtension 9.3, iOS 9.3, *) { if let colorSpace = CGColorSpace(name: CGColorSpace.displayP3) { return colorSpace } else { return CGColorSpaceCreateDeviceRGB() } } else { return CGColorSpaceCreateDeviceRGB() } }() private extension UIColor { convenience init(rgb: UInt32) { self.init(red: CGFloat((rgb >> 16) & 0xff) / 255.0, green: CGFloat((rgb >> 8) & 0xff) / 255.0, blue: CGFloat(rgb & 0xff) / 255.0, alpha: 1.0) } } private let gradientColors: [NSArray] = [ [UIColor(rgb: 0xff516a).cgColor, UIColor(rgb: 0xff885e).cgColor], [UIColor(rgb: 0xffa85c).cgColor, UIColor(rgb: 0xffcd6a).cgColor], [UIColor(rgb: 0x665fff).cgColor, UIColor(rgb: 0x82b1ff).cgColor], [UIColor(rgb: 0x54cb68).cgColor, UIColor(rgb: 0xa0de7e).cgColor], [UIColor(rgb: 0x4acccd).cgColor, UIColor(rgb: 0x00fcfd).cgColor], [UIColor(rgb: 0x2a9ef1).cgColor, UIColor(rgb: 0x72d5fd).cgColor], [UIColor(rgb: 0xd669ed).cgColor, UIColor(rgb: 0xe0a2f3).cgColor], ] private func avatarViewLettersImage(size: CGSize, peerId: Int64, accountPeerId: Int64, letters: [String]) -> UIImage? { UIGraphicsBeginImageContextWithOptions(size, false, 0.0) let context = UIGraphicsGetCurrentContext() context?.beginPath() context?.addEllipse(in: CGRect(x: 0.0, y: 0.0, width: size.width, height: size.height)) context?.clip() let colorIndex = abs(Int(accountPeerId + peerId)) let colorsArray = gradientColors[colorIndex % gradientColors.count] var locations: [CGFloat] = [1.0, 0.0] let gradient = CGGradient(colorsSpace: deviceColorSpace, colors: colorsArray, locations: &locations)! context?.drawLinearGradient(gradient, start: CGPoint(), end: CGPoint(x: 0.0, y: size.height), options: CGGradientDrawingOptions()) context?.setBlendMode(.normal) let string = letters.count == 0 ? "" : (letters[0] + (letters.count == 1 ? "" : letters[1])) let attributedString = NSAttributedString(string: string, attributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 20.0), NSAttributedString.Key.foregroundColor: UIColor.white]) let line = CTLineCreateWithAttributedString(attributedString) let lineBounds = CTLineGetBoundsWithOptions(line, .useGlyphPathBounds) let lineOffset = CGPoint(x: string == "B" ? 1.0 : 0.0, y: 0.0) let lineOrigin = CGPoint(x: floor(-lineBounds.origin.x + (size.width - lineBounds.size.width) / 2.0) + lineOffset.x, y: floor(-lineBounds.origin.y + (size.height - lineBounds.size.height) / 2.0)) context?.translateBy(x: size.width / 2.0, y: size.height / 2.0) context?.scaleBy(x: 1.0, y: -1.0) context?.translateBy(x: -size.width / 2.0, y: -size.height / 2.0) context?.translateBy(x: lineOrigin.x, y: lineOrigin.y) if let context = context { CTLineDraw(line, context) } context?.translateBy(x: -lineOrigin.x, y: -lineOrigin.y) let image = UIGraphicsGetImageFromCurrentImageContext() UIGraphicsEndImageContext() return image } private func avatarImage(path: String?, peerId: Int64, accountPeerId: Int64, letters: [String], size: CGSize) -> UIImage { if let path = path, let image = UIImage(contentsOfFile: path), let roundImage = avatarRoundImage(size: size, source: image) { return roundImage } else { return avatarViewLettersImage(size: size, peerId: peerId, accountPeerId: accountPeerId, letters: letters)! } }