Create CommonErrors module.
This commit is contained in:
parent
fbc0c72cd5
commit
2eb14ada1f
|
@ -20,7 +20,8 @@ let package = Package(
|
|||
.package(path: "../SyncDatabase"),
|
||||
.package(path: "../Core"),
|
||||
.package(path: "../CloudKitExtras"),
|
||||
.package(path: "../ReaderAPI")
|
||||
.package(path: "../ReaderAPI"),
|
||||
.package(path: "../CommonErrors")
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
|
@ -35,7 +36,8 @@ let package = Package(
|
|||
"Database",
|
||||
"Core",
|
||||
"CloudKitExtras",
|
||||
"ReaderAPI"
|
||||
"ReaderAPI",
|
||||
"CommonErrors"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
|
|
|
@ -8,13 +8,11 @@
|
|||
|
||||
import Foundation
|
||||
import Web
|
||||
import CommonErrors
|
||||
|
||||
public enum AccountError: LocalizedError {
|
||||
|
||||
case createErrorNotFound
|
||||
case createErrorAlreadySubscribed
|
||||
case opmlImportInProgress
|
||||
case wrappedError(error: Error, accountID: String, accountName: String)
|
||||
typealias AccountError = CommonError // Temporary, for compatibility with existing code
|
||||
|
||||
public extension CommonError {
|
||||
|
||||
@MainActor public var account: Account? {
|
||||
if case .wrappedError(_, let accountID, _) = self {
|
||||
|
@ -23,78 +21,8 @@ public enum AccountError: LocalizedError {
|
|||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@MainActor public static func wrappedError(error: Error, account: Account) -> AccountError {
|
||||
wrappedError(error: error, accountID: account.accountID, accountName: account.nameForDisplay)
|
||||
}
|
||||
|
||||
@MainActor public var isCredentialsError: Bool {
|
||||
if case .wrappedError(let error, _, _) = self {
|
||||
if case TransportError.httpError(let status) = error {
|
||||
return isCredentialsError(status: status)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .createErrorNotFound:
|
||||
return NSLocalizedString("The feed couldn’t be found and can’t be added.", comment: "Not found")
|
||||
case .createErrorAlreadySubscribed:
|
||||
return NSLocalizedString("You are already subscribed to this feed and can’t add it again.", comment: "Already subscribed")
|
||||
case .opmlImportInProgress:
|
||||
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
|
||||
case .wrappedError(let error, _, let accountName):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String
|
||||
} else {
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
default:
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .createErrorNotFound:
|
||||
return nil
|
||||
case .createErrorAlreadySubscribed:
|
||||
return nil
|
||||
case .wrappedError(let error, _, _):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
|
||||
} else {
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension AccountError {
|
||||
|
||||
func unknownError(_ error: Error, _ accountName: String) -> String {
|
||||
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String
|
||||
}
|
||||
|
||||
func isCredentialsError(status: Int) -> Bool {
|
||||
return status == 401 || status == 403
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -16,26 +16,6 @@ import Database
|
|||
import Core
|
||||
import ReaderAPI
|
||||
|
||||
public enum ReaderAPIAccountDelegateError: LocalizedError {
|
||||
case unknown
|
||||
case invalidParameter
|
||||
case invalidResponse
|
||||
case urlNotFound
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return NSLocalizedString("An unexpected error occurred.", comment: "An unexpected error occurred.")
|
||||
case .invalidParameter:
|
||||
return NSLocalizedString("An invalid parameter was passed.", comment: "An invalid parameter was passed.")
|
||||
case .invalidResponse:
|
||||
return NSLocalizedString("There was an invalid response from the server.", comment: "There was an invalid response from the server.")
|
||||
case .urlNotFound:
|
||||
return NSLocalizedString("The API URL wasn't found.", comment: "The API URL wasn't found.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||
|
||||
private let variant: ReaderAPIVariant
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,30 @@
|
|||
// swift-tools-version: 5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "CommonErrors",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "CommonErrors",
|
||||
targets: ["CommonErrors"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../Web"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "CommonErrors",
|
||||
dependencies: [
|
||||
"Web"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "CommonErrorsTests",
|
||||
dependencies: ["CommonErrors"]),
|
||||
]
|
||||
)
|
|
@ -0,0 +1,86 @@
|
|||
//
|
||||
// CommonError.swift
|
||||
//
|
||||
//
|
||||
// Created by Brent Simmons on 4/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Web
|
||||
|
||||
public enum CommonError: LocalizedError {
|
||||
|
||||
case createErrorNotFound
|
||||
case createErrorAlreadySubscribed
|
||||
case opmlImportInProgress
|
||||
case wrappedError(error: Error, accountID: String, accountName: String)
|
||||
|
||||
@MainActor public var isCredentialsError: Bool {
|
||||
if case .wrappedError(let error, _, _) = self {
|
||||
if case TransportError.httpError(let status) = error {
|
||||
return isCredentialsError(status: status)
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .createErrorNotFound:
|
||||
return NSLocalizedString("The feed couldn’t be found and can’t be added.", comment: "Not found")
|
||||
case .createErrorAlreadySubscribed:
|
||||
return NSLocalizedString("You are already subscribed to this feed and can’t add it again.", comment: "Already subscribed")
|
||||
case .opmlImportInProgress:
|
||||
return NSLocalizedString("An OPML import for this account is already running.", comment: "Import running")
|
||||
case .wrappedError(let error, _, let accountName):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
let localizedText = NSLocalizedString("Your “%@” credentials are invalid or expired.", comment: "Invalid or expired")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName) as String
|
||||
} else {
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
default:
|
||||
return unknownError(error, accountName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .createErrorNotFound:
|
||||
return nil
|
||||
case .createErrorAlreadySubscribed:
|
||||
return nil
|
||||
case .wrappedError(let error, _, _):
|
||||
switch error {
|
||||
case TransportError.httpError(let status):
|
||||
if isCredentialsError(status: status) {
|
||||
return NSLocalizedString("Please update your credentials for this account, or ensure that your account with this service is still valid.", comment: "Expired credentials")
|
||||
} else {
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
default:
|
||||
return NSLocalizedString("Please try again later.", comment: "Try later")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension CommonError {
|
||||
|
||||
func unknownError(_ error: Error, _ accountName: String) -> String {
|
||||
let localizedText = NSLocalizedString("An error occurred while processing the “%@” account: %@", comment: "Unknown error")
|
||||
return NSString.localizedStringWithFormat(localizedText as NSString, accountName, error.localizedDescription) as String
|
||||
}
|
||||
|
||||
func isCredentialsError(status: Int) -> Bool {
|
||||
return status == 401 || status == 403
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import XCTest
|
||||
@testable import CommonErrors
|
||||
|
||||
final class CommonErrorsTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// XCTest Documentation
|
||||
// https://developer.apple.com/documentation/xctest
|
||||
|
||||
// Defining Test Cases and Test Methods
|
||||
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||
}
|
||||
}
|
|
@ -1303,6 +1303,7 @@
|
|||
840D617E2029031C009BC708 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
840D61952029031D009BC708 /* NetNewsWire_iOSTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetNewsWire_iOSTests.swift; sourceTree = "<group>"; };
|
||||
840D61972029031D009BC708 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
8410C4A62BC221C900D4F799 /* CommonErrors */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = CommonErrors; sourceTree = "<group>"; };
|
||||
841550F42B9E3F8000D4B345 /* Database */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Database; sourceTree = "<group>"; };
|
||||
841550F52B9E4D6800D4B345 /* FMDB */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FMDB; sourceTree = "<group>"; };
|
||||
84162A142038C12C00035290 /* MarkCommandValidationStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkCommandValidationStatus.swift; sourceTree = "<group>"; };
|
||||
|
@ -2356,6 +2357,7 @@
|
|||
51C452B22265141B00C03939 /* Frameworks */,
|
||||
51CD32C624D2DEF9009ABAEF /* Account */,
|
||||
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,
|
||||
8410C4A62BC221C900D4F799 /* CommonErrors */,
|
||||
51CD32C424D2CF1D009ABAEF /* Articles */,
|
||||
51CD32C324D2CD57009ABAEF /* ArticlesDatabase */,
|
||||
51CD32C724D2E06C009ABAEF /* Secrets */,
|
||||
|
|
|
@ -11,12 +11,20 @@ let package = Package(
|
|||
targets: ["ReaderAPI"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../FoundationExtras")
|
||||
.package(path: "../FoundationExtras"),
|
||||
.package(path: "../Web"),
|
||||
.package(path: "../Secrets"),
|
||||
.package(path: "../CommonErrors"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "ReaderAPI",
|
||||
dependencies: ["FoundationExtras"],
|
||||
dependencies: [
|
||||
"FoundationExtras",
|
||||
"Web",
|
||||
"Secrets",
|
||||
"CommonErrors"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
]
|
||||
|
|
|
@ -9,14 +9,23 @@
|
|||
import Foundation
|
||||
import Web
|
||||
import Secrets
|
||||
import ReaderAPI
|
||||
import CommonErrors
|
||||
|
||||
enum CreateReaderAPISubscriptionResult {
|
||||
public protocol ReaderAPICallerDelegate: AnyObject {
|
||||
|
||||
var endpointURL: URL? { get }
|
||||
|
||||
var lastArticleFetchStartTime: Date? { get set }
|
||||
var lastArticleFetchEndTime: Date? { get set }
|
||||
}
|
||||
|
||||
public enum CreateReaderAPISubscriptionResult: Sendable {
|
||||
|
||||
case created(ReaderAPISubscription)
|
||||
case notFound
|
||||
}
|
||||
|
||||
@MainActor final class ReaderAPICaller: NSObject {
|
||||
@MainActor final class ReaderAPICaller {
|
||||
|
||||
enum ItemIDType {
|
||||
case unread
|
||||
|
@ -54,7 +63,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
private var accessToken: String?
|
||||
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
weak var delegate: ReaderAPICallerDelegate?
|
||||
|
||||
var variant: ReaderAPIVariant = .generic
|
||||
var credentials: Credentials?
|
||||
|
@ -69,10 +78,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
get {
|
||||
switch variant {
|
||||
case .generic, .freshRSS:
|
||||
guard let accountMetadata = accountMetadata else {
|
||||
return nil
|
||||
}
|
||||
return accountMetadata.endpointURL
|
||||
return delegate?.endpointURL
|
||||
default:
|
||||
return URL(string: variant.host)
|
||||
}
|
||||
|
@ -80,14 +86,14 @@ enum CreateReaderAPISubscriptionResult {
|
|||
}
|
||||
|
||||
init(transport: Transport, secretsProvider: SecretsProvider) {
|
||||
|
||||
self.transport = transport
|
||||
self.secretsProvider = secretsProvider
|
||||
|
||||
var urlHostAllowed = CharacterSet.urlHostAllowed
|
||||
urlHostAllowed.remove("+")
|
||||
urlHostAllowed.remove("&")
|
||||
uriComponentAllowed = urlHostAllowed
|
||||
super.init()
|
||||
self.uriComponentAllowed = urlHostAllowed
|
||||
}
|
||||
|
||||
func cancelAll() {
|
||||
|
@ -100,7 +106,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw CredentialsError.incompleteCredentials
|
||||
}
|
||||
|
||||
var request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.login.rawValue), credentials: credentials)
|
||||
var request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.login.rawValue), readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
do {
|
||||
|
@ -134,7 +140,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
} catch {
|
||||
if let transportError = error as? TransportError, case .httpError(let code) = transportError, code == 404 {
|
||||
throw ReaderAPIAccountDelegateError.urlNotFound
|
||||
throw ReaderAPIError.urlNotFound
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
|
@ -153,7 +159,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw CredentialsError.incompleteCredentials
|
||||
}
|
||||
|
||||
var request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.token.rawValue), credentials: credentials)
|
||||
var request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.token.rawValue), readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
let (_, data) = try await transport.send(request: request)
|
||||
|
@ -185,7 +191,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw TransportError.noURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
var request = URLRequest(url: callURL, readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
let (_, wrapper) = try await transport.send(request: request, resultType: ReaderAPITagContainer.self)
|
||||
|
@ -200,13 +206,13 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
let token = try await requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.renameTag.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.renameTag.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
||||
guard let encodedOldName = self.encodeForURLPath(oldName), let encodedNewName = self.encodeForURLPath(newName) else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
throw ReaderAPIError.invalidParameter
|
||||
}
|
||||
|
||||
let oldTagName = "user/-/label/\(encodedOldName)"
|
||||
|
@ -217,18 +223,15 @@ enum CreateReaderAPISubscriptionResult {
|
|||
}
|
||||
|
||||
|
||||
func deleteTag(folder: Folder) async throws {
|
||||
func deleteTag(folderExternalID: String) async throws {
|
||||
|
||||
guard let baseURL = apiBaseURL else {
|
||||
throw CredentialsError.incompleteCredentials
|
||||
}
|
||||
guard let folderExternalID = folder.externalID else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
}
|
||||
|
||||
|
||||
let token = try await self.requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.disableTag.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.disableTag.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
@ -252,14 +255,14 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw TransportError.noURL
|
||||
}
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
var request = URLRequest(url: callURL, readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
let (_, container) = try await transport.send(request: request, resultType: ReaderAPISubscriptionContainer.self)
|
||||
return container?.subscriptions
|
||||
}
|
||||
|
||||
func createSubscription(url: String, name: String?, folder: Folder?) async throws -> CreateReaderAPISubscriptionResult {
|
||||
func createSubscription(url: String, name: String?) async throws -> CreateReaderAPISubscriptionResult {
|
||||
|
||||
guard let baseURL = apiBaseURL else {
|
||||
throw CredentialsError.incompleteCredentials
|
||||
|
@ -270,13 +273,13 @@ enum CreateReaderAPISubscriptionResult {
|
|||
let callURL = baseURL
|
||||
.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue)
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: self.credentials)
|
||||
var request = URLRequest(url: callURL, readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
||||
guard let encodedFeedURL = self.encodeForURLPath(url) else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
throw ReaderAPIError.invalidParameter
|
||||
}
|
||||
|
||||
let postData = "T=\(token)&quickadd=\(encodedFeedURL)".data(using: String.Encoding.utf8)
|
||||
|
@ -293,10 +296,10 @@ enum CreateReaderAPISubscriptionResult {
|
|||
// There is no call to get a single subscription entry, so we get them all,
|
||||
// look up the one we just subscribed to and return that
|
||||
guard let subscriptions = try await retrieveSubscriptions() else {
|
||||
throw AccountError.createErrorNotFound
|
||||
throw CommonError.createErrorNotFound
|
||||
}
|
||||
guard let subscription = subscriptions.first(where: { $0.feedID == subResult.streamID }) else {
|
||||
throw AccountError.createErrorNotFound
|
||||
throw CommonError.createErrorNotFound
|
||||
}
|
||||
|
||||
return .created(subscription)
|
||||
|
@ -315,7 +318,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
let token = try await self.requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
@ -343,16 +346,15 @@ enum CreateReaderAPISubscriptionResult {
|
|||
private func changeSubscription(subscriptionID: String, removeTagName: String? = nil, addTagName: String? = nil, title: String? = nil) async throws {
|
||||
|
||||
guard removeTagName != nil || addTagName != nil || title != nil else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
throw ReaderAPIError.invalidParameter
|
||||
}
|
||||
guard let baseURL = apiBaseURL else {
|
||||
throw CredentialsError.incompleteCredentials
|
||||
return
|
||||
}
|
||||
|
||||
let token = try await requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
@ -383,7 +385,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
let token = try await requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.contents.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.contents.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
@ -404,7 +406,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
let (_, entryWrapper) = try await transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIEntryWrapper.self)
|
||||
|
||||
guard let entryWrapper else {
|
||||
throw ReaderAPIAccountDelegateError.invalidResponse
|
||||
throw ReaderAPIError.invalidResponse
|
||||
}
|
||||
|
||||
return entryWrapper.entries
|
||||
|
@ -424,7 +426,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
switch type {
|
||||
case .allForAccount:
|
||||
let since: Date = {
|
||||
if let lastArticleFetch = self.accountMetadata?.lastArticleFetchStartTime {
|
||||
if let lastArticleFetch = delegate?.lastArticleFetchStartTime {
|
||||
return lastArticleFetch
|
||||
} else {
|
||||
return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
|
||||
|
@ -436,7 +438,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
queryItems.append(URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue))
|
||||
case .allForFeed:
|
||||
guard let feedID else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
throw ReaderAPIError.invalidParameter
|
||||
}
|
||||
let sinceTimeInterval = (Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()).timeIntervalSince1970
|
||||
queryItems.append(URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval))))
|
||||
|
@ -456,7 +458,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw TransportError.noURL
|
||||
}
|
||||
|
||||
var request: URLRequest = URLRequest(url: callURL, credentials: credentials)
|
||||
var request: URLRequest = URLRequest(url: callURL, readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
let (response, entries) = try await transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self)
|
||||
|
@ -475,14 +477,14 @@ enum CreateReaderAPISubscriptionResult {
|
|||
|
||||
guard let continuation else {
|
||||
if type == .allForAccount {
|
||||
self.accountMetadata?.lastArticleFetchStartTime = dateInfo?.date
|
||||
self.accountMetadata?.lastArticleFetchEndTime = Date()
|
||||
delegate?.lastArticleFetchStartTime = dateInfo?.date
|
||||
delegate?.lastArticleFetchEndTime = Date()
|
||||
}
|
||||
return itemIDs
|
||||
}
|
||||
|
||||
guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
throw ReaderAPIAccountDelegateError.invalidParameter
|
||||
throw ReaderAPIError.invalidParameter
|
||||
}
|
||||
|
||||
var queryItems = urlComponents.queryItems!.filter({ $0.name != "c" })
|
||||
|
@ -493,7 +495,7 @@ enum CreateReaderAPISubscriptionResult {
|
|||
throw TransportError.noURL
|
||||
}
|
||||
|
||||
var request: URLRequest = URLRequest(url: callURL, credentials: credentials)
|
||||
var request: URLRequest = URLRequest(url: callURL, readerAPICredentials: credentials)
|
||||
addVariantHeaders(&request)
|
||||
|
||||
let (_, entries) = try await self.transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self)
|
||||
|
@ -554,7 +556,7 @@ private extension ReaderAPICaller {
|
|||
let token = try await requestAuthorizationToken(endpoint: baseURL)
|
||||
|
||||
// Do POST asking for data about all the new articles
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.editTag.rawValue), credentials: self.credentials)
|
||||
var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.editTag.rawValue), readerAPICredentials: self.credentials)
|
||||
self.addVariantHeaders(&request)
|
||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
|
@ -0,0 +1,29 @@
|
|||
//
|
||||
// ReaderAPIError.swift
|
||||
//
|
||||
//
|
||||
// Created by Brent Simmons on 4/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public enum ReaderAPIError: LocalizedError {
|
||||
|
||||
case unknown
|
||||
case invalidParameter
|
||||
case invalidResponse
|
||||
case urlNotFound
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .unknown:
|
||||
return NSLocalizedString("An unexpected error occurred.", comment: "An unexpected error occurred.")
|
||||
case .invalidParameter:
|
||||
return NSLocalizedString("An invalid parameter was passed.", comment: "An invalid parameter was passed.")
|
||||
case .invalidResponse:
|
||||
return NSLocalizedString("There was an invalid response from the server.", comment: "There was an invalid response from the server.")
|
||||
case .urlNotFound:
|
||||
return NSLocalizedString("The API URL wasn't found.", comment: "The API URL wasn't found.")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
//
|
||||
// URLRequest+ReaderAPI.swift
|
||||
//
|
||||
//
|
||||
// Created by Brent Simmons on 4/6/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
import Web
|
||||
|
||||
extension URLRequest {
|
||||
|
||||
init(url: URL, readerAPICredentials: Credentials?, conditionalGet: HTTPConditionalGetInfo? = nil) {
|
||||
|
||||
self.init(url: url)
|
||||
|
||||
guard let credentials = readerAPICredentials else {
|
||||
return
|
||||
}
|
||||
|
||||
let credentialsType = credentials.type
|
||||
precondition(credentialsType == .readerBasic || credentialsType == .readerAPIKey)
|
||||
|
||||
if credentialsType == .readerBasic {
|
||||
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
httpMethod = "POST"
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = [
|
||||
URLQueryItem(name: "Email", value: credentials.username),
|
||||
URLQueryItem(name: "Passwd", value: credentials.secret)
|
||||
]
|
||||
httpBody = postData.enhancedPercentEncodedQuery?.data(using: .utf8)
|
||||
|
||||
} else if credentialsType == .readerAPIKey {
|
||||
|
||||
let auth = "GoogleLogin auth=\(credentials.secret)"
|
||||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
}
|
||||
|
||||
guard let conditionalGet = conditionalGet else {
|
||||
return
|
||||
}
|
||||
|
||||
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
|
||||
// TODO: drop this check in late 2037.
|
||||
if let lastModified = conditionalGet.lastModified, !lastModified.contains("2038") {
|
||||
setValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
|
||||
}
|
||||
if let etag = conditionalGet.etag {
|
||||
setValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue