// // ActionRequestHandler.swift // OpenInActionExtension // // Created by Marcus Kida on 03.01.23. // import Combine import UIKit import MobileCoreServices import UniformTypeIdentifiers import MastodonCore import MastodonSDK import MastodonLocalization class ActionRequestHandler: NSObject, NSExtensionRequestHandling { var extensionContext: NSExtensionContext? var cancellables = [AnyCancellable]() /// Capturing a static shared instance of AppContext here as otherwise there /// will be lifecycle issues and we don't want to keep multiple AppContexts around /// in case there another Action Extension process is spawned private static let appContext = AppContext() func beginRequest(with context: NSExtensionContext) { // Do not call super in an Action extension with no user interface self.extensionContext = context let itemProvider = context.inputItems .compactMap({ $0 as? NSExtensionItem }) .reduce([NSItemProvider](), { partialResult, acc in var nextResult = partialResult nextResult += acc.attachments ?? [] return nextResult }) .filter({ $0.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) }) .first guard let itemProvider = itemProvider else { return doneWithInvalidLink() } itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in DispatchQueue.main.async { guard let dictionary = item as? NSDictionary, let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary else { self?.doneWithInvalidLink() return } if let url = results["url"] as? String { self?.performSearch(for: url) } else { self?.doneWithInvalidLink() } } }) } } // Search API private extension ActionRequestHandler { func performSearch(for url: String) { guard let activeAuthenticationBox = Self.appContext .authenticationService .mastodonAuthenticationBoxes .first else { return doneWithResults(nil) } Mastodon.API .V2 .Search .search( session: .shared, domain: activeAuthenticationBox.domain, query: .init(q: url, resolve: true), authorization: activeAuthenticationBox.userAuthorization ) .receive(on: DispatchQueue.main) .sink { completion in // no-op } receiveValue: { [weak self] result in let value = result.value if let foundAccount = value.accounts.first { self?.doneWithResults([ "openURL": "mastodon://profile/\(foundAccount.acct)" ]) } else if let foundStatus = value.statuses.first { self?.doneWithResults([ "openURL": "mastodon://status/\(foundStatus.id)" ]) } else if let foundHashtag = value.hashtags.first { self?.continueWithSearch(foundHashtag.name) } else { self?.continueWithSearch(url) } } .store(in: &cancellables) } } // Fallback to In-App Search private extension ActionRequestHandler { func continueWithSearch(_ query: String) { guard let url = URL(string: query), let host = url.host else { return doneWithInvalidLink() } Mastodon.API .Instance .instance( session: .shared, domain: host ) .receive(on: DispatchQueue.main) .sink { _ in // no-op } receiveValue: { [weak self] response in guard response.value.version != nil else { self?.doneWithInvalidLink() return } guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else { self?.doneWithInvalidLink() return } self?.doneWithResults( ["openURL": "mastodon://search?query=\(query)"] ) } .store(in: &cancellables) } } // Action response handling private extension ActionRequestHandler { func doneWithInvalidLink() { doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError]) } func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) { if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg { let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize] let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier) let resultsItem = NSExtensionItem() resultsItem.attachments = [resultsProvider] self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil) } else { self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) } self.extensionContext = nil } }