Rework add feed process so that it is compatible with more services
This commit is contained in:
parent
c75dc8b54d
commit
f7771fc509
@ -267,7 +267,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
case .newsBlur:
|
case .newsBlur:
|
||||||
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
|
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||||
case .freshRSS:
|
case .freshRSS:
|
||||||
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .generic)
|
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .freshRSS)
|
||||||
case .inoreader:
|
case .inoreader:
|
||||||
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader)
|
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport, variant: .inoreader)
|
||||||
case .bazQux:
|
case .bazQux:
|
||||||
|
@ -28,7 +28,13 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
private let caller: ReaderAPICaller
|
private let caller: ReaderAPICaller
|
||||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI")
|
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? {
|
var server: String? {
|
||||||
get {
|
get {
|
||||||
@ -297,36 +303,54 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||||
guard let folder = container as? Folder else {
|
guard let url = URL(string: url) else {
|
||||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||||
caller.createSubscription(url: url, name: name, folder: folder) { result in
|
|
||||||
|
FeedFinder.find(url: url) { result in
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let subResult):
|
case .success(let feedSpecifiers):
|
||||||
switch subResult {
|
let feedSpecifiers = feedSpecifiers.filter { !$0.urlString.hasSuffix(".json") }
|
||||||
case .created(let subscription):
|
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers) else {
|
||||||
self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion)
|
completion(.failure(AccountError.createErrorNotFound))
|
||||||
case .alreadySubscribed:
|
return
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
|
||||||
}
|
|
||||||
case .notFound:
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.failure(AccountError.createErrorNotFound))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
self.caller.createSubscription(url: bestFeedSpecifier.urlString, name: name, folder: container as? Folder) { result in
|
||||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
self.refreshProgress.completeTask()
|
||||||
completion(.failure(wrappedError))
|
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, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
@ -42,6 +42,7 @@ final class ReaderAPICaller: NSObject {
|
|||||||
case tagList = "/reader/api/0/tag/list"
|
case tagList = "/reader/api/0/tag/list"
|
||||||
case subscriptionList = "/reader/api/0/subscription/list"
|
case subscriptionList = "/reader/api/0/subscription/list"
|
||||||
case subscriptionEdit = "/reader/api/0/subscription/edit"
|
case subscriptionEdit = "/reader/api/0/subscription/edit"
|
||||||
|
case subscriptionAdd = "/reader/api/0/subscription/quickadd"
|
||||||
case contents = "/reader/api/0/stream/items/contents"
|
case contents = "/reader/api/0/stream/items/contents"
|
||||||
case itemIds = "/reader/api/0/stream/items/ids"
|
case itemIds = "/reader/api/0/stream/items/ids"
|
||||||
case editTag = "/reader/api/0/edit-tag"
|
case editTag = "/reader/api/0/edit-tag"
|
||||||
@ -64,7 +65,7 @@ final class ReaderAPICaller: NSObject {
|
|||||||
private var APIBaseURL: URL? {
|
private var APIBaseURL: URL? {
|
||||||
get {
|
get {
|
||||||
switch variant {
|
switch variant {
|
||||||
case .generic:
|
case .generic, .freshRSS:
|
||||||
guard let accountMetadata = accountMetadata else {
|
guard let accountMetadata = accountMetadata else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -307,80 +308,108 @@ final class ReaderAPICaller: NSObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSubscription(url: String, name: String?, folder: Folder, completion: @escaping (Result<CreateReaderAPISubscriptionResult, Error>) -> Void) {
|
func createSubscription(url: String, name: String?, folder: Folder?, completion: @escaping (Result<CreateReaderAPISubscriptionResult, Error>) -> Void) {
|
||||||
guard let baseURL = APIBaseURL else {
|
guard let baseURL = APIBaseURL else {
|
||||||
completion(.failure(CredentialsError.incompleteCredentials))
|
completion(.failure(CredentialsError.incompleteCredentials))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let url = URL(string: url) else {
|
func findSubscription(streamID: String, completion: @escaping (Result<CreateReaderAPISubscriptionResult, Error>) -> Void) {
|
||||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
// There is no call to get a single subscription entry, so we get them all,
|
||||||
return
|
// 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 {
|
switch result {
|
||||||
case .success(let feedSpecifiers):
|
case .success(let token):
|
||||||
|
let url = baseURL
|
||||||
|
.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue)
|
||||||
|
.appendingQueryItem(URLQueryItem(name: "quickadd", value: url))
|
||||||
|
|
||||||
let feedSpecifiers = feedSpecifiers.filter { !$0.urlString.hasSuffix(".json") }
|
guard let callURL = url else {
|
||||||
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), let feedURL = URL(string: bestFeedSpecifier.urlString) else {
|
completion(.failure(TransportError.noURL))
|
||||||
completion(.failure(AccountError.createErrorNotFound))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.requestAuthorizationToken(endpoint: baseURL) { (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 {
|
switch result {
|
||||||
case .success(let token):
|
case .success(let (_, subResult)):
|
||||||
let callURL = baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue)
|
|
||||||
|
|
||||||
var request = URLRequest(url: callURL, credentials: self.credentials)
|
switch subResult?.numResults {
|
||||||
self.addVariantHeaders(&request)
|
case 0:
|
||||||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
completion(.success(.alreadySubscribed))
|
||||||
request.httpMethod = "POST"
|
default:
|
||||||
|
guard let streamId = subResult?.streamId else {
|
||||||
|
completion(.failure(AccountError.createErrorNotFound))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let postData = "T=\(token)&ac=subscribe&s=feed/\(feedURL.absoluteString)&a=user/-/label/\(folder.nameForDisplay)&t=\(name ?? "")".data(using: String.Encoding.utf8)
|
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"
|
||||||
|
|
||||||
self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in
|
var postString = "T=\(token)&ac=subscribe&s=\(streamId)"
|
||||||
switch result {
|
if let folder = folder {
|
||||||
case .success:
|
postString += "&a=user/-/label/\(folder.nameForDisplay)"
|
||||||
|
}
|
||||||
|
if let name = name {
|
||||||
|
postString += "&t=\(name)"
|
||||||
|
}
|
||||||
|
|
||||||
// There is no call to get a single subscription entry, so we get them all,
|
let postData = postString.data(using: String.Encoding.utf8)
|
||||||
// look up the one we just subscribed to and return that
|
self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in
|
||||||
self.retrieveSubscriptions(completion: { (result) in
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let subscriptions):
|
case .success:
|
||||||
guard let subscriptions = subscriptions else {
|
findSubscription(streamID: streamId, completion: completion)
|
||||||
completion(.failure(AccountError.createErrorNotFound))
|
case .failure:
|
||||||
return
|
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
||||||
}
|
|
||||||
|
|
||||||
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 .failure:
|
|
||||||
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .failure:
|
})
|
||||||
completion(.failure(AccountError.createErrorNotFound))
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
struct ReaderAPISubscriptionContainer: Codable {
|
||||||
let subscriptions: [ReaderAPISubscription]
|
let subscriptions: [ReaderAPISubscription]
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ import Foundation
|
|||||||
|
|
||||||
public enum ReaderAPIVariant {
|
public enum ReaderAPIVariant {
|
||||||
case generic
|
case generic
|
||||||
|
case freshRSS
|
||||||
case inoreader
|
case inoreader
|
||||||
case bazQux
|
case bazQux
|
||||||
case theOldReader
|
case theOldReader
|
||||||
|
Loading…
x
Reference in New Issue
Block a user