ActionRequestHandler.swift 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. //
  2. // ActionRequestHandler.swift
  3. // OpenInActionExtension
  4. //
  5. // Created by Marcus Kida on 03.01.23.
  6. //
  7. import Combine
  8. import UIKit
  9. import MobileCoreServices
  10. import UniformTypeIdentifiers
  11. import MastodonCore
  12. import MastodonSDK
  13. import MastodonLocalization
  14. class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
  15. var extensionContext: NSExtensionContext?
  16. var cancellables = [AnyCancellable]()
  17. /// Capturing a static shared instance of AppContext here as otherwise there
  18. /// will be lifecycle issues and we don't want to keep multiple AppContexts around
  19. /// in case there another Action Extension process is spawned
  20. private static let appContext = AppContext()
  21. func beginRequest(with context: NSExtensionContext) {
  22. // Do not call super in an Action extension with no user interface
  23. self.extensionContext = context
  24. let itemProvider = context.inputItems
  25. .compactMap({ $0 as? NSExtensionItem })
  26. .reduce([NSItemProvider](), { partialResult, acc in
  27. var nextResult = partialResult
  28. nextResult += acc.attachments ?? []
  29. return nextResult
  30. })
  31. .filter({ $0.hasItemConformingToTypeIdentifier(UTType.propertyList.identifier) })
  32. .first
  33. guard let itemProvider = itemProvider else {
  34. return doneWithInvalidLink()
  35. }
  36. itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in
  37. DispatchQueue.main.async {
  38. guard
  39. let dictionary = item as? NSDictionary,
  40. let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary
  41. else {
  42. self?.doneWithInvalidLink()
  43. return
  44. }
  45. if let url = results["url"] as? String {
  46. self?.performSearch(for: url)
  47. } else {
  48. self?.doneWithInvalidLink()
  49. }
  50. }
  51. })
  52. }
  53. }
  54. // Search API
  55. private extension ActionRequestHandler {
  56. func performSearch(for url: String) {
  57. guard
  58. let activeAuthenticationBox = Self.appContext
  59. .authenticationService
  60. .mastodonAuthenticationBoxes
  61. .first
  62. else {
  63. return doneWithResults(nil)
  64. }
  65. Mastodon.API
  66. .V2
  67. .Search
  68. .search(
  69. session: .shared,
  70. domain: activeAuthenticationBox.domain,
  71. query: .init(q: url, resolve: true),
  72. authorization: activeAuthenticationBox.userAuthorization
  73. )
  74. .receive(on: DispatchQueue.main)
  75. .sink { completion in
  76. // no-op
  77. } receiveValue: { [weak self] result in
  78. let value = result.value
  79. if let foundAccount = value.accounts.first {
  80. self?.doneWithResults([
  81. "openURL": "mastodon://profile/\(foundAccount.acct)"
  82. ])
  83. } else if let foundStatus = value.statuses.first {
  84. self?.doneWithResults([
  85. "openURL": "mastodon://status/\(foundStatus.id)"
  86. ])
  87. } else if let foundHashtag = value.hashtags.first {
  88. self?.continueWithSearch(foundHashtag.name)
  89. } else {
  90. self?.continueWithSearch(url)
  91. }
  92. }
  93. .store(in: &cancellables)
  94. }
  95. }
  96. // Fallback to In-App Search
  97. private extension ActionRequestHandler {
  98. func continueWithSearch(_ query: String) {
  99. guard
  100. let url = URL(string: query),
  101. let host = url.host
  102. else {
  103. return doneWithInvalidLink()
  104. }
  105. Mastodon.API
  106. .Instance
  107. .instance(
  108. session: .shared,
  109. domain: host
  110. )
  111. .receive(on: DispatchQueue.main)
  112. .sink { _ in
  113. // no-op
  114. } receiveValue: { [weak self] response in
  115. guard response.value.version != nil else {
  116. self?.doneWithInvalidLink()
  117. return
  118. }
  119. guard let query = query.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
  120. self?.doneWithInvalidLink()
  121. return
  122. }
  123. self?.doneWithResults(
  124. ["openURL": "mastodon://search?query=\(query)"]
  125. )
  126. }
  127. .store(in: &cancellables)
  128. }
  129. }
  130. // Action response handling
  131. private extension ActionRequestHandler {
  132. func doneWithInvalidLink() {
  133. doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError])
  134. }
  135. func doneWithResults(_ resultsForJavaScriptFinalizeArg: [String: Any]?) {
  136. if let resultsForJavaScriptFinalize = resultsForJavaScriptFinalizeArg {
  137. let resultsDictionary = [NSExtensionJavaScriptFinalizeArgumentKey: resultsForJavaScriptFinalize]
  138. let resultsProvider = NSItemProvider(item: resultsDictionary as NSDictionary, typeIdentifier: UTType.propertyList.identifier)
  139. let resultsItem = NSExtensionItem()
  140. resultsItem.attachments = [resultsProvider]
  141. self.extensionContext!.completeRequest(returningItems: [resultsItem], completionHandler: nil)
  142. } else {
  143. self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
  144. }
  145. self.extensionContext = nil
  146. }
  147. }