Add silent x-callback-url actions

User confirmation is required the first time a source app attempts to
run an action silently. Rejecting will always display the UI for the
given action (as if the silent parameter had been false).
This commit is contained in:
Shadowfacts 2018-09-23 19:04:39 -04:00
parent 6c3ae9ab14
commit af0d0612ba
Signed by untrusted user: shadowfacts
GPG Key ID: 94A5AB95422746E5
7 changed files with 94 additions and 24 deletions

View File

@ -95,6 +95,7 @@
D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCallbackURL.swift */; };
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.swift */; };
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */ = {isa = PBXBuildFile; fileRef = D679C09E215850EF00DA27FE /* XCBActions.swift */; };
D6BED170212663DA00F02DA0 /* SwiftSoup.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; };
@ -253,6 +254,7 @@
D6757A7B2157E01900721E32 /* XCBManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBManager.swift; sourceTree = "<group>"; };
D6757A7D2157E02600721E32 /* XCallbackURL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCallbackURL.swift; sourceTree = "<group>"; };
D6757A812157E8FA00721E32 /* XCBSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBSession.swift; sourceTree = "<group>"; };
D679C09E215850EF00DA27FE /* XCBActions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = XCBActions.swift; sourceTree = "<group>"; };
D6BED16E212663DA00F02DA0 /* SwiftSoup.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = SwiftSoup.framework; sourceTree = BUILT_PRODUCTS_DIR; };
D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusTableViewCell.swift; sourceTree = "<group>"; };
D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = "<group>"; };
@ -578,6 +580,7 @@
D6757A7B2157E01900721E32 /* XCBManager.swift */,
D6757A812157E8FA00721E32 /* XCBSession.swift */,
D6757A7D2157E02600721E32 /* XCallbackURL.swift */,
D679C09E215850EF00DA27FE /* XCBActions.swift */,
);
path = XCallbackURL;
sourceTree = "<group>";
@ -951,6 +954,7 @@
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,

View File

@ -39,4 +39,12 @@ class Preferences: Codable {
var defaultPostVisibility = Status.Visibility.public
var silentActions: [String: Permission] = [:]
}
extension Preferences {
enum Permission: String, Codable {
case undecided, accepted, rejected
}
}

View File

@ -254,7 +254,7 @@ class ComposeViewController: UIViewController {
self.performSegue(withIdentifier: "postComplete", sender: self)
self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url!.absoluteString,
"statusURL": status.url?.absoluteString,
"statusURI": status.uri
])
}

View File

@ -0,0 +1,73 @@
//
// XCBActions.swift
// Tusker
//
// Created by Shadowfacts on 9/23/18.
// Copyright © 2018 Shadowfacts. All rights reserved.
//
import UIKit
struct XCBActions {
// MARK: - Posts
static func postStatus(_ url: XCallbackURL) -> Bool {
let session = XCBManager.createSession(type: .postStatus, url: url)
let mentioning = url.arguments["mentioning"]
let text = url.arguments["text"]
func postStatusSilently() {
var status = ""
if let mentioning = mentioning { status += mentioning }
if let text = text { status += text }
let request = MastodonController.shared.client.createStatus(text: status)
MastodonController.shared.client.run(request) { response in
if case let .success(status, _) = response {
session.complete(with: .success, additionalData: [
"statusURL": status.url?.absoluteString,
"statusURI": status.uri
])
} else if case let .failure(error) = response {
session.complete(with: .error, additionalData: [
"error": error.localizedDescription
])
}
}
}
func showComposeStatus() {
let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text)
UIApplication.shared.keyWindow!.rootViewController!.present(vc, animated: true)
}
let silent = Bool(url.arguments["silent"] ?? "false") ?? false
if silent {
if let source = url.source {
let permission = Preferences.shared.silentActions[source] ?? .undecided
switch permission {
case .accepted:
postStatusSilently()
case .rejected:
showComposeStatus()
case .undecided:
let alert = UIAlertController(title: "\(source) wants to perform actions silently", message: "Accepting will allow \(source) to perform actions without your confirmation, rejecting will always prompt for confirmation.", preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Accept", style: .default, handler: { (_) in
Preferences.shared.silentActions[source] = .accepted
postStatusSilently()
}))
alert.addAction(UIAlertAction(title: "Reject", style: .default, handler: { (_) in
Preferences.shared.silentActions[source] = .rejected
showComposeStatus()
}))
UIApplication.shared.keyWindow!.rootViewController!.present(alert, animated: true)
}
} else {
session.complete(with: .error, additionalData: ["error": "Cannot perform silent action without source app, x-source parameter must be specified"])
}
} else {
showComposeStatus()
}
return true
}
}

View File

@ -11,7 +11,7 @@ import UIKit
class XCBManager {
static var specs: [XCallbackURLSpec] = [
XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true], handle: postStatus)
XCallbackURLSpec(path: "/postStatus", arguments: ["mentioning": true, "text": true, "silent": true], handle: XCBActions.postStatus)
]
static var currentSession: XCBSession?
@ -25,30 +25,10 @@ class XCBManager {
return false
}
static func didComplete(session type: XCBSessionType, result: XCBSessionResult) {
if let currentSession = currentSession,
currentSession.type == type {
currentSession.complete(with: result)
self.currentSession = nil
}
}
private static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession {
static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession {
let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel)
currentSession = session
return session
}
static func postStatus(_ url: XCallbackURL) -> Bool {
let mentioning = url.arguments["mentioning"]
let text = url.arguments["text"]
if let window = UIApplication.shared.keyWindow {
let session = createSession(type: .postStatus, url: url)
let vc = ComposeViewController.create(for: session, mentioning: mentioning, text: text)
window.rootViewController?.present(vc, animated: true)
return true
}
return false
}
}

View File

@ -24,13 +24,16 @@ class XCBSession {
func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) {
let url = result == .success ? success : result == .error ? error : cancel
if var url = url {
XCBManager.currentSession = nil
if let additionalData = additionalData {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = components.queryItems ?? []
components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init))
url = components.url!
}
UIApplication.shared.open(url, options: [:])
DispatchQueue.main.async {
UIApplication.shared.open(url, options: [:])
}
}
}
}

View File

@ -25,6 +25,7 @@ struct XCallbackURLSpec {
struct XCallbackURL {
let path: String
let arguments: [String: String]
let source: String?
let success: URL?
let error: URL?
let cancel: URL?
@ -37,6 +38,7 @@ struct XCallbackURL {
result[el.key] = value
}
})
source = queryItems.first(where: { $0.name == "x-source" }).flatMap { $0.value }
success = queryItems.first(where: { $0.name == "x-success" }).flatMap { $0.value }.flatMap { URL(string: $0) }
error = queryItems.first(where: { $0.name == "x-error" }).flatMap { $0.value }.flatMap { URL(string: $0) }
cancel = queryItems.first(where: { $0.name == "x-cancel" }).flatMap { $0.value }.flatMap { URL(string: $0) }