2023-01-10 16:40:20 +01:00
|
|
|
//
|
|
|
|
// ActionRequestHandler.swift
|
|
|
|
// OpenInActionExtension
|
|
|
|
//
|
|
|
|
// Created by Marcus Kida on 03.01.23.
|
|
|
|
//
|
|
|
|
|
2023-01-11 15:12:07 +01:00
|
|
|
import Combine
|
2023-01-10 16:40:20 +01:00
|
|
|
import UIKit
|
|
|
|
import MobileCoreServices
|
|
|
|
import UniformTypeIdentifiers
|
2023-01-16 23:36:00 +01:00
|
|
|
import MastodonCore
|
2023-01-11 15:12:07 +01:00
|
|
|
import MastodonSDK
|
|
|
|
import MastodonLocalization
|
2023-01-10 16:40:20 +01:00
|
|
|
|
|
|
|
class ActionRequestHandler: NSObject, NSExtensionRequestHandling {
|
|
|
|
var extensionContext: NSExtensionContext?
|
2023-01-11 15:12:07 +01:00
|
|
|
var cancellables = [AnyCancellable]()
|
2023-01-10 16:40:20 +01:00
|
|
|
|
2023-01-16 23:36:00 +01:00
|
|
|
/// 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()
|
|
|
|
|
2023-01-10 16:40:20 +01:00
|
|
|
func beginRequest(with context: NSExtensionContext) {
|
|
|
|
// Do not call super in an Action extension with no user interface
|
|
|
|
self.extensionContext = context
|
2023-01-11 13:09:23 +01:00
|
|
|
|
|
|
|
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 {
|
2023-01-11 15:12:07 +01:00
|
|
|
return doneWithInvalidLink()
|
2023-01-10 16:40:20 +01:00
|
|
|
}
|
|
|
|
|
2023-01-11 13:09:23 +01:00
|
|
|
itemProvider.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] item, error in
|
2023-01-10 16:40:20 +01:00
|
|
|
DispatchQueue.main.async {
|
2023-01-11 13:09:23 +01:00
|
|
|
guard
|
|
|
|
let dictionary = item as? NSDictionary,
|
|
|
|
let results = dictionary[NSExtensionJavaScriptPreprocessingResultsKey] as? NSDictionary
|
|
|
|
else {
|
2023-01-11 15:12:07 +01:00
|
|
|
self?.doneWithInvalidLink()
|
2023-01-11 13:09:23 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-01-16 23:36:00 +01:00
|
|
|
if let url = results["url"] as? String {
|
|
|
|
self?.performSearch(for: url)
|
2023-01-11 13:09:23 +01:00
|
|
|
} else {
|
2023-01-11 15:12:07 +01:00
|
|
|
self?.doneWithInvalidLink()
|
2023-01-11 13:09:23 +01:00
|
|
|
}
|
2023-01-10 16:40:20 +01:00
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-16 23:36:00 +01:00
|
|
|
// Search API
|
2023-01-10 16:40:20 +01:00
|
|
|
private extension ActionRequestHandler {
|
2023-01-16 23:36:00 +01:00
|
|
|
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)
|
|
|
|
|
2023-01-10 16:40:20 +01:00
|
|
|
}
|
2023-01-16 23:36:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Fallback to In-App Search
|
|
|
|
private extension ActionRequestHandler {
|
2023-01-11 15:12:07 +01:00
|
|
|
func continueWithSearch(_ query: String) {
|
|
|
|
guard
|
|
|
|
let url = URL(string: query),
|
2024-05-08 18:12:52 +02:00
|
|
|
let host = url.host,
|
|
|
|
let activeAuthenticationBox = Self.appContext
|
|
|
|
.authenticationService
|
|
|
|
.mastodonAuthenticationBoxes
|
|
|
|
.first
|
|
|
|
|
2023-01-11 15:12:07 +01:00
|
|
|
else {
|
|
|
|
return doneWithInvalidLink()
|
2023-01-11 13:09:23 +01:00
|
|
|
}
|
2023-01-11 15:12:07 +01:00
|
|
|
|
|
|
|
Mastodon.API
|
|
|
|
.Instance
|
|
|
|
.instance(
|
|
|
|
session: .shared,
|
2024-05-08 18:12:52 +02:00
|
|
|
authorization: activeAuthenticationBox.userAuthorization,
|
2023-01-11 15:12:07 +01:00
|
|
|
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)
|
|
|
|
}
|
2023-01-16 23:36:00 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// Action response handling
|
|
|
|
private extension ActionRequestHandler {
|
2023-01-11 15:12:07 +01:00
|
|
|
func doneWithInvalidLink() {
|
|
|
|
doneWithResults(["alert": L10n.Extension.OpenIn.invalidLinkError])
|
2023-01-10 16:40:20 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|