mirror of
https://github.com/SideStore/SideStore.git
synced 2026-02-08 22:33:26 +01:00
Supports downloading apps from locked Patreon posts
Uses cached Patreon session cookies to access post attachments despite no official API support.
This commit is contained in:
@@ -421,6 +421,7 @@
|
|||||||
D5A299872AAB9E4E00A3988D /* ProcessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */; };
|
D5A299872AAB9E4E00A3988D /* ProcessError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5FB7A1B2AA284ED00EF863D /* ProcessError.swift */; };
|
||||||
D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; };
|
D5A299882AAB9E4E00A3988D /* JITError.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A1D2E32AA50EB60066CACC /* JITError.swift */; };
|
||||||
D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; };
|
D5A299892AAB9E5900A3988D /* AppProcess.swift in Sources */ = {isa = PBXBuildFile; fileRef = D59A6B7D2AA9226C00F61259 /* AppProcess.swift */; };
|
||||||
|
D5A645212AF591980047D980 /* UTType+AltStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645202AF591980047D980 /* UTType+AltStore.swift */; };
|
||||||
D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */; };
|
D5A645232AF5B5C50047D980 /* PatreonAPI+Responses.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */; };
|
||||||
D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645242AF5BC7F0047D980 /* UserAccount.swift */; };
|
D5A645252AF5BC7F0047D980 /* UserAccount.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5A645242AF5BC7F0047D980 /* UserAccount.swift */; };
|
||||||
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
|
D5ACE84528E3B8450021CAB9 /* ClearAppCacheOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */; };
|
||||||
@@ -1095,6 +1096,7 @@
|
|||||||
D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteServiceDiscoveryTunnel.swift; sourceTree = "<group>"; };
|
D5A1D2E82AA512940066CACC /* RemoteServiceDiscoveryTunnel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteServiceDiscoveryTunnel.swift; sourceTree = "<group>"; };
|
||||||
D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.swift"; sourceTree = "<group>"; };
|
D5A1D2EA2AA513410066CACC /* URL+Tools.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URL+Tools.swift"; sourceTree = "<group>"; };
|
||||||
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
|
D5A2193329B14F94002229FC /* DeprecatedAPIs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeprecatedAPIs.swift; sourceTree = "<group>"; };
|
||||||
|
D5A645202AF591980047D980 /* UTType+AltStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UTType+AltStore.swift"; sourceTree = "<group>"; };
|
||||||
D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatreonAPI+Responses.swift"; sourceTree = "<group>"; };
|
D5A645222AF5B5C50047D980 /* PatreonAPI+Responses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "PatreonAPI+Responses.swift"; sourceTree = "<group>"; };
|
||||||
D5A645242AF5BC7F0047D980 /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = "<group>"; };
|
D5A645242AF5BC7F0047D980 /* UserAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAccount.swift; sourceTree = "<group>"; };
|
||||||
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = "<group>"; };
|
D5ACE84428E3B8450021CAB9 /* ClearAppCacheOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClearAppCacheOperation.swift; sourceTree = "<group>"; };
|
||||||
@@ -2049,6 +2051,7 @@
|
|||||||
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
D54058BA2A1D8FE3008CCC58 /* UIColor+AltStore.swift */,
|
||||||
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
D5FB28EB2ADDF68D00A1C337 /* UIFontDescriptor+Bold.swift */,
|
||||||
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
|
D56915052AD5D75B00A2B747 /* Regex+Permissions.swift */,
|
||||||
|
D5A645202AF591980047D980 /* UTType+AltStore.swift */,
|
||||||
);
|
);
|
||||||
path = Extensions;
|
path = Extensions;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -3334,6 +3337,7 @@
|
|||||||
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
BFB3645A2325985F00CD0EB1 /* FindServerOperation.swift in Sources */,
|
||||||
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */,
|
D52EF2BE2A0594550096C377 /* AppDetailCollectionViewController.swift in Sources */,
|
||||||
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
BF3BEFBF2408673400DE7D55 /* FetchProvisioningProfilesOperation.swift in Sources */,
|
||||||
|
D5A645212AF591980047D980 /* UTType+AltStore.swift in Sources */,
|
||||||
D5F982212AB910180045751F /* AppScreenshotsViewController.swift in Sources */,
|
D5F982212AB910180045751F /* AppScreenshotsViewController.swift in Sources */,
|
||||||
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
|
BFF0B69023219C6D007A79E1 /* PatreonComponents.swift in Sources */,
|
||||||
BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */,
|
BFBE0004250ACFFB0080826E /* ViewApp.intentdefinition in Sources */,
|
||||||
|
|||||||
14
AltStore/Extensions/UTType+AltStore.swift
Normal file
14
AltStore/Extensions/UTType+AltStore.swift
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
//
|
||||||
|
// UTType+AltStore.swift
|
||||||
|
// AltStore
|
||||||
|
//
|
||||||
|
// Created by Riley Testut on 11/3/23.
|
||||||
|
// Copyright © 2023 Riley Testut. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
|
extension UTType
|
||||||
|
{
|
||||||
|
static let ipa = UTType(importedAs: "com.apple.itunes.ipa")
|
||||||
|
}
|
||||||
@@ -195,6 +195,8 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>public.filename-extension</key>
|
<key>public.filename-extension</key>
|
||||||
<string>ipa</string>
|
<string>ipa</string>
|
||||||
|
<key>public.mime-type</key>
|
||||||
|
<string>application/x-ios-app</string>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
<dict>
|
<dict>
|
||||||
|
|||||||
@@ -1549,8 +1549,22 @@ private extension AppManager
|
|||||||
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
progress.addChild(installOperation.progress, withPendingUnitCount: 30)
|
||||||
installOperation.addDependency(sendAppOperation)
|
installOperation.addDependency(sendAppOperation)
|
||||||
|
|
||||||
let operations = [downloadOperation, verifyOperation, removeAppExtensionsOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, deactivateAppsOperation, patchAppOperation, resignAppOperation, sendAppOperation, installOperation]
|
let operations = [downloadOperation, verifyOperation, deactivateAppsOperation, removeAppExtensionsOperation, patchAppOperation, refreshAnisetteDataOperation, fetchProvisioningProfilesOperation, resignAppOperation, sendAppOperation, installOperation]
|
||||||
group.add(operations)
|
group.add(operations)
|
||||||
|
|
||||||
|
if let storeApp = downloadingApp.storeApp, storeApp.isPledgeRequired
|
||||||
|
{
|
||||||
|
// Patreon apps may require authenticating with WebViewController,
|
||||||
|
// so make sure to run DownloadAppOperation serially.
|
||||||
|
self.run([downloadOperation], context: group.context, requiresSerialQueue: true)
|
||||||
|
|
||||||
|
if let index = operations.firstIndex(of: downloadOperation)
|
||||||
|
{
|
||||||
|
// Remove downloadOperation from operations to prevent running it twice.
|
||||||
|
operations.remove(at: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
self.run(operations, context: group.context)
|
self.run(operations, context: group.context)
|
||||||
|
|
||||||
return progress
|
return progress
|
||||||
|
|||||||
@@ -7,10 +7,12 @@
|
|||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import Roxas
|
import WebKit
|
||||||
|
import UniformTypeIdentifiers
|
||||||
|
|
||||||
import AltStoreCore
|
import AltStoreCore
|
||||||
import AltSign
|
import AltSign
|
||||||
|
import Roxas
|
||||||
|
|
||||||
@objc(DownloadAppOperation)
|
@objc(DownloadAppOperation)
|
||||||
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
final class DownloadAppOperation: ResultOperation<ALTApplication>
|
||||||
@@ -25,6 +27,8 @@ final class DownloadAppOperation: ResultOperation<ALTApplication>
|
|||||||
private let session = URLSession(configuration: .default)
|
private let session = URLSession(configuration: .default)
|
||||||
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
private let temporaryDirectory = FileManager.default.uniqueTemporaryURL()
|
||||||
|
|
||||||
|
private var downloadPatreonAppContinuation: CheckedContinuation<URL, Error>?
|
||||||
|
|
||||||
init(app: AppProtocol, destinationURL: URL, context: InstallAppOperationContext)
|
init(app: AppProtocol, destinationURL: URL, context: InstallAppOperationContext)
|
||||||
{
|
{
|
||||||
self.app = app
|
self.app = app
|
||||||
@@ -183,11 +187,43 @@ private extension DownloadAppOperation {
|
|||||||
|
|
||||||
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
func downloadIPA(from sourceURL: URL, completionHandler: @escaping (Result<ALTApplication, Error>) -> Void)
|
||||||
{
|
{
|
||||||
func finishOperation(_ result: Result<URL, Error>)
|
Task<Void, Never>.detached(priority: .userInitiated) {
|
||||||
{
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
let fileURL = try result.get()
|
let fileURL: URL
|
||||||
|
|
||||||
|
if sourceURL.isFileURL
|
||||||
|
{
|
||||||
|
fileURL = sourceURL
|
||||||
|
self.progress.completedUnitCount += 3
|
||||||
|
}
|
||||||
|
else if let isPledged = await self.context.$appVersion.perform({ $0?.app?.isPledged }), !isPledged
|
||||||
|
{
|
||||||
|
// Not pledged, so just show Patreon page.
|
||||||
|
guard let presentingViewController = self.context.presentingViewController,
|
||||||
|
let patreonURL = await self.context.$appVersion.perform({ $0?.app?.source?.patreonURL })
|
||||||
|
else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||||
|
|
||||||
|
// Intercept downloads just in case they are in fact pledged.
|
||||||
|
fileURL = try await self.downloadFromPatreon(patreonURL, presentingViewController: presentingViewController)
|
||||||
|
}
|
||||||
|
else if let host = sourceURL.host, host.lowercased().hasSuffix("patreon.com") && sourceURL.path.lowercased() == "/file"
|
||||||
|
{
|
||||||
|
// Patreon app
|
||||||
|
fileURL = try await self.downloadPatreonApp(from: sourceURL)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Regular app
|
||||||
|
fileURL = try await self.downloadFile(from: sourceURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer {
|
||||||
|
if !sourceURL.isFileURL
|
||||||
|
{
|
||||||
|
try? FileManager.default.removeItem(at: fileURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var isDirectory: ObjCBool = false
|
var isDirectory: ObjCBool = false
|
||||||
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
guard FileManager.default.fileExists(atPath: fileURL.path, isDirectory: &isDirectory) else { throw OperationError.appNotFound(name: self.appName) }
|
||||||
@@ -223,31 +259,26 @@ private extension DownloadAppOperation {
|
|||||||
completionHandler(.failure(error))
|
completionHandler(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if sourceURL.isFileURL
|
func downloadFile(from downloadURL: URL) async throws -> URL
|
||||||
{
|
{
|
||||||
finishOperation(.success(sourceURL))
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
let downloadTask = self.session.downloadTask(with: downloadURL) { (fileURL, response, error) in
|
||||||
self.progress.completedUnitCount += 3
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
let downloadTask = self.session.downloadTask(with: sourceURL) { (fileURL, response, error) in
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
if let response = response as? HTTPURLResponse
|
if let response = response as? HTTPURLResponse
|
||||||
{
|
{
|
||||||
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: sourceURL]) }
|
guard response.statusCode != 403 else { throw URLError(.noPermissionsToReadFile) }
|
||||||
|
guard response.statusCode != 404 else { throw CocoaError(.fileNoSuchFile, userInfo: [NSURLErrorKey: downloadURL]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
let (fileURL, _) = try Result((fileURL, response), error).get()
|
let (fileURL, _) = try Result((fileURL, response), error).get()
|
||||||
finishOperation(.success(fileURL))
|
continuation.resume(returning: fileURL)
|
||||||
|
|
||||||
try? FileManager.default.removeItem(at: fileURL)
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
finishOperation(.failure(error))
|
continuation.resume(throwing: error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
self.progress.addChild(downloadTask.progress, withPendingUnitCount: 3)
|
||||||
@@ -255,6 +286,157 @@ private extension DownloadAppOperation {
|
|||||||
downloadTask.resume()
|
downloadTask.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func downloadPatreonApp(from patreonURL: URL) async throws -> URL
|
||||||
|
{
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// User is pledged to this app, attempt to download.
|
||||||
|
|
||||||
|
let fileURL = try await self.downloadFile(from: patreonURL)
|
||||||
|
return fileURL
|
||||||
|
}
|
||||||
|
catch URLError.noPermissionsToReadFile
|
||||||
|
{
|
||||||
|
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||||
|
|
||||||
|
// Attempt to sign-in again in case our Patreon session has expired.
|
||||||
|
try await withCheckedThrowingContinuation { continuation in
|
||||||
|
PatreonAPI.shared.authenticate(presentingViewController: presentingViewController) { result in
|
||||||
|
do
|
||||||
|
{
|
||||||
|
let account = try result.get()
|
||||||
|
try account.managedObjectContext?.save()
|
||||||
|
|
||||||
|
continuation.resume()
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
// Success, so try to download once more now that we're definitely authenticated.
|
||||||
|
|
||||||
|
let fileURL = try await self.downloadFile(from: patreonURL)
|
||||||
|
return fileURL
|
||||||
|
}
|
||||||
|
catch URLError.noPermissionsToReadFile
|
||||||
|
{
|
||||||
|
// We know authentication succeeded, so failure must mean user isn't patron/on the correct tier,
|
||||||
|
// or that our hacky workaround for downloading Patreon attachments has failed.
|
||||||
|
// Either way, taking them directly to the post serves as a decent fallback.
|
||||||
|
|
||||||
|
return try await downloadFromPatreonPost()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadFromPatreonPost() async throws -> URL
|
||||||
|
{
|
||||||
|
guard let presentingViewController = self.context.presentingViewController else { throw OperationError.pledgeRequired(appName: self.appName) }
|
||||||
|
|
||||||
|
let downloadURL: URL
|
||||||
|
|
||||||
|
if let components = URLComponents(url: patreonURL, resolvingAgainstBaseURL: false),
|
||||||
|
let postItem = components.queryItems?.first(where: { $0.name == "h" }),
|
||||||
|
let postID = postItem.value,
|
||||||
|
let patreonPostURL = URL(string: "https://www.patreon.com/posts/" + postID)
|
||||||
|
{
|
||||||
|
downloadURL = patreonPostURL
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
downloadURL = patreonURL
|
||||||
|
}
|
||||||
|
|
||||||
|
return try await self.downloadFromPatreon(downloadURL, presentingViewController: presentingViewController)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func downloadFromPatreon(_ patreonURL: URL, presentingViewController: UIViewController) async throws -> URL
|
||||||
|
{
|
||||||
|
let webViewController = WebViewController(url: patreonURL)
|
||||||
|
webViewController.delegate = self
|
||||||
|
webViewController.webView.navigationDelegate = self
|
||||||
|
|
||||||
|
let navigationController = UINavigationController(rootViewController: webViewController)
|
||||||
|
presentingViewController.present(navigationController, animated: true)
|
||||||
|
|
||||||
|
let downloadURL: URL
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
defer {
|
||||||
|
navigationController.dismiss(animated: true)
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadURL = try await withCheckedThrowingContinuation { continuation in
|
||||||
|
self.downloadPatreonAppContinuation = continuation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let fileURL = try await self.downloadFile(from: downloadURL)
|
||||||
|
return fileURL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DownloadAppOperation: WebViewControllerDelegate
|
||||||
|
{
|
||||||
|
func webViewControllerDidFinish(_ webViewController: WebViewController)
|
||||||
|
{
|
||||||
|
guard let continuation = self.downloadPatreonAppContinuation else { return }
|
||||||
|
self.downloadPatreonAppContinuation = nil
|
||||||
|
|
||||||
|
continuation.resume(throwing: CancellationError())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension DownloadAppOperation: WKNavigationDelegate
|
||||||
|
{
|
||||||
|
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction) async -> WKNavigationActionPolicy
|
||||||
|
{
|
||||||
|
guard #available(iOS 14.5, *), navigationAction.shouldPerformDownload else { return .allow }
|
||||||
|
|
||||||
|
guard let continuation = self.downloadPatreonAppContinuation else { return .allow }
|
||||||
|
self.downloadPatreonAppContinuation = nil
|
||||||
|
|
||||||
|
if let downloadURL = navigationAction.request.url
|
||||||
|
{
|
||||||
|
continuation.resume(returning: downloadURL)
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
continuation.resume(throwing: URLError(.badURL))
|
||||||
|
}
|
||||||
|
|
||||||
|
return .cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy
|
||||||
|
{
|
||||||
|
// Called for Patreon attachments
|
||||||
|
|
||||||
|
guard !navigationResponse.canShowMIMEType else { return .allow }
|
||||||
|
|
||||||
|
guard let continuation = self.downloadPatreonAppContinuation else { return .allow }
|
||||||
|
self.downloadPatreonAppContinuation = nil
|
||||||
|
|
||||||
|
guard let response = navigationResponse.response as? HTTPURLResponse, let responseURL = response.url,
|
||||||
|
let mimeType = response.mimeType, let type = UTType(mimeType: mimeType),
|
||||||
|
type.conforms(to: .ipa) || type.conforms(to: .zip) || type.conforms(to: .application)
|
||||||
|
else {
|
||||||
|
continuation.resume(throwing: OperationError.invalidApp)
|
||||||
|
return .cancel
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation.resume(returning: responseURL)
|
||||||
|
|
||||||
|
return .cancel
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension DownloadAppOperation
|
private extension DownloadAppOperation
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ extension OperationError
|
|||||||
case serverNotFound = 1200
|
case serverNotFound = 1200
|
||||||
case connectionFailed = 1201
|
case connectionFailed = 1201
|
||||||
case connectionDropped = 1202
|
case connectionDropped = 1202
|
||||||
|
|
||||||
|
/* Pledges */
|
||||||
|
case pledgeRequired = 1401
|
||||||
|
case pledgeInactive = 1402
|
||||||
}
|
}
|
||||||
|
|
||||||
static var cancelled: CancellationError { CancellationError() }
|
static var cancelled: CancellationError { CancellationError() }
|
||||||
@@ -125,6 +129,14 @@ extension OperationError
|
|||||||
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
static func forbidden(failureReason: String? = nil, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
OperationError(code: .forbidden, failureReason: failureReason, sourceFile: file, sourceLine: line)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static func pledgeRequired(appName: String, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
|
OperationError(code: .pledgeRequired, appName: appName, sourceFile: file, sourceLine: line)
|
||||||
|
}
|
||||||
|
|
||||||
|
static func pledgeInactive(appName: String, file: String = #fileID, line: UInt = #line) -> OperationError {
|
||||||
|
OperationError(code: .pledgeInactive, appName: appName, sourceFile: file, sourceLine: line)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -205,6 +217,17 @@ struct OperationError: ALTLocalizedError {
|
|||||||
case .invalidParameters:
|
case .invalidParameters:
|
||||||
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
let message = self._failureReason.map { ": \n\($0)" } ?? "."
|
||||||
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
return String(format: NSLocalizedString("Invalid parameters%@", comment: ""), message)
|
||||||
|
case .serverNotFound: return NSLocalizedString("AltServer could not be found.", comment: "")
|
||||||
|
case .connectionFailed: return NSLocalizedString("A connection to AltServer could not be established.", comment: "")
|
||||||
|
case .connectionDropped: return NSLocalizedString("The connection to AltServer was dropped.", comment: "")
|
||||||
|
|
||||||
|
case .pledgeRequired:
|
||||||
|
let appName = self.appName ?? NSLocalizedString("This app", comment: "")
|
||||||
|
return String(format: NSLocalizedString("%@ requires an active pledge in order to be installed.", comment: ""), appName)
|
||||||
|
|
||||||
|
case .pledgeInactive:
|
||||||
|
let appName = self.appName ?? NSLocalizedString("this app", comment: "")
|
||||||
|
return String(format: NSLocalizedString("Your pledge is no longer active. Please renew it to continue using %@ normally.", comment: ""), appName)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -237,6 +237,8 @@ extension OperationError
|
|||||||
case .connectionFailed: return .connectionFailed
|
case .connectionFailed: return .connectionFailed
|
||||||
case .connectionDropped: return .connectionDropped
|
case .connectionDropped: return .connectionDropped
|
||||||
case .forbidden: return .forbidden()
|
case .forbidden: return .forbidden()
|
||||||
|
case .pledgeRequired: return .pledgeRequired(appName: "Delta")
|
||||||
|
case .pledgeInactive: return .pledgeInactive(appName: "Delta")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user