diff --git a/Account/Sources/Account/Account.swift b/Account/Sources/Account/Account.swift index 40ed20899..86ea89176 100644 --- a/Account/Sources/Account/Account.swift +++ b/Account/Sources/Account/Account.swift @@ -267,7 +267,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, case .newsBlur: self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport) case .freshRSS: - self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .generic) + self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS) case .inoreader: self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader) case .bazQux: diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 871d1ffa6..f14e63d5e 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -28,7 +28,13 @@ final class ReaderAPIAccountDelegate: AccountDelegate { private let caller: ReaderAPICaller private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI") - var behaviors: AccountBehaviors = [.disallowFeedInRootFolder, .disallowOPMLImports] + var behaviors: AccountBehaviors { + var behaviors: AccountBehaviors = [.disallowOPMLImports] + if variant == .freshRSS { + behaviors.append(.disallowFeedInRootFolder) + } + return behaviors + } var server: String? { get { @@ -297,36 +303,54 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { - guard let folder = container as? Folder else { + guard let url = URL(string: url) else { completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) return } - refreshProgress.addToNumberOfTasksAndRemaining(1) - caller.createSubscription(url: url, name: name, folder: folder) { result in + refreshProgress.addToNumberOfTasksAndRemaining(2) + + FeedFinder.find(url: url) { result in self.refreshProgress.completeTask() + switch result { - case .success(let subResult): - switch subResult { - case .created(let subscription): - self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) - case .alreadySubscribed: - DispatchQueue.main.async { - completion(.failure(AccountError.createErrorAlreadySubscribed)) - } - case .notFound: - DispatchQueue.main.async { - completion(.failure(AccountError.createErrorNotFound)) - } + case .success(let feedSpecifiers): + let feedSpecifiers = feedSpecifiers.filter { !$0.urlString.hasSuffix(".json") } + guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else { + completion(.failure(AccountError.createErrorNotFound)) + return } - case .failure(let error): - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + + self.caller.createSubscription(url: bestFeedSpecifier.urlString, name: name, folder: container as? Folder) { result in + self.refreshProgress.completeTask() + switch result { + case .success(let subResult): + switch subResult { + case .created(let subscription): + self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) + case .alreadySubscribed: + DispatchQueue.main.async { + completion(.failure(AccountError.createErrorAlreadySubscribed)) + } + case .notFound: + DispatchQueue.main.async { + completion(.failure(AccountError.createErrorNotFound)) + } + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + case .failure: + completion(.failure(AccountError.createErrorNotFound)) } } + } func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result) -> Void) { diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index e295972c2..0e40f79be 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -42,6 +42,7 @@ final class ReaderAPICaller: NSObject { case tagList = "/reader/api/0/tag/list" case subscriptionList = "/reader/api/0/subscription/list" case subscriptionEdit = "/reader/api/0/subscription/edit" + case subscriptionAdd = "/reader/api/0/subscription/quickadd" case contents = "/reader/api/0/stream/items/contents" case itemIds = "/reader/api/0/stream/items/ids" case editTag = "/reader/api/0/edit-tag" @@ -64,7 +65,7 @@ final class ReaderAPICaller: NSObject { private var APIBaseURL: URL? { get { switch variant { - case .generic: + case .generic, .freshRSS: guard let accountMetadata = accountMetadata else { return nil } @@ -307,82 +308,110 @@ final class ReaderAPICaller: NSObject { } - func createSubscription(url: String, name: String?, folder: Folder, completion: @escaping (Result) -> Void) { + func createSubscription(url: String, name: String?, folder: Folder?, completion: @escaping (Result) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } - guard let url = URL(string: url) else { - completion(.failure(LocalAccountDelegateError.invalidParameter)) - return + func findSubscription(streamID: String, completion: @escaping (Result) -> Void) { + // 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 + self.retrieveSubscriptions(completion: { (result) in + switch result { + case .success(let subscriptions): + guard let subscriptions = subscriptions else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + guard let subscription = subscriptions.first(where: { (sub) -> Bool in + sub.feedID == streamID + }) else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + completion(.success(.created(subscription))) + + case .failure(let error): + completion(.failure(error)) + } + }) } - FeedFinder.find(url: url) { result in - + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { - case .success(let feedSpecifiers): - - let feedSpecifiers = feedSpecifiers.filter { !$0.urlString.hasSuffix(".json") } - guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let feedURL = URL(string: bestFeedSpecifier.urlString) else { - completion(.failure(AccountError.createErrorNotFound)) + case .success(let token): + let url = baseURL + .appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue) + .appendingQueryItem(URLQueryItem(name: "quickadd", value: url)) + + guard let callURL = url else { + completion(.failure(TransportError.noURL)) return } - - self.requestAuthorizationToken(endpoint: baseURL) { (result) in - switch result { - case .success(let token): - let callURL = baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue) - var request = URLRequest(url: callURL, credentials: self.credentials) - self.addVariantHeaders(&request) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - - let postData = "T=\(token)&ac=subscribe&s=feed/\(feedURL.absoluteString)&a=user/-/label/\(folder.nameForDisplay)&t=\(name ?? "")".data(using: String.Encoding.utf8) - - self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in - switch result { - case .success: - - // 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 - self.retrieveSubscriptions(completion: { (result) in + var request = URLRequest(url: callURL, credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let postData = "T=\(token)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIQuickAddResult.self, completion: { (result) in + switch result { + case .success(let (_, subResult)): + + switch subResult?.numResults { + case 0: + completion(.success(.alreadySubscribed)) + default: + guard let streamId = subResult?.streamId else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + if name == nil && folder == nil { + findSubscription(streamID: streamId, completion: completion) + } else { + let callURL = baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue) + var request = URLRequest(url: callURL, credentials: self.credentials) + self.addVariantHeaders(&request) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + var postString = "T=\(token)&ac=subscribe&s=\(streamId)" + if let folder = folder { + postString += "&a=user/-/label/\(folder.nameForDisplay)" + } + if let name = name { + postString += "&t=\(name)" + } + + let postData = postString.data(using: String.Encoding.utf8) + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { - case .success(let subscriptions): - guard let subscriptions = subscriptions else { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - guard let subscription = subscriptions.first(where: { (sub) -> Bool in - sub.url == feedURL.absoluteString - }) else { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - completion(.success(.created(subscription))) - - case .failure(let error): - completion(.failure(error)) + case .success: + findSubscription(streamID: streamId, completion: completion) + case .failure: + completion(.failure(AccountError.createErrorAlreadySubscribed)) } }) - - case .failure: - completion(.failure(AccountError.createErrorAlreadySubscribed)) } - }) - + + } + case .failure(let error): completion(.failure(error)) } - } - - case .failure: - completion(.failure(AccountError.createErrorNotFound)) + + }) + + case .failure(let error): + completion(.failure(error)) } - + } } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift b/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift index f4421cba8..e74491ca6 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPISubscription.swift @@ -19,6 +19,18 @@ import RSParser */ +struct ReaderAPIQuickAddResult: Codable { + let numResults: Int + let error: String? + let streamId: String? + + enum CodingKeys: String, CodingKey { + case numResults = "numResults" + case error = "error" + case streamId = "streamId" + } +} + struct ReaderAPISubscriptionContainer: Codable { let subscriptions: [ReaderAPISubscription] diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift index 07c9a2883..a2cf5682c 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIVariant.swift @@ -9,6 +9,7 @@ import Foundation public enum ReaderAPIVariant { case generic + case freshRSS case inoreader case bazQux case theOldReader