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 */; }; D6757A7C2157E01900721E32 /* XCBManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7B2157E01900721E32 /* XCBManager.swift */; };
D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCallbackURL.swift */; }; D6757A7E2157E02600721E32 /* XCallbackURL.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A7D2157E02600721E32 /* XCallbackURL.swift */; };
D6757A822157E8FA00721E32 /* XCBSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6757A812157E8FA00721E32 /* XCBSession.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, ); }; }; 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 */; }; D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BED173212667E900F02DA0 /* StatusTableViewCell.swift */; };
D6C94D852139DFD800CB5196 /* LargeImage.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = D6C94D842139DFD800CB5196 /* LargeImage.storyboard */; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; 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>"; }; D6C94D842139DFD800CB5196 /* LargeImage.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = LargeImage.storyboard; sourceTree = "<group>"; };
@ -578,6 +580,7 @@
D6757A7B2157E01900721E32 /* XCBManager.swift */, D6757A7B2157E01900721E32 /* XCBManager.swift */,
D6757A812157E8FA00721E32 /* XCBSession.swift */, D6757A812157E8FA00721E32 /* XCBSession.swift */,
D6757A7D2157E02600721E32 /* XCallbackURL.swift */, D6757A7D2157E02600721E32 /* XCallbackURL.swift */,
D679C09E215850EF00DA27FE /* XCBActions.swift */,
); );
path = XCallbackURL; path = XCallbackURL;
sourceTree = "<group>"; sourceTree = "<group>";
@ -951,6 +954,7 @@
D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */, D6333B772138D94E00CE884A /* ComposeMediaView.swift in Sources */,
04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */, 04ED00B121481ED800567C53 /* SteppedProgressView.swift in Sources */,
D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */, D663626421360D2300C9CBA2 /* AvatarStyle.swift in Sources */,
D679C09F215850EF00DA27FE /* XCBActions.swift in Sources */,
D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */, D6538945214D6D7500E3CEFC /* TableViewSwipeActionProvider.swift in Sources */,
D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */, D6BED174212667E900F02DA0 /* StatusTableViewCell.swift in Sources */,
D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */, D64D0AAD2128D88B005A6F37 /* LocalData.swift in Sources */,

View File

@ -39,4 +39,12 @@ class Preferences: Codable {
var defaultPostVisibility = Status.Visibility.public 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.performSegue(withIdentifier: "postComplete", sender: self)
self.xcbSession?.complete(with: .success, additionalData: [ self.xcbSession?.complete(with: .success, additionalData: [
"statusURL": status.url!.absoluteString, "statusURL": status.url?.absoluteString,
"statusURI": status.uri "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 { class XCBManager {
static var specs: [XCallbackURLSpec] = [ 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? static var currentSession: XCBSession?
@ -25,30 +25,10 @@ class XCBManager {
return false return false
} }
static func didComplete(session type: XCBSessionType, result: XCBSessionResult) { static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession {
if let currentSession = currentSession,
currentSession.type == type {
currentSession.complete(with: result)
self.currentSession = nil
}
}
private static func createSession(type: XCBSessionType, url: XCallbackURL) -> XCBSession {
let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel) let session = XCBSession(type: type, success: url.success, error: url.error, cancel: url.cancel)
currentSession = session currentSession = session
return 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,15 +24,18 @@ class XCBSession {
func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) { func complete(with result: XCBSessionResult, additionalData: [String: String?]? = nil) {
let url = result == .success ? success : result == .error ? error : cancel let url = result == .success ? success : result == .error ? error : cancel
if var url = url { if var url = url {
XCBManager.currentSession = nil
if let additionalData = additionalData { if let additionalData = additionalData {
var components = URLComponents(url: url, resolvingAgainstBaseURL: true)! var components = URLComponents(url: url, resolvingAgainstBaseURL: true)!
components.queryItems = components.queryItems ?? [] components.queryItems = components.queryItems ?? []
components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init)) components.queryItems!.append(contentsOf: additionalData.map(URLQueryItem.init))
url = components.url! url = components.url!
} }
DispatchQueue.main.async {
UIApplication.shared.open(url, options: [:]) UIApplication.shared.open(url, options: [:])
} }
} }
}
} }
enum XCBSessionType { enum XCBSessionType {

View File

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