From 9d362c8262a0fa8c6fde2fbe407aab11785efd9b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sat, 2 Sep 2023 11:27:15 -0700 Subject: [PATCH] Make Feedbin a separate module. --- Account/Package.swift | 82 +++--- .../FeedbinAccountDelegate.swift | 54 +++- .../NewsBlurAccountDelegate+Internal.swift | 0 .../NewsBlurAccountDelegate.swift | 0 .../ReaderAPIAccountDelegate.swift | 0 NetNewsWire.xcodeproj/project.pbxproj | 2 + SyncClients/Feedbin/.gitignore | 9 + SyncClients/Feedbin/Package.swift | 36 +++ SyncClients/Feedbin/README.md | 3 + .../Sources}/Feedbin/FeedbinAPICaller.swift | 268 +++++++++++++----- .../Sources}/Feedbin/FeedbinDate.swift | 0 .../Sources}/Feedbin/FeedbinEntry.swift | 36 +-- .../Feedbin/FeedbinImportResult.swift | 6 +- .../Feedbin/FeedbinStarredEntry.swift | 0 .../Feedbin/FeedbinSubscription.swift | 28 +- .../Feedbin/Sources}/Feedbin/FeedbinTag.swift | 4 +- .../Sources}/Feedbin/FeedbinTagging.swift | 8 +- .../Sources}/Feedbin/FeedbinUnreadEntry.swift | 0 .../Tests/FeedbinTests/FeedbinTests.swift | 2 + 19 files changed, 364 insertions(+), 174 deletions(-) rename Account/Sources/Account/{Feedbin => AccountDelegates}/FeedbinAccountDelegate.swift (97%) rename Account/Sources/Account/{NewsBlur/Internals => AccountDelegates}/NewsBlurAccountDelegate+Internal.swift (100%) rename Account/Sources/Account/{NewsBlur => AccountDelegates}/NewsBlurAccountDelegate.swift (100%) rename Account/Sources/Account/{ReaderAPI => AccountDelegates}/ReaderAPIAccountDelegate.swift (100%) create mode 100644 SyncClients/Feedbin/.gitignore create mode 100644 SyncClients/Feedbin/Package.swift create mode 100644 SyncClients/Feedbin/README.md rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinAPICaller.swift (67%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinDate.swift (100%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinEntry.swift (70%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinImportResult.swift (72%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinStarredEntry.swift (100%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinSubscription.swift (64%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinTag.swift (90%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinTagging.swift (79%) rename {Account/Sources/Account => SyncClients/Feedbin/Sources}/Feedbin/FeedbinUnreadEntry.swift (100%) create mode 100644 SyncClients/Feedbin/Tests/FeedbinTests/FeedbinTests.swift diff --git a/Account/Package.swift b/Account/Package.swift index dad306e8f..da1dd50a1 100644 --- a/Account/Package.swift +++ b/Account/Package.swift @@ -1,48 +1,35 @@ // swift-tools-version:5.7.1 import PackageDescription -var dependencies: [Package.Dependency] = [ - .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), - .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), - .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), - .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), -] - -#if swift(>=5.6) -dependencies.append(contentsOf: [ - .package(path: "../AccountError"), - .package(path: "../Articles"), - .package(path: "../ArticlesDatabase"), - .package(path: "../FeedFinder"), - .package(path: "../Secrets"), - .package(path: "../SyncDatabase"), - .package(path: "../SyncClients/NewsBlur"), - .package(path: "../SyncClients/ReaderAPI"), -]) -#else -dependencies.append(contentsOf: [ - .package(url: "../Articles", .upToNextMajor(from: "1.0.0")), - .package(url: "../ArticlesDatabase", .upToNextMajor(from: "1.0.0")), - .package(url: "../Secrets", .upToNextMajor(from: "1.0.0")), - .package(url: "../SyncDatabase", .upToNextMajor(from: "1.0.0")), -]) -#endif - let package = Package( - name: "Account", - defaultLocalization: "en", + name: "Account", + defaultLocalization: "en", platforms: [.macOS(.v13), .iOS(.v16)], - products: [ - .library( - name: "Account", + products: [ + .library( + name: "Account", type: .dynamic, - targets: ["Account"]), - ], - dependencies: dependencies, - targets: [ - .target( - name: "Account", - dependencies: [ + targets: ["Account"]), + ], + dependencies: [ + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSDatabase.git", .upToNextMajor(from: "2.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")), + .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), + .package(path: "../AccountError"), + .package(path: "../Articles"), + .package(path: "../ArticlesDatabase"), + .package(path: "../FeedFinder"), + .package(path: "../Secrets"), + .package(path: "../SyncDatabase"), + .package(path: "../SyncClients/NewsBlur"), + .package(path: "../SyncClients/ReaderAPI"), + .package(path: "../SyncClients/Feedbin"), + ], + targets: [ + .target( + name: "Account", + dependencies: [ "RSCore", "RSDatabase", "RSParser", @@ -54,16 +41,17 @@ let package = Package( "Secrets", "SyncDatabase", "NewsBlur", - "ReaderAPI" + "ReaderAPI", + "Feedbin" ], - linkerSettings: [ - .unsafeFlags(["-Xlinker", "-no_application_extension"]) - ]), - .testTarget( - name: "AccountTests", - dependencies: ["Account"], + linkerSettings: [ + .unsafeFlags(["-Xlinker", "-no_application_extension"]) + ]), + .testTarget( + name: "AccountTests", + dependencies: ["Account"], resources: [ .copy("JSON"), ]), - ] + ] ) diff --git a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift similarity index 97% rename from Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift rename to Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift index 292486efc..80d6f7d0f 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Account/Sources/Account/AccountDelegates/FeedbinAccountDelegate.swift @@ -15,6 +15,7 @@ import RSWeb import SyncDatabase import Secrets import FeedFinder +import Feedbin public enum FeedbinAccountDelegateError: String, Error { case invalidParameter = "There was an invalid parameter passed." @@ -37,25 +38,20 @@ public enum FeedbinAccountDelegateError: String, Error { } } - weak var accountMetadata: AccountMetadata? { - didSet { - caller.accountMetadata = accountMetadata - } - } + weak var accountMetadata: AccountMetadata? var refreshProgress = DownloadProgress(numberOfTasks: 0) init(dataFolder: String, transport: Transport?) { let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") - database = SyncDatabase(databaseFilePath: databaseFilePath) + self.database = SyncDatabase(databaseFilePath: databaseFilePath) if transport != nil { - - caller = FeedbinAPICaller(transport: transport!) - + self.caller = FeedbinAPICaller(transport: transport!) + } else { - + let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData sessionConfiguration.timeoutIntervalForRequest = 60.0 @@ -69,10 +65,10 @@ public enum FeedbinAccountDelegateError: String, Error { sessionConfiguration.httpAdditionalHeaders = userAgentHeaders } - caller = FeedbinAPICaller(transport: URLSession(configuration: sessionConfiguration)) - + self.caller = FeedbinAPICaller(transport: URLSession(configuration: sessionConfiguration)) } - + + self.caller.delegate = self } func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { @@ -647,7 +643,37 @@ public enum FeedbinAccountDelegateError: String, Error { } } -// MARK: Private +// MARK: - FeedbinAPICallerDelegate + +extension FeedbinAccountDelegate: FeedbinAPICallerDelegate { + + var lastArticleFetchStartTime: Date? { + accountMetadata?.lastArticleFetchStartTime + } + + func conditionalGetInfo(key: String) -> HTTPConditionalGetInfo? { + accountMetadata?.conditionalGetInfo[key] + } + + func setConditionalGetInfo(_ info: HTTPConditionalGetInfo, forKey key: String) { + if var conditionalGetInfo = accountMetadata?.conditionalGetInfo { + conditionalGetInfo[key] = info + } + else { + var conditionalGetInfo = [String: HTTPConditionalGetInfo]() + conditionalGetInfo[key] = info + accountMetadata?.conditionalGetInfo = conditionalGetInfo + } + } + + func createURLRequest(url: URL, credentials: Secrets.Credentials?, conditionalGet: RSWeb.HTTPConditionalGetInfo?) -> URLRequest { + URLRequest(url: url, credentials: credentials, conditionalGet: conditionalGet) + } + + +} + +// MARK: - Private private extension FeedbinAccountDelegate { diff --git a/Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift similarity index 100% rename from Account/Sources/Account/NewsBlur/Internals/NewsBlurAccountDelegate+Internal.swift rename to Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate+Internal.swift diff --git a/Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift similarity index 100% rename from Account/Sources/Account/NewsBlur/NewsBlurAccountDelegate.swift rename to Account/Sources/Account/AccountDelegates/NewsBlurAccountDelegate.swift diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift similarity index 100% rename from Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift rename to Account/Sources/Account/AccountDelegates/ReaderAPIAccountDelegate.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 37ef24180..b940fb59d 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -1345,6 +1345,7 @@ 841ABA5D20145E9200980E11 /* FolderInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderInspectorViewController.swift; sourceTree = ""; }; 841ABA5F20145EC100980E11 /* BuiltinSmartFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BuiltinSmartFeedInspectorViewController.swift; sourceTree = ""; }; 84208B732A9CEE2B009FE5B9 /* FeedFinder */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = FeedFinder; sourceTree = ""; }; + 84208B742A9CF1B3009FE5B9 /* Feedbin */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Feedbin; path = SyncClients/Feedbin; sourceTree = ""; }; 84216D0222128B9D0049B9B9 /* DetailWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailWebViewController.swift; sourceTree = ""; }; 842611891FCB67AA0086A189 /* FeedIconDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconDownloader.swift; sourceTree = ""; }; 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadataDownloader.swift; sourceTree = ""; }; @@ -2509,6 +2510,7 @@ 84208B732A9CEE2B009FE5B9 /* FeedFinder */, 8486EC3F2A9C2431007EF90D /* ReaderAPI */, 8486EC3E2A9BE083007EF90D /* NewsBlur */, + 84208B742A9CF1B3009FE5B9 /* Feedbin */, 51CD32C424D2CF1D009ABAEF /* Articles */, 51CD32C324D2CD57009ABAEF /* ArticlesDatabase */, 51CD32C724D2E06C009ABAEF /* Secrets */, diff --git a/SyncClients/Feedbin/.gitignore b/SyncClients/Feedbin/.gitignore new file mode 100644 index 000000000..3b2981208 --- /dev/null +++ b/SyncClients/Feedbin/.gitignore @@ -0,0 +1,9 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +DerivedData/ +.swiftpm/config/registries.json +.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata +.netrc diff --git a/SyncClients/Feedbin/Package.swift b/SyncClients/Feedbin/Package.swift new file mode 100644 index 000000000..78fb37d03 --- /dev/null +++ b/SyncClients/Feedbin/Package.swift @@ -0,0 +1,36 @@ +// swift-tools-version: 5.8 +// The swift-tools-version declares the minimum version of Swift required to build this package. + +import PackageDescription + +let package = Package( + name: "Feedbin", + platforms: [.macOS(.v13), .iOS(.v16)], + products: [ + // Products define the executables and libraries a package produces, and make them visible to other packages. + .library( + name: "Feedbin", + targets: ["Feedbin"]), + ], + dependencies: [ + .package(path: "../../Secrets"), + .package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")), + .package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")), + .package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2")) + ], + targets: [ + // Targets are the basic building blocks of a package. A target can define a module or a test suite. + // Targets can depend on other targets in this package, and on products in packages this package depends on. + .target( + name: "Feedbin", + dependencies: [ + "RSCore", + "RSParser", + "RSWeb", + "Secrets" + ]), + .testTarget( + name: "FeedbinTests", + dependencies: ["Feedbin"]), + ] +) diff --git a/SyncClients/Feedbin/README.md b/SyncClients/Feedbin/README.md new file mode 100644 index 000000000..304b78f01 --- /dev/null +++ b/SyncClients/Feedbin/README.md @@ -0,0 +1,3 @@ +# Feedbin + +A description of this package. diff --git a/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinAPICaller.swift similarity index 67% rename from Account/Sources/Account/Feedbin/FeedbinAPICaller.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinAPICaller.swift index e8e1d8ec1..d8a975162 100644 --- a/Account/Sources/Account/Feedbin/FeedbinAPICaller.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinAPICaller.swift @@ -14,21 +14,31 @@ import Foundation import RSWeb import Secrets -enum CreateSubscriptionResult { +public enum CreateSubscriptionResult { case created(FeedbinSubscription) case multipleChoice([FeedbinSubscriptionChoice]) case alreadySubscribed case notFound } -final class FeedbinAPICaller: NSObject { +public protocol FeedbinAPICallerDelegate: AnyObject { + + var lastArticleFetchStartTime: Date? { get } + + func conditionalGetInfo(key: String) -> HTTPConditionalGetInfo? + func setConditionalGetInfo(_: HTTPConditionalGetInfo, forKey: String) + + func createURLRequest(url: URL, credentials: Secrets.Credentials?, conditionalGet: HTTPConditionalGetInfo?) -> URLRequest +} + +public final class FeedbinAPICaller { - struct ConditionalGetKeys { - static let subscriptions = "subscriptions" - static let tags = "tags" - static let taggings = "taggings" - static let unreadEntries = "unreadEntries" - static let starredEntries = "starredEntries" + public struct ConditionalGetKeys { + public static let subscriptions = "subscriptions" + public static let tags = "tags" + public static let taggings = "taggings" + public static let unreadEntries = "unreadEntries" + public static let starredEntries = "starredEntries" } private let feedbinBaseURL = URL(string: "https://api.feedbin.com/v2/")! @@ -36,28 +46,30 @@ final class FeedbinAPICaller: NSObject { private var suspended = false private var lastBackdateStartTime: Date? - var credentials: Credentials? - weak var accountMetadata: AccountMetadata? + public var credentials: Credentials? + public weak var delegate: FeedbinAPICallerDelegate? - init(transport: Transport) { - super.init() + public init(transport: Transport) { self.transport = transport } /// Cancels all pending requests rejects any that come in later - func suspend() { + public func suspend() { transport.cancelAll() suspended = true } - func resume() { + public func resume() { suspended = false } - func validateCredentials(completion: @escaping (Result) -> Void) { - + public func validateCredentials(completion: @escaping (Result) -> Void) { + let callURL = feedbinBaseURL.appendingPathComponent("authentication.json") - let request = URLRequest(url: callURL, credentials: credentials) + guard let request = delegate?.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) else { + completion(.failure(TransportError.suspended)) + return + } transport.send(request: request) { result in @@ -85,10 +97,15 @@ final class FeedbinAPICaller: NSObject { } - func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { - + public func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("imports.json") - var request = URLRequest(url: callURL, credentials: credentials) + var request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in @@ -121,10 +138,15 @@ final class FeedbinAPICaller: NSObject { } - func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { + public func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("imports/\(importID).json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) transport.send(request: request, resultType: FeedbinImportResult.self) { result in @@ -144,11 +166,16 @@ final class FeedbinAPICaller: NSObject { } - func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { - + public func retrieveTags(completion: @escaping (Result<[FeedbinTag]?, Error>) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("tags.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + let conditionalGet = delegate.conditionalGetInfo(key: ConditionalGetKeys.tags) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) transport.send(request: request, resultType: [FeedbinTag].self) { result in @@ -169,9 +196,15 @@ final class FeedbinAPICaller: NSObject { } - func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { + public func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("tags.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinRenameTag(oldName: oldName, newName: newName) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in @@ -189,13 +222,18 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { - + public func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("subscriptions.json"), resolvingAgainstBaseURL: false)! callComponents.queryItems = [URLQueryItem(name: "mode", value: "extended")] - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] - let request = URLRequest(url: callComponents.url!, credentials: credentials, conditionalGet: conditionalGet) + let conditionalGet = delegate.conditionalGetInfo(key: ConditionalGetKeys.subscriptions) + let request = delegate.createURLRequest(url: callComponents.url!, credentials: credentials, conditionalGet: conditionalGet) transport.send(request: request, resultType: [FeedbinSubscription].self) { result in @@ -216,12 +254,17 @@ final class FeedbinAPICaller: NSObject { } - func createSubscription(url: String, completion: @escaping (Result) -> Void) { - + public func createSubscription(url: String, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + var callComponents = URLComponents(url: feedbinBaseURL.appendingPathComponent("subscriptions.json"), resolvingAgainstBaseURL: false)! callComponents.queryItems = [URLQueryItem(name: "mode", value: "extended")] - var request = URLRequest(url: callComponents.url!, credentials: credentials) + var request = delegate.createURLRequest(url: callComponents.url!, credentials: credentials, conditionalGet: nil) request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) let payload: Data @@ -295,9 +338,15 @@ final class FeedbinAPICaller: NSObject { } - func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { + public func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinUpdateSubscription(title: newName) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in @@ -315,9 +364,15 @@ final class FeedbinAPICaller: NSObject { } } - func deleteSubscription(subscriptionID: String, completion: @escaping (Result) -> Void) { + public func deleteSubscription(subscriptionID: String, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) transport.send(request: request, method: HTTPMethod.delete) { result in if self.suspended { completion(.failure(TransportError.suspended)) @@ -333,11 +388,16 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { + public func retrieveTaggings(completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + let conditionalGet = delegate.conditionalGetInfo(key: ConditionalGetKeys.taggings) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) transport.send(request: request, resultType: [FeedbinTagging].self) { result in if self.suspended { @@ -357,10 +417,15 @@ final class FeedbinAPICaller: NSObject { } - func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { + public func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("taggings.json") - var request = URLRequest(url: callURL, credentials: credentials) + var request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) let payload: Data @@ -396,9 +461,15 @@ final class FeedbinAPICaller: NSObject { } - func deleteTagging(taggingID: String, completion: @escaping (Result) -> Void) { + public func deleteTagging(taggingID: String, completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("taggings/\(taggingID).json") - var request = URLRequest(url: callURL, credentials: credentials) + var request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) transport.send(request: request, method: HTTPMethod.delete) { result in if self.suspended { @@ -415,8 +486,13 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) { + public func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([FeedbinEntry]?), Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + guard !articleIDs.isEmpty else { completion(.success(([FeedbinEntry]()))) return @@ -431,7 +507,7 @@ final class FeedbinAPICaller: NSObject { URLQueryItem(name: "ids", value: paramIDs), URLQueryItem(name: "mode", value: "extended") ]) - let request = URLRequest(url: url!, credentials: credentials) + let request = delegate.createURLRequest(url: url!, credentials: credentials, conditionalGet: nil) transport.send(request: request, resultType: [FeedbinEntry].self) { result in @@ -451,8 +527,13 @@ final class FeedbinAPICaller: NSObject { } - func retrieveEntries(feedID: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) { + public func retrieveEntries(feedID: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() let sinceString = FeedbinDate.formatter.string(from: since) @@ -463,7 +544,7 @@ final class FeedbinAPICaller: NSObject { URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended") ]) - let request = URLRequest(url: url!, credentials: credentials) + let request = delegate.createURLRequest(url: url!, credentials: credentials, conditionalGet: nil) transport.send(request: request, resultType: [FeedbinEntry].self) { result in @@ -486,7 +567,12 @@ final class FeedbinAPICaller: NSObject { } - func retrieveEntries(completion: @escaping (Result<([FeedbinEntry]?, String?, Date?, Int?), Error>) -> Void) { + public func retrieveEntries(completion: @escaping (Result<([FeedbinEntry]?, String?, Date?, Int?), Error>) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } // If this is an initial sync, go and grab the previous 3 months of entries. If not, use the last // article fetch to only get the articles **published** since the last article fetch. @@ -495,7 +581,7 @@ final class FeedbinAPICaller: NSObject { // getting **updated** articles that normally wouldn't be found with a regular fetch. // https://github.com/Ranchero-Software/NetNewsWire/issues/2549#issuecomment-722341356 let since: Date = { - if let lastArticleFetch = accountMetadata?.lastArticleFetchStartTime { + if let lastArticleFetch = delegate.lastArticleFetchStartTime { if let lastBackdateStartTime = lastBackdateStartTime { if lastBackdateStartTime.byAdding(days: 1) < lastArticleFetch { self.lastBackdateStartTime = lastArticleFetch @@ -520,7 +606,8 @@ final class FeedbinAPICaller: NSObject { URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended") ]) - let request = URLRequest(url: url!, credentials: credentials) + + let request = delegate.createURLRequest(url: url!, credentials: credentials, conditionalGet: nil) transport.send(request: request, resultType: [FeedbinEntry].self) { result in @@ -546,14 +633,18 @@ final class FeedbinAPICaller: NSObject { } - func retrieveEntries(page: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) { + public func retrieveEntries(page: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } guard let url = URL(string: page) else { completion(.success((nil, nil))) return } - let request = URLRequest(url: url, credentials: credentials) + let request = delegate.createURLRequest(url: url, credentials: credentials, conditionalGet: nil) transport.send(request: request, resultType: [FeedbinEntry].self) { result in @@ -576,11 +667,16 @@ final class FeedbinAPICaller: NSObject { } - func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + public func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + let conditionalGet = delegate.conditionalGetInfo(key: ConditionalGetKeys.unreadEntries) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) transport.send(request: request, resultType: [Int].self) { result in @@ -601,9 +697,15 @@ final class FeedbinAPICaller: NSObject { } - func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + public func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinUnreadEntry(unreadEntries: entries) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in if self.suspended { @@ -620,9 +722,15 @@ final class FeedbinAPICaller: NSObject { } } - func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + public func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("unread_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinUnreadEntry(unreadEntries: entries) transport.send(request: request, method: HTTPMethod.delete, payload: payload) { result in if self.suspended { @@ -639,11 +747,16 @@ final class FeedbinAPICaller: NSObject { } } - func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + public func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + let conditionalGet = delegate.conditionalGetInfo(key: ConditionalGetKeys.starredEntries) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) transport.send(request: request, resultType: [Int].self) { result in if self.suspended { @@ -663,9 +776,15 @@ final class FeedbinAPICaller: NSObject { } - func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + public func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinStarredEntry(starredEntries: entries) transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in if self.suspended { @@ -682,9 +801,15 @@ final class FeedbinAPICaller: NSObject { } } - func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + public func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + + guard let delegate else { + completion(.failure(TransportError.suspended)) + return + } + let callURL = feedbinBaseURL.appendingPathComponent("starred_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) + let request = delegate.createURLRequest(url: callURL, credentials: credentials, conditionalGet: nil) let payload = FeedbinStarredEntry(starredEntries: entries) transport.send(request: request, method: HTTPMethod.delete, payload: payload) { result in if self.suspended { @@ -708,9 +833,8 @@ final class FeedbinAPICaller: NSObject { extension FeedbinAPICaller { func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { - if var conditionalGet = accountMetadata?.conditionalGetInfo { - conditionalGet[key] = HTTPConditionalGetInfo(headers: headers) - accountMetadata?.conditionalGetInfo = conditionalGet + if let conditionalGetInfo = HTTPConditionalGetInfo(headers: headers) { + delegate?.setConditionalGetInfo(conditionalGetInfo, forKey: key) } } diff --git a/Account/Sources/Account/Feedbin/FeedbinDate.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinDate.swift similarity index 100% rename from Account/Sources/Account/Feedbin/FeedbinDate.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinDate.swift diff --git a/Account/Sources/Account/Feedbin/FeedbinEntry.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinEntry.swift similarity index 70% rename from Account/Sources/Account/Feedbin/FeedbinEntry.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinEntry.swift index 27e23a140..f70255a60 100644 --- a/Account/Sources/Account/Feedbin/FeedbinEntry.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinEntry.swift @@ -10,24 +10,24 @@ import Foundation import RSParser import RSCore -final class FeedbinEntry: Decodable { +public final class FeedbinEntry: Decodable { - let articleID: Int - let feedID: Int - let title: String? - let url: String? - let authorName: String? - let contentHTML: String? - let summary: String? - let datePublished: String? - let dateArrived: String? - let jsonFeed: FeedbinEntryJSONFeed? + public let articleID: Int + public let feedID: Int + public let title: String? + public let url: String? + public let authorName: String? + public let contentHTML: String? + public let summary: String? + public let datePublished: String? + public let dateArrived: String? + public let jsonFeed: FeedbinEntryJSONFeed? // Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin // requires a very specific date formatter to work and even then it fails occasionally. // Rather than loose all the entries we only lose the one date by decoding as a string // and letting the one date fail when parsed. - lazy var parsedDatePublished: Date? = { + public lazy var parsedDatePublished: Date? = { if let datePublished = datePublished { return RSDateWithString(datePublished) } @@ -50,9 +50,9 @@ final class FeedbinEntry: Decodable { } } -struct FeedbinEntryJSONFeed: Decodable { - let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor? - let jsonFeedExternalURL: String? +public struct FeedbinEntryJSONFeed: Decodable { + public let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor? + public let jsonFeedExternalURL: String? enum CodingKeys: String, CodingKey { case jsonFeedAuthor = "author" @@ -75,9 +75,9 @@ struct FeedbinEntryJSONFeed: Decodable { } -struct FeedbinEntryJSONFeedAuthor: Decodable { - let url: String? - let avatarURL: String? +public struct FeedbinEntryJSONFeedAuthor: Decodable { + public let url: String? + public let avatarURL: String? enum CodingKeys: String, CodingKey { case url = "url" case avatarURL = "avatar" diff --git a/Account/Sources/Account/Feedbin/FeedbinImportResult.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinImportResult.swift similarity index 72% rename from Account/Sources/Account/Feedbin/FeedbinImportResult.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinImportResult.swift index bce437960..b45863d77 100644 --- a/Account/Sources/Account/Feedbin/FeedbinImportResult.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinImportResult.swift @@ -8,10 +8,10 @@ import Foundation -struct FeedbinImportResult: Codable { +public struct FeedbinImportResult: Codable { - let importResultID: Int - let complete: Bool + public let importResultID: Int + public let complete: Bool enum CodingKeys: String, CodingKey { case importResultID = "id" diff --git a/Account/Sources/Account/Feedbin/FeedbinStarredEntry.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinStarredEntry.swift similarity index 100% rename from Account/Sources/Account/Feedbin/FeedbinStarredEntry.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinStarredEntry.swift diff --git a/Account/Sources/Account/Feedbin/FeedbinSubscription.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinSubscription.swift similarity index 64% rename from Account/Sources/Account/Feedbin/FeedbinSubscription.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinSubscription.swift index d789d1deb..4e40416c6 100644 --- a/Account/Sources/Account/Feedbin/FeedbinSubscription.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinSubscription.swift @@ -10,14 +10,14 @@ import Foundation import RSCore import RSParser -struct FeedbinSubscription: Hashable, Codable { +public struct FeedbinSubscription: Hashable, Codable { - let subscriptionID: Int - let feedID: Int - let name: String? - let url: String - let homePageURL: String? - let jsonFeed: FeedbinSubscriptionJSONFeed? + public let subscriptionID: Int + public let feedID: Int + public let name: String? + public let url: String + public let homePageURL: String? + public let jsonFeed: FeedbinSubscriptionJSONFeed? enum CodingKeys: String, CodingKey { case subscriptionID = "id" @@ -32,15 +32,15 @@ struct FeedbinSubscription: Hashable, Codable { hasher.combine(subscriptionID) } - static func == (lhs: FeedbinSubscription, rhs: FeedbinSubscription) -> Bool { + public static func == (lhs: FeedbinSubscription, rhs: FeedbinSubscription) -> Bool { return lhs.subscriptionID == rhs.subscriptionID } } -struct FeedbinSubscriptionJSONFeed: Codable { - let favicon: String? - let icon: String? +public struct FeedbinSubscriptionJSONFeed: Codable { + public let favicon: String? + public let icon: String? enum CodingKeys: String, CodingKey { case favicon = "favicon" case icon = "icon" @@ -61,10 +61,10 @@ struct FeedbinUpdateSubscription: Codable { } } -struct FeedbinSubscriptionChoice: Codable { +public struct FeedbinSubscriptionChoice: Codable { - let name: String? - let url: String + public let name: String? + public let url: String enum CodingKeys: String, CodingKey { case name = "title" diff --git a/Account/Sources/Account/Feedbin/FeedbinTag.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinTag.swift similarity index 90% rename from Account/Sources/Account/Feedbin/FeedbinTag.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinTag.swift index b7aaa6f58..f6d6dfddd 100644 --- a/Account/Sources/Account/Feedbin/FeedbinTag.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinTag.swift @@ -8,10 +8,10 @@ import Foundation -struct FeedbinTag: Codable { +public struct FeedbinTag: Codable { let tagID: Int - let name: String + public let name: String enum CodingKeys: String, CodingKey { case tagID = "id" diff --git a/Account/Sources/Account/Feedbin/FeedbinTagging.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinTagging.swift similarity index 79% rename from Account/Sources/Account/Feedbin/FeedbinTagging.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinTagging.swift index a3f830aec..d369010fe 100644 --- a/Account/Sources/Account/Feedbin/FeedbinTagging.swift +++ b/SyncClients/Feedbin/Sources/Feedbin/FeedbinTagging.swift @@ -8,11 +8,11 @@ import Foundation -struct FeedbinTagging: Codable { +public struct FeedbinTagging: Codable { - let taggingID: Int - let feedID: Int - let name: String + public let taggingID: Int + public let feedID: Int + public let name: String enum CodingKeys: String, CodingKey { case taggingID = "id" diff --git a/Account/Sources/Account/Feedbin/FeedbinUnreadEntry.swift b/SyncClients/Feedbin/Sources/Feedbin/FeedbinUnreadEntry.swift similarity index 100% rename from Account/Sources/Account/Feedbin/FeedbinUnreadEntry.swift rename to SyncClients/Feedbin/Sources/Feedbin/FeedbinUnreadEntry.swift diff --git a/SyncClients/Feedbin/Tests/FeedbinTests/FeedbinTests.swift b/SyncClients/Feedbin/Tests/FeedbinTests/FeedbinTests.swift new file mode 100644 index 000000000..b6d068288 --- /dev/null +++ b/SyncClients/Feedbin/Tests/FeedbinTests/FeedbinTests.swift @@ -0,0 +1,2 @@ +import XCTest +@testable import Feedbin