From 84dbdf25e223f15ec4e777c83264af30d2543d6f Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 28 May 2019 13:08:15 -0400 Subject: [PATCH 01/31] Google Reader API Account Provider and initial integration * Creation of account classes (based on FeedBin) * Integration on Mac side into account dialog * Initial authentication call works and extracts auth token, but no where to put it right now. --- Frameworks/Account/Account.swift | 9 + .../Account/Account.xcodeproj/project.pbxproj | 52 + .../GoogleReaderCompatibleAPICaller.swift | 615 +++++++++ ...oogleReaderCompatibleAccountDelegate.swift | 1199 +++++++++++++++++ .../GoogleReaderCompatibleDate.swift | 21 + .../GoogleReaderCompatibleEntry.swift | 67 + .../GoogleReaderCompatibleIcon.swift | 21 + .../GoogleReaderCompatibleImportResult.swift | 21 + .../GoogleReaderCompatibleStarredEntry.swift | 19 + .../GoogleReaderCompatibleSubscription.swift | 55 + .../GoogleReaderCompatibleTag.swift | 43 + .../GoogleReaderCompatibleTagging.swift | 35 + .../GoogleReaderCompatibleUnreadEntry.swift | 19 + .../Accounts/AccountsAddViewController.swift | 9 +- .../AccountsGoogleReaderCompatible.xib | 210 +++ ...ogleReaderCompatibleWindowController.swift | 125 ++ .../AccountsPreferencesViewController.swift | 2 + Mac/Scriptability/Account+Scriptability.swift | 2 + NetNewsWire.xcodeproj/project.pbxproj | 20 +- 19 files changed, 2537 insertions(+), 7 deletions(-) create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift create mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift create mode 100644 Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib create mode 100644 Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 4bb5df495..6c436895c 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -33,6 +33,7 @@ public enum AccountType: Int { case feedbin = 17 case feedWrangler = 18 case newsBlur = 19 + case googleReaderCompatible = 20 // TODO: more } @@ -196,6 +197,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.delegate = LocalAccountDelegate() case .feedbin: self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport) + case .googleReaderCompatible: + self.delegate = GoogleReaderCompatibleAccountDelegate(dataFolder: dataFolder, transport: transport) default: fatalError("Only Local and Feedbin accounts are supported") } @@ -223,6 +226,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, defaultName = "FeedWrangler" case .newsBlur: defaultName = "NewsBlur" + case .googleReaderCompatible: + defaultName = "Google Reader Compatible" } NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil) @@ -254,6 +259,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch credentials { case .basic(let username, _): self.username = username + case .googleLogin(let username, _, _, _): + self.username = username } try CredentialsManager.storeCredentials(credentials, server: server) @@ -283,6 +290,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) case .feedbin: FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) + case .googleReaderCompatible: + GoogleReaderCompatibleAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) default: break } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 4e8d20f58..a9b840800 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -35,6 +35,17 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; }; 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; + 552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */; }; + 552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */; }; + 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */; }; + 552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */; }; + 552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */; }; + 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */; }; + 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */; }; + 552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */; }; + 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */; }; + 552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */; }; + 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */; }; 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; }; 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; }; 841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; }; @@ -136,6 +147,17 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; + 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleDate.swift; sourceTree = ""; }; + 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleIcon.swift; sourceTree = ""; }; + 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleEntry.swift; sourceTree = ""; }; + 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleSubscription.swift; sourceTree = ""; }; + 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleStarredEntry.swift; sourceTree = ""; }; + 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTag.swift; sourceTree = ""; }; + 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleUnreadEntry.swift; sourceTree = ""; }; + 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTagging.swift; sourceTree = ""; }; + 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAccountDelegate.swift; sourceTree = ""; }; + 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleImportResult.swift; sourceTree = ""; }; + 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAPICaller.swift; sourceTree = ""; }; 841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; @@ -222,6 +244,24 @@ path = JSON; sourceTree = ""; }; + 552032EA229D5D5A009559E0 /* GoogleReaderCompatible */ = { + isa = PBXGroup; + children = ( + 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */, + 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */, + 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */, + 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */, + 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */, + 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */, + 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */, + 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */, + 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */, + 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */, + 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */, + ); + path = GoogleReaderCompatible; + sourceTree = ""; + }; 841973E91F6DD19E006346C4 /* Products */ = { isa = PBXGroup; children = ( @@ -302,6 +342,7 @@ 5165D71F22835E9800D9D53D /* FeedFinder */, 8419742B1F6DDE84006346C4 /* LocalAccount */, 84245C7D1FDDD2580074AFBB /* Feedbin */, + 552032EA229D5D5A009559E0 /* GoogleReaderCompatible */, 848935031F62484F00CEBD24 /* AccountTests */, 848934F71F62484F00CEBD24 /* Products */, 8469F80F1F6DC3C10084783E /* Frameworks */, @@ -515,14 +556,17 @@ buildActionMask = 2147483647; files = ( 84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */, + 552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */, 84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */, 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 846E77451F6EF9B900A165E2 /* Container.swift in Sources */, + 552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, + 552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */, @@ -532,11 +576,19 @@ 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */, + 552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, + 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */, 51E3EB41229AF61B00645299 /* AccountError.swift in Sources */, 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */, + 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */, + 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */, 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, + 552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */, + 552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, + 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */, + 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, 5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */, diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift new file mode 100644 index 000000000..002da3d61 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -0,0 +1,615 @@ +// +// GoogleReaderCompatibleAPICaller.swift +// Account +// +// Created by Maurice Parker on 5/2/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +// GoogleReaderCompatible currently has a maximum of 250 requests per second. If you begin to receive +// HTTP Response Codes of 403, you have exceeded this limit. Wait 5 minutes and your +// IP address will become unblocked and you can use the service again. + +import Foundation +import RSWeb + +enum CreateGoogleReaderSubscriptionResult { + case created(GoogleReaderCompatibleSubscription) + case multipleChoice([GoogleReaderCompatibleSubscriptionChoice]) + case alreadySubscribed + case notFound +} + +final class GoogleReaderCompatibleAPICaller: NSObject { + + struct ConditionalGetKeys { + static let subscriptions = "subscriptions" + static let tags = "tags" + static let taggings = "taggings" + static let icons = "icons" + static let unreadEntries = "unreadEntries" + static let starredEntries = "starredEntries" + } + + private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! + private var transport: Transport! + + var credentials: Credentials? + var apiAuthToken: String? + weak var accountMetadata: AccountMetadata? + + init(transport: Transport) { + super.init() + self.transport = transport + } + + func validateCredentials(completion: @escaping (Result) -> Void) { + guard let credentials = credentials else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + guard case .googleLogin(let username, let password, let apiUrl, _) = credentials else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + guard var loginURL = URLComponents(url: apiUrl.appendingPathComponent("/accounts/ClientLogin"), resolvingAgainstBaseURL: false) else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + loginURL.queryItems = [ + URLQueryItem(name: "Email", value: username), + URLQueryItem(name: "Passwd", value: password) + ] + + guard let callURL = loginURL.url else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request) { result in + switch result { + case .success(let (_, data)): + guard let resultData = data else { + completion(.failure(TransportError.noData)) + break + } + + // Convert the return data to UTF8 and then parse out the Auth token + guard let rawData = String(data: resultData, encoding: .utf8) else { + completion(.failure(TransportError.noData)) + break + } + + var authData: [String: String] = [:] + rawData.split(separator: "\n").forEach({ (line: Substring) in + let items = line.split(separator: "=").map{String($0)} + authData[items[0]] = items[1] + }) + + guard let authString = authData["Auth"] else { + completion(.failure(CredentialsError.incompleteCredentials)) + break + } + + // Save Auth Token for later use + self.apiAuthToken = authString + + completion(.success(true)) + case .failure(let error): + switch error { + case TransportError.httpError(let status): + if status == 401 { + completion(.success(false)) + } else { + completion(.failure(error)) + } + default: + completion(.failure(error)) + } + } + } + + } + + func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + + transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in + + switch result { + case .success(let (_, data)): + + guard let resultData = data else { + completion(.failure(TransportError.noData)) + break + } + + do { + let result = try JSONDecoder().decode(GoogleReaderCompatibleImportResult.self, from: resultData) + completion(.success(result)) + } catch { + completion(.failure(error)) + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports/\(importID).json") + let request = URLRequest(url: callURL, credentials: credentials) + + transport.send(request: request, resultType: GoogleReaderCompatibleImportResult.self) { result in + + switch result { + case .success(let (_, importResult)): + completion(.success(importResult)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveTags(completion: @escaping (Result<[GoogleReaderCompatibleTag]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [GoogleReaderCompatibleTag].self) { result in + + switch result { + case .success(let (response, tags)): + self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) + completion(.success(tags)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleRenameTag(oldName: oldName, newName: newName) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteTag(name: String, completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleDeleteTag(name: name) + + transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [GoogleReaderCompatibleTagging].self) { result in + + switch result { + case .success(let (_, taggings)): + completion(.success(taggings)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveSubscriptions(completion: @escaping (Result<[GoogleReaderCompatibleSubscription]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [GoogleReaderCompatibleSubscription].self) { result in + + switch result { + case .success(let (response, subscriptions)): + self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) + completion(.success(subscriptions)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func createSubscription(url: String, completion: @escaping (Result) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + + let payload: Data + do { + payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateSubscription(feedURL: url)) + } catch { + completion(.failure(error)) + return + } + + transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in + + switch result { + case .success(let (response, data)): + + switch response.forcedStatusCode { + case 201: + guard let subData = data else { + completion(.failure(TransportError.noData)) + break + } + do { + let subscription = try JSONDecoder().decode(GoogleReaderCompatibleSubscription.self, from: subData) + completion(.success(.created(subscription))) + } catch { + completion(.failure(error)) + } + case 300: + guard let subData = data else { + completion(.failure(TransportError.noData)) + break + } + do { + let subscriptions = try JSONDecoder().decode([GoogleReaderCompatibleSubscriptionChoice].self, from: subData) + completion(.success(.multipleChoice(subscriptions))) + } catch { + completion(.failure(error)) + } + case 302: + completion(.success(.alreadySubscribed)) + default: + completion(.failure(TransportError.httpError(status: response.forcedStatusCode))) + } + + case .failure(let error): + + switch error { + case TransportError.httpError(let status): + switch status { + case 401: + // I don't know why we get 401's here. This looks like a GoogleReaderCompatible bug, but it only happens + // when you are already subscribed to the feed. + completion(.success(.alreadySubscribed)) + case 404: + completion(.success(.notFound)) + default: + completion(.failure(error)) + } + default: + completion(.failure(error)) + } + + } + + } + + } + + func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleUpdateSubscription(title: newName) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteSubscription(subscriptionID: String, completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json") + let request = URLRequest(url: callURL, credentials: credentials) + transport.send(request: request, method: HTTPMethod.delete, completion: completion) + } + + func retrieveTaggings(completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [GoogleReaderCompatibleTagging].self) { result in + + switch result { + case .success(let (response, taggings)): + self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields) + completion(.success(taggings)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + + let payload: Data + do { + payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateTagging(feedID: feedID, name: name)) + } catch { + completion(.failure(error)) + return + } + + transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in + + switch result { + case .success(let (response, _)): + if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location), + let lowerBound = taggingLocation.range(of: "v2/taggings/")?.upperBound, + let upperBound = taggingLocation.range(of: ".json")?.lowerBound, + let taggingID = Int(taggingLocation[lowerBound..) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings/\(taggingID).json") + var request = URLRequest(url: callURL, credentials: credentials) + request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) + transport.send(request: request, method: HTTPMethod.delete, completion: completion) + } + + func retrieveIcons(completion: @escaping (Result<[GoogleReaderCompatibleIcon]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("icons.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [GoogleReaderCompatibleIcon].self) { result in + + switch result { + case .success(let (response, icons)): + self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields) + completion(.success(icons)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([GoogleReaderCompatibleEntry]?), Error>) -> Void) { + + guard !articleIDs.isEmpty else { + completion(.success(([GoogleReaderCompatibleEntry]()))) + return + } + + let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" } + let paramIDs = String(concatIDs.dropFirst()) + + var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)! + callComponents.queryItems = [URLQueryItem(name: "ids", value: paramIDs), URLQueryItem(name: "mode", value: "extended")] + let request = URLRequest(url: callComponents.url!, credentials: credentials) + + transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + + switch result { + case .success(let (_, entries)): + completion(.success((entries))) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveEntries(feedID: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { + + let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() + let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since) + + var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("feeds/\(feedID)/entries.json"), resolvingAgainstBaseURL: false)! + callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")] + let request = URLRequest(url: callComponents.url!, credentials: credentials) + + transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + + switch result { + case .success(let (response, entries)): + + let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) + completion(.success((entries, pagingInfo.nextPage))) + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func retrieveEntries(completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?, Int?), Error>) -> Void) { + + let since: Date = { + if let lastArticleFetch = accountMetadata?.lastArticleFetch { + return lastArticleFetch + } else { + return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() + } + }() + + let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since) + var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)! + callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")] + let request = URLRequest(url: callComponents.url!, credentials: credentials) + + transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + + switch result { + case .success(let (response, entries)): + + let dateInfo = HTTPDateInfo(urlResponse: response) + self.accountMetadata?.lastArticleFetch = dateInfo?.date + + let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) + let lastPageNumber = self.extractPageNumber(link: pagingInfo.lastPage) + completion(.success((entries, pagingInfo.nextPage, lastPageNumber))) + + case .failure(let error): + self.accountMetadata?.lastArticleFetch = nil + completion(.failure(error)) + } + + } + + } + + func retrieveEntries(page: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { + + guard let url = URL(string: page), var callComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + completion(.success((nil, nil))) + return + } + + callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended")) + let request = URLRequest(url: callComponents.url!, credentials: credentials) + + transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + + switch result { + case .success(let (response, entries)): + + let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) + completion(.success((entries, pagingInfo.nextPage))) + + case .failure(let error): + self.accountMetadata?.lastArticleFetch = nil + completion(.failure(error)) + } + + } + + } + + func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [Int].self) { result in + + switch result { + case .success(let (response, unreadEntries)): + self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields) + completion(.success(unreadEntries)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) + transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) + } + + func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: [Int].self) { result in + + switch result { + case .success(let (response, starredEntries)): + self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields) + completion(.success(starredEntries)) + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries) + transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + } + + func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") + let request = URLRequest(url: callURL, credentials: credentials) + let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries) + transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) + } + +} + +// MARK: Private + +extension GoogleReaderCompatibleAPICaller { + + func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { + if var conditionalGet = accountMetadata?.conditionalGetInfo { + conditionalGet[key] = HTTPConditionalGetInfo(headers: headers) + accountMetadata?.conditionalGetInfo = conditionalGet + } + } + + func extractPageNumber(link: String?) -> Int? { + + guard let link = link else { + return nil + } + + if let lowerBound = link.range(of: "page=")?.upperBound { + if let upperBound = link.range(of: "&")?.lowerBound { + return Int(link[lowerBound..")?.lowerBound { + return Int(link[lowerBound..) -> Void) { + + refreshProgress.addToNumberOfTasksAndRemaining(6) + + refreshAccount(account) { result in + switch result { + case .success(): + + self.refreshArticles(account) { + self.refreshArticleStatus(for: account) { + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } + } + } + } + + case .failure(let error): + DispatchQueue.main.async { + self.refreshProgress.clear() + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + + } + + } + + func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + + os_log(.debug, log: log, "Sending article statuses...") + + let syncStatuses = database.selectForProcessing() + let createUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == false } + let deleteUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == true } + let createStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == true } + let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false } + + let group = DispatchGroup() + + group.enter() + sendArticleStatuses(createUnreadStatuses, apiCall: caller.createUnreadEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(deleteUnreadStatuses, apiCall: caller.deleteUnreadEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(createStarredStatuses, apiCall: caller.createStarredEntries) { + group.leave() + } + + group.enter() + sendArticleStatuses(deleteStarredStatuses, apiCall: caller.deleteStarredEntries) { + group.leave() + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done sending article statuses.") + completion() + } + + } + + func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) { + + os_log(.debug, log: log, "Refreshing article statuses...") + + let group = DispatchGroup() + + group.enter() + caller.retrieveUnreadEntries() { result in + switch result { + case .success(let articleIDs): + self.syncArticleReadState(account: account, articleIDs: articleIDs) + group.leave() + case .failure(let error): + os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription) + group.leave() + } + + } + + group.enter() + caller.retrieveStarredEntries() { result in + switch result { + case .success(let articleIDs): + self.syncArticleStarredState(account: account, articleIDs: articleIDs) + group.leave() + case .failure(let error): + os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) + group.leave() + } + + } + + group.notify(queue: DispatchQueue.main) { + os_log(.debug, log: self.log, "Done refreshing article statuses.") + completion() + } + + } + + func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) { + + var fileData: Data? + + do { + fileData = try Data(contentsOf: opmlFile) + } catch { + completion(.failure(error)) + return + } + + guard let opmlData = fileData else { + completion(.success(())) + return + } + + os_log(.debug, log: log, "Begin importing OPML...") + opmlImportInProgress = true + + caller.importOPML(opmlData: opmlData) { result in + switch result { + case .success(let importResult): + if importResult.complete { + os_log(.debug, log: self.log, "Import OPML done.") + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + } else { + self.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion) + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML failed.") + self.opmlImportInProgress = false + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { + + caller.renameTag(oldName: folder.name ?? "", newName: name) { result in + switch result { + case .success: + DispatchQueue.main.async { + folder.name = name + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { + + // GoogleReaderCompatible uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system + guard folder.hasAtLeastOneFeed() else { + account.deleteFolder(folder) + return + } + + // After we successfully delete at GoogleReaderCompatible, we add all the feeds to the account to save them. We then + // delete the folder. We then sync the taggings we received on the delete to remove any feeds from + // the account that might be in another folder. + caller.deleteTag(name: folder.name ?? "") { result in + switch result { + case .success(let taggings): + DispatchQueue.main.sync { + BatchUpdate.shared.perform { + for feed in folder.topLevelFeeds { + account.addFeed(feed) + self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + } + account.deleteFolder(folder) + } + completion(.success(())) + } + self.syncTaggings(account, taggings) + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func createFeed(for account: Account, url: String, completion: @escaping (Result) -> Void) { + + caller.createSubscription(url: url) { result in + switch result { + case .success(let subResult): + switch subResult { + case .created(let subscription): + self.createFeed(account: account, subscription: subscription, completion: completion) + case .multipleChoice(let choices): + self.decideBestFeedChoice(account: account, url: url, choices: choices, 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)) + } + } + + } + + } + + func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { + + // This error should never happen + guard let subscriptionID = feed.subscriptionID else { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + return + } + + caller.renameSubscription(subscriptionID: subscriptionID, newName: name) { result in + switch result { + case .success: + DispatchQueue.main.async { + feed.editedName = name + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result) -> Void) { + + // This error should never happen + guard let subscriptionID = feed.subscriptionID else { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + return + } + + caller.deleteSubscription(subscriptionID: subscriptionID) { result in + switch result { + case .success: + DispatchQueue.main.async { + account.removeFeed(feed) + if let folders = account.folders { + for folder in folders { + folder.removeFeed(feed) + } + } + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + + if let folder = container as? Folder, let feedID = Int(feed.feedID) { + caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in + switch result { + case .success(let taggingID): + DispatchQueue.main.async { + self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) + folder.addFeed(feed) + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + } else { + if let account = container as? Account { + account.addFeed(feed) + } + DispatchQueue.main.async { + completion(.success(())) + } + } + + } + + func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + + if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { + caller.deleteTagging(taggingID: feedTaggingID) { result in + switch result { + case .success: + DispatchQueue.main.async { + folder.removeFeed(feed) + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + } else { + if let account = container as? Account { + account.removeFeed(feed) + } + completion(.success(())) + } + + } + + func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result) -> Void) { + + let editedName = feed.editedName + + createFeed(for: account, url: feed.url) { result in + switch result { + case .success(let feed): + self.processRestoredFeed(for: account, feed: feed, editedName: editedName, folder: folder, completion: completion) + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } + + func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { + + account.addFolder(folder) + let group = DispatchGroup() + + for feed in folder.topLevelFeeds { + + group.enter() + addFeed(for: account, to: folder, with: feed) { result in + if account.topLevelFeeds.contains(feed) { + account.removeFeed(feed) + } + group.leave() + } + + } + + group.notify(queue: DispatchQueue.main) { + completion(.success(())) + } + + } + + func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool) -> Set
? { + + let syncStatuses = articles.map { article in + return SyncStatus(articleID: article.articleID, key: statusKey, flag: flag) + } + database.insertStatuses(syncStatuses) + + if database.selectPendingCount() > 100 { + sendArticleStatus(for: account) {} + } + + return account.update(articles, statusKey: statusKey, flag: flag) + + } + + func accountDidInitialize(_ account: Account) { + credentials = try? account.retrieveBasicCredentials() + accountMetadata = account.metadata + } + + static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { + + let caller = GoogleReaderCompatibleAPICaller(transport: transport) + caller.credentials = credentials + caller.validateCredentials() { result in + DispatchQueue.main.async { + completion(result) + } + } + + } + +} + +// MARK: Private + +private extension GoogleReaderCompatibleAccountDelegate { + + func refreshAccount(_ account: Account, completion: @escaping (Result) -> Void) { + + caller.retrieveTags { result in + switch result { + case .success(let tags): + BatchUpdate.shared.perform { + self.syncFolders(account, tags) + } + self.refreshProgress.completeTask() + self.refreshFeeds(account, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + + } + + func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result) -> Void) { + + DispatchQueue.main.async { + + Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { timer in + + os_log(.debug, log: self.log, "Checking status of OPML import...") + + self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in + switch result { + case .success(let importResult): + if let result = importResult, result.complete { + os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.") + timer.invalidate() + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.success(())) + } + } + case .failure(let error): + os_log(.debug, log: self.log, "Import OPML check failed.") + timer.invalidate() + self.opmlImportInProgress = false + DispatchQueue.main.async { + completion(.failure(error)) + } + } + } + + } + + } + + } + + func syncFolders(_ account: Account, _ tags: [GoogleReaderCompatibleTag]?) { + + guard let tags = tags else { return } + + os_log(.debug, log: log, "Syncing folders with %ld tags.", tags.count) + + let tagNames = tags.map { $0.name } + + // Delete any folders not at GoogleReaderCompatible + if let folders = account.folders { + folders.forEach { folder in + if !tagNames.contains(folder.name ?? "") { + DispatchQueue.main.sync { + for feed in folder.topLevelFeeds { + account.addFeed(feed) + clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + } + account.deleteFolder(folder) + } + } + } + } + + let folderNames: [String] = { + if let folders = account.folders { + return folders.map { $0.name ?? "" } + } else { + return [String]() + } + }() + + // Make any folders GoogleReaderCompatible has, but we don't + tagNames.forEach { tagName in + if !folderNames.contains(tagName) { + DispatchQueue.main.sync { + _ = account.ensureFolder(with: tagName) + } + } + } + + } + + func refreshFeeds(_ account: Account, completion: @escaping (Result) -> Void) { + + caller.retrieveSubscriptions { result in + switch result { + case .success(let subscriptions): + + self.refreshProgress.completeTask() + self.caller.retrieveTaggings { result in + switch result { + case .success(let taggings): + + self.refreshProgress.completeTask() + self.caller.retrieveIcons { result in + switch result { + case .success(let icons): + + BatchUpdate.shared.perform { + self.syncFeeds(account, subscriptions) + self.syncTaggings(account, taggings) + self.syncFavicons(account, icons) + } + + self.refreshProgress.completeTask() + completion(.success(())) + + case .failure(let error): + completion(.failure(error)) + } + + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + + func syncFeeds(_ account: Account, _ subscriptions: [GoogleReaderCompatibleSubscription]?) { + + guard let subscriptions = subscriptions else { return } + + os_log(.debug, log: log, "Syncing feeds with %ld subscriptions.", subscriptions.count) + + let subFeedIds = subscriptions.map { String($0.feedID) } + + // Remove any feeds that are no longer in the subscriptions + if let folders = account.folders { + for folder in folders { + for feed in folder.topLevelFeeds { + if !subFeedIds.contains(feed.feedID) { + DispatchQueue.main.sync { + folder.removeFeed(feed) + } + } + } + } + } + + for feed in account.topLevelFeeds { + if !subFeedIds.contains(feed.feedID) { + DispatchQueue.main.sync { + account.removeFeed(feed) + } + } + } + + // Add any feeds we don't have and update any we do + subscriptions.forEach { subscription in + + let subFeedId = String(subscription.feedID) + + DispatchQueue.main.sync { + if let feed = account.idToFeedDictionary[subFeedId] { + feed.name = subscription.name + feed.homePageURL = subscription.homePageURL + } else { + let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) + feed.subscriptionID = String(subscription.subscriptionID) + account.addFeed(feed) + } + } + + } + + } + + func syncTaggings(_ account: Account, _ taggings: [GoogleReaderCompatibleTagging]?) { + + guard let taggings = taggings else { return } + + os_log(.debug, log: log, "Syncing taggings with %ld taggings.", taggings.count) + + // Set up some structures to make syncing easier + let folderDict: [String: Folder] = { + if let folders = account.folders { + return Dictionary(uniqueKeysWithValues: folders.map { ($0.name ?? "", $0) } ) + } else { + return [String: Folder]() + } + }() + + let taggingsDict = taggings.reduce([String: [GoogleReaderCompatibleTagging]]()) { (dict, tagging) in + var taggedFeeds = dict + if var taggedFeed = taggedFeeds[tagging.name] { + taggedFeed.append(tagging) + taggedFeeds[tagging.name] = taggedFeed + } else { + taggedFeeds[tagging.name] = [tagging] + } + return taggedFeeds + } + + // Sync the folders + for (folderName, groupedTaggings) in taggingsDict { + + guard let folder = folderDict[folderName] else { return } + + let taggingFeedIDs = groupedTaggings.map { String($0.feedID) } + + // Move any feeds not in the folder to the account + for feed in folder.topLevelFeeds { + if !taggingFeedIDs.contains(feed.feedID) { + DispatchQueue.main.sync { + folder.removeFeed(feed) + clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + account.addFeed(feed) + } + } + } + + // Add any feeds not in the folder + let folderFeedIds = folder.topLevelFeeds.map { $0.feedID } + + for tagging in groupedTaggings { + let taggingFeedID = String(tagging.feedID) + if !folderFeedIds.contains(taggingFeedID) { + guard let feed = account.idToFeedDictionary[taggingFeedID] else { + continue + } + DispatchQueue.main.sync { + saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID)) + folder.addFeed(feed) + } + } + } + + } + + let taggedFeedIDs = Set(taggings.map { String($0.feedID) }) + + // Remove all feeds from the account container that have a tag + DispatchQueue.main.sync { + for feed in account.topLevelFeeds { + if taggedFeedIDs.contains(feed.feedID) { + account.removeFeed(feed) + } + } + } + + } + + func syncFavicons(_ account: Account, _ icons: [GoogleReaderCompatibleIcon]?) { + + guard let icons = icons else { return } + + os_log(.debug, log: log, "Syncing favicons with %ld icons.", icons.count) + + let iconDict = Dictionary(uniqueKeysWithValues: icons.map { ($0.host, $0.url) } ) + + for feed in account.flattenedFeeds() { + for (key, value) in iconDict { + if feed.homePageURL?.contains(key) ?? false { + DispatchQueue.main.sync { + feed.faviconURL = value + } + break + } + } + } + + } + + + func sendArticleStatuses(_ statuses: [SyncStatus], + apiCall: ([Int], @escaping (Result) -> Void) -> Void, + completion: @escaping (() -> Void)) { + + guard !statuses.isEmpty else { + completion() + return + } + + let group = DispatchGroup() + + let articleIDs = statuses.compactMap { Int($0.articleID) } + let articleIDGroups = articleIDs.chunked(into: 1000) + for articleIDGroup in articleIDGroups { + + group.enter() + apiCall(articleIDGroup) { result in + switch result { + case .success: + self.database.deleteSelectedForProcessing(articleIDGroup.map { String($0) } ) + group.leave() + case .failure(let error): + os_log(.error, log: self.log, "Article status sync call failed: %@.", error.localizedDescription) + self.database.resetSelectedForProcessing(articleIDGroup.map { String($0) } ) + group.leave() + } + } + + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + + } + + func processRestoredFeed(for account: Account, feed: Feed, editedName: String?, folder: Folder?, completion: @escaping (Result) -> Void) { + + if let folder = folder { + + addFeed(for: account, to: folder, with: feed) { result in + + switch result { + case .success: + + if editedName != nil { + DispatchQueue.main.async { + account.removeFeed(feed) + folder.addFeed(feed) + } + self.processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion) + } else { + DispatchQueue.main.async { + account.removeFeed(feed) + folder.addFeed(feed) + completion(.success(())) + } + } + + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(error)) + } + } + + } + + } else { + + DispatchQueue.main.async { + account.addFeed(feed) + } + + if editedName != nil { + processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion) + } else { + DispatchQueue.main.async { + completion(.success(())) + } + } + + } + + } + + func processRestoredFeedName(for account: Account, feed: Feed, editedName: String, completion: @escaping (Result) -> Void) { + + renameFeed(for: account, with: feed, to: editedName) { result in + switch result { + case .success: + DispatchQueue.main.async { + feed.editedName = editedName + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + completion(.failure(error)) + } + } + + } + + } + + func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) { + if var folderRelationship = feed.folderRelationship { + folderRelationship[folderName] = nil + feed.folderRelationship = folderRelationship + } + } + + func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) { + if var folderRelationship = feed.folderRelationship { + folderRelationship[folderName] = id + feed.folderRelationship = folderRelationship + } else { + feed.folderRelationship = [folderName: id] + } + } + + func decideBestFeedChoice(account: Account, url: String, choices: [GoogleReaderCompatibleSubscriptionChoice], completion: @escaping (Result) -> Void) { + + let feedSpecifiers: [FeedSpecifier] = choices.map { choice in + let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink + let specifier = FeedSpecifier(title: choice.name, urlString: choice.url, source: source) + return specifier + } + + if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) { + if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first { + createFeed(for: account, url: bestSubscription.url, completion: completion) + } else { + DispatchQueue.main.async { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + } + } + } else { + DispatchQueue.main.async { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + } + } + + } + + func createFeed( account: Account, subscription sub: GoogleReaderCompatibleSubscription, completion: @escaping (Result) -> Void) { + + DispatchQueue.main.async { + + let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL) + feed.subscriptionID = String(sub.subscriptionID) + + // Download the initial articles + self.caller.retrieveEntries(feedID: feed.feedID) { result in + + switch result { + case .success(let (entries, page)): + + self.processEntries(account: account, entries: entries) { + self.refreshArticles(account, page: page) { + self.refreshArticleStatus(for: account) { + self.refreshMissingArticles(account) { + DispatchQueue.main.async { + completion(.success(feed)) + } + } + } + } + } + + case .failure(let error): + os_log(.error, log: self.log, "Initial articles download failed: %@.", error.localizedDescription) + DispatchQueue.main.async { + completion(.success(feed)) + } + } + + } + + } + + } + + func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) { + + os_log(.debug, log: log, "Refreshing articles...") + + caller.retrieveEntries() { result in + + switch result { + case .success(let (entries, page, lastPageNumber)): + + if let last = lastPageNumber { + self.refreshProgress.addToNumberOfTasksAndRemaining(last - 1) + } + + self.processEntries(account: account, entries: entries) { + + self.refreshProgress.completeTask() + self.refreshArticles(account, page: page) { + os_log(.debug, log: self.log, "Done refreshing articles.") + completion() + } + + } + + case .failure(let error): + os_log(.error, log: self.log, "Refresh articles failed: %@.", error.localizedDescription) + completion() + } + + } + + } + + func refreshMissingArticles(_ account: Account, completion: @escaping (() -> Void)) { + + os_log(.debug, log: log, "Refreshing missing articles...") + let articleIDs = Array(account.fetchArticleIDsForStatusesWithoutArticles()) + + let group = DispatchGroup() + + let chunkedArticleIDs = articleIDs.chunked(into: 100) + + for chunk in chunkedArticleIDs { + + group.enter() + caller.retrieveEntries(articleIDs: chunk) { result in + + switch result { + case .success(let entries): + + self.processEntries(account: account, entries: entries) { + group.leave() + } + + case .failure(let error): + os_log(.error, log: self.log, "Refresh missing articles failed: %@.", error.localizedDescription) + group.leave() + } + + } + + } + + group.notify(queue: DispatchQueue.main) { + self.refreshProgress.completeTask() + os_log(.debug, log: self.log, "Done refreshing missing articles.") + completion() + } + + } + + func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) { + + guard let page = page else { + completion() + return + } + + caller.retrieveEntries(page: page) { result in + + switch result { + case .success(let (entries, nextPage)): + + self.processEntries(account: account, entries: entries) { + self.refreshProgress.completeTask() + self.refreshArticles(account, page: nextPage, completion: completion) + } + + case .failure(let error): + os_log(.error, log: self.log, "Refresh articles for additional pages failed: %@.", error.localizedDescription) + completion() + } + + } + + } + + func processEntries(account: Account, entries: [GoogleReaderCompatibleEntry]?, completion: @escaping (() -> Void)) { + + let parsedItems = mapEntriesToParsedItems(entries: entries) + let parsedMap = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ) + + let group = DispatchGroup() + + for (feedID, mapItems) in parsedMap { + + group.enter() + + if let feed = account.idToFeedDictionary[feedID] { + DispatchQueue.main.async { + account.update(feed, parsedItems: Set(mapItems), defaultRead: true) { + group.leave() + } + } + } else { + group.leave() + } + + } + + group.notify(queue: DispatchQueue.main) { + completion() + } + + } + + func mapEntriesToParsedItems(entries: [GoogleReaderCompatibleEntry]?) -> Set { + + guard let entries = entries else { + return Set() + } + + let parsedItems: [ParsedItem] = entries.map { entry in + let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) + return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: authors, tags: nil, attachments: nil) + } + + return Set(parsedItems) + + } + + func syncArticleReadState(account: Account, articleIDs: [Int]?) { + + guard let articleIDs = articleIDs else { + return + } + + let GoogleReaderCompatibleUnreadArticleIDs = Set(articleIDs.map { String($0) } ) + let currentUnreadArticleIDs = account.fetchUnreadArticleIDs() + + // Mark articles as unread + let deltaUnreadArticleIDs = GoogleReaderCompatibleUnreadArticleIDs.subtracting(currentUnreadArticleIDs) + let markUnreadArticles = account.fetchArticles(forArticleIDs: deltaUnreadArticleIDs) + DispatchQueue.main.async { + _ = account.update(markUnreadArticles, statusKey: .read, flag: false) + } + + // Save any unread statuses for articles we haven't yet received + let markUnreadArticleIDs = Set(markUnreadArticles.map { $0.articleID }) + let missingUnreadArticleIDs = deltaUnreadArticleIDs.subtracting(markUnreadArticleIDs) + if !missingUnreadArticleIDs.isEmpty { + DispatchQueue.main.async { + account.ensureStatuses(missingUnreadArticleIDs, .read, false) + } + } + + // Mark articles as read + let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(GoogleReaderCompatibleUnreadArticleIDs) + let markReadArticles = account.fetchArticles(forArticleIDs: deltaReadArticleIDs) + DispatchQueue.main.async { + _ = account.update(markReadArticles, statusKey: .read, flag: true) + } + + // Save any read statuses for articles we haven't yet received + let markReadArticleIDs = Set(markReadArticles.map { $0.articleID }) + let missingReadArticleIDs = deltaReadArticleIDs.subtracting(markReadArticleIDs) + if !missingReadArticleIDs.isEmpty { + DispatchQueue.main.async { + account.ensureStatuses(missingReadArticleIDs, .read, true) + } + } + + } + + func syncArticleStarredState(account: Account, articleIDs: [Int]?) { + + guard let articleIDs = articleIDs else { + return + } + + let GoogleReaderCompatibleStarredArticleIDs = Set(articleIDs.map { String($0) } ) + let currentStarredArticleIDs = account.fetchStarredArticleIDs() + + // Mark articles as starred + let deltaStarredArticleIDs = GoogleReaderCompatibleStarredArticleIDs.subtracting(currentStarredArticleIDs) + let markStarredArticles = account.fetchArticles(forArticleIDs: deltaStarredArticleIDs) + DispatchQueue.main.async { + _ = account.update(markStarredArticles, statusKey: .starred, flag: true) + } + + // Save any starred statuses for articles we haven't yet received + let markStarredArticleIDs = Set(markStarredArticles.map { $0.articleID }) + let missingStarredArticleIDs = deltaStarredArticleIDs.subtracting(markStarredArticleIDs) + if !missingStarredArticleIDs.isEmpty { + DispatchQueue.main.async { + account.ensureStatuses(missingStarredArticleIDs, .starred, true) + } + } + + // Mark articles as unstarred + let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(GoogleReaderCompatibleStarredArticleIDs) + let markUnstarredArticles = account.fetchArticles(forArticleIDs: deltaUnstarredArticleIDs) + DispatchQueue.main.async { + _ = account.update(markUnstarredArticles, statusKey: .starred, flag: false) + } + + // Save any unstarred statuses for articles we haven't yet received + let markUnstarredArticleIDs = Set(markUnstarredArticles.map { $0.articleID }) + let missingUnstarredArticleIDs = deltaUnstarredArticleIDs.subtracting(markUnstarredArticleIDs) + if !missingUnstarredArticleIDs.isEmpty { + DispatchQueue.main.async { + account.ensureStatuses(missingUnstarredArticleIDs, .starred, false) + } + } + + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift new file mode 100644 index 000000000..c292eab35 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift @@ -0,0 +1,21 @@ +// +// GoogleReaderCompatibleDate.swift +// Account +// +// Created by Maurice Parker on 5/12/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleDate { + + public static var formatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" + formatter.locale = Locale(identifier: "en_US") + formatter.timeZone = TimeZone(abbreviation: "GMT") + return formatter + }() + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift new file mode 100644 index 000000000..24fabd9dc --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift @@ -0,0 +1,67 @@ +// +// GoogleReaderCompatibleArticle.swift +// Account +// +// Created by Brent Simmons on 12/11/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSParser +import RSCore + +struct GoogleReaderCompatibleEntry: Codable { + + 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: GoogleReaderCompatibleEntryJSONFeed? + + enum CodingKeys: String, CodingKey { + case articleID = "id" + case feedID = "feed_id" + case title = "title" + case url = "url" + case authorName = "author" + case contentHTML = "content" + case summary = "summary" + case datePublished = "published" + case dateArrived = "created_at" + case jsonFeed = "json_feed" + } + + // GoogleReaderCompatible dates can't be decoded by the JSONDecoding 8601 decoding strategy. GoogleReaderCompatible + // 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. + func parseDatePublished() -> Date? { + if datePublished != nil { + return GoogleReaderCompatibleDate.formatter.date(from: datePublished!) + } else { + return nil + } + } + +} + +struct GoogleReaderCompatibleEntryJSONFeed: Codable { + let jsonFeedAuthor: GoogleReaderCompatibleEntryJSONFeedAuthor? + enum CodingKeys: String, CodingKey { + case jsonFeedAuthor = "author" + } +} + +struct GoogleReaderCompatibleEntryJSONFeedAuthor: Codable { + let url: String? + let avatarURL: String? + enum CodingKeys: String, CodingKey { + case url = "url" + case avatarURL = "avatar" + } +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift new file mode 100644 index 000000000..69b2fc5d7 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift @@ -0,0 +1,21 @@ +// +// GoogleReaderCompatibleIcon.swift +// Account +// +// Created by Maurice Parker on 5/6/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleIcon: Codable { + + let host: String + let url: String + + enum CodingKeys: String, CodingKey { + case host + case url + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift new file mode 100644 index 000000000..fbc6a45e9 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift @@ -0,0 +1,21 @@ +// +// GoogleReaderCompatibleImportResult.swift +// Account +// +// Created by Maurice Parker on 5/17/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleImportResult: Codable { + + let importResultID: Int + let complete: Bool + + enum CodingKeys: String, CodingKey { + case importResultID = "id" + case complete + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift new file mode 100644 index 000000000..8036f7b63 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift @@ -0,0 +1,19 @@ +// +// GoogleReaderCompatibleStarredEntry.swift +// Account +// +// Created by Maurice Parker on 5/15/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleStarredEntry: Codable { + + let starredEntries: [Int] + + enum CodingKeys: String, CodingKey { + case starredEntries = "starred_entries" + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift new file mode 100644 index 000000000..6850133fd --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift @@ -0,0 +1,55 @@ +// +// GoogleReaderCompatibleFeed.swift +// Account +// +// Created by Brent Simmons on 12/10/17. +// Copyright © 2017 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSCore +import RSParser + +struct GoogleReaderCompatibleSubscription: Codable { + + let subscriptionID: Int + let feedID: Int + let name: String? + let url: String + let homePageURL: String? + + enum CodingKeys: String, CodingKey { + case subscriptionID = "id" + case feedID = "feed_id" + case name = "title" + case url = "feed_url" + case homePageURL = "site_url" + } + +} + +struct GoogleReaderCompatibleCreateSubscription: Codable { + let feedURL: String + enum CodingKeys: String, CodingKey { + case feedURL = "feed_url" + } +} + +struct GoogleReaderCompatibleUpdateSubscription: Codable { + let title: String + enum CodingKeys: String, CodingKey { + case title + } +} + +struct GoogleReaderCompatibleSubscriptionChoice: Codable { + + let name: String? + let url: String + + enum CodingKeys: String, CodingKey { + case name = "title" + case url = "feed_url" + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift new file mode 100644 index 000000000..3daf9f0eb --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift @@ -0,0 +1,43 @@ +// +// GoogleReaderCompatibleTag.swift +// Account +// +// Created by Maurice Parker on 5/5/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleTag: Codable { + + let tagID: Int + let name: String + + enum CodingKeys: String, CodingKey { + case tagID = "id" + case name = "name" + } + +} + +struct GoogleReaderCompatibleRenameTag: Codable { + + let oldName: String + let newName: String + + enum CodingKeys: String, CodingKey { + case oldName = "old_name" + case newName = "new_name" + } + +} + +struct GoogleReaderCompatibleDeleteTag: Codable { + + let name: String + + enum CodingKeys: String, CodingKey { + case name + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift new file mode 100644 index 000000000..b3a0f57aa --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift @@ -0,0 +1,35 @@ +// +// GoogleReaderCompatibleTagging.swift +// Account +// +// Created by Brent Simmons on 10/14/18. +// Copyright © 2018 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleTagging: Codable { + + let taggingID: Int + let feedID: Int + let name: String + + enum CodingKeys: String, CodingKey { + case taggingID = "id" + case feedID = "feed_id" + case name = "name" + } + +} + +struct GoogleReaderCompatibleCreateTagging: Codable { + + let feedID: Int + let name: String + + enum CodingKeys: String, CodingKey { + case feedID = "feed_id" + case name = "name" + } + +} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift new file mode 100644 index 000000000..a5ce66ae0 --- /dev/null +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift @@ -0,0 +1,19 @@ +// +// GoogleReaderCompatibleUnreadEntry.swift +// Account +// +// Created by Maurice Parker on 5/15/19. +// Copyright © 2019 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct GoogleReaderCompatibleUnreadEntry: Codable { + + let unreadEntries: [Int] + + enum CodingKeys: String, CodingKey { + case unreadEntries = "unread_entries" + } + +} diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index 6c3a4158f..cf091fff1 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -39,7 +39,7 @@ class AccountsAddViewController: NSViewController { extension AccountsAddViewController: NSTableViewDataSource { func numberOfRows(in tableView: NSTableView) -> Int { - return 2 + return 3 } func tableView(_ tableView: NSTableView, objectValueFor tableColumn: NSTableColumn?, row: Int) -> Any? { @@ -63,6 +63,9 @@ extension AccountsAddViewController: NSTableViewDelegate { case 1: cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin") cell.accountImageView?.image = AppAssets.accountFeedbin + case 2: + cell.accountNameLabel?.stringValue = NSLocalizedString("Google Reader API", comment: "Google Reader API") + cell.accountImageView?.image = AppAssets.accountLocal default: break } @@ -87,6 +90,10 @@ extension AccountsAddViewController: NSTableViewDelegate { let accountsFeedbinWindowController = AccountsFeedbinWindowController() accountsFeedbinWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsFeedbinWindowController + case 2: + let accountsGoogleReaderCompatibleWindowController = AccountsGoogleReaderCompatibleWindowController() + accountsGoogleReaderCompatibleWindowController.runSheetOnWindow(self.view.window!) + accountsAddWindowController = accountsGoogleReaderCompatibleWindowController default: break } diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib new file mode 100644 index 000000000..e734fab2c --- /dev/null +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib @@ -0,0 +1,210 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + NSAllRomanInputSourcesLocaleIdentifier + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift new file mode 100644 index 000000000..a8471eba7 --- /dev/null +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift @@ -0,0 +1,125 @@ +// +// AccountsAddFeedbinWindowController.swift +// NetNewsWire +// +// Created by Maurice Parker on 5/2/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import AppKit +import Account +import RSWeb + +class AccountsGoogleReaderCompatibleWindowController: NSWindowController { + + @IBOutlet weak var progressIndicator: NSProgressIndicator! + @IBOutlet weak var usernameTextField: NSTextField! + @IBOutlet weak var apiURLTextField: NSTextField! + @IBOutlet weak var passwordTextField: NSSecureTextField! + @IBOutlet weak var errorMessageLabel: NSTextField! + @IBOutlet weak var actionButton: NSButton! + + var account: Account? + + private weak var hostWindow: NSWindow? + + convenience init() { + self.init(windowNibName: NSNib.Name("AccountsGoogleReaderCompatible")) + } + + override func windowDidLoad() { + if let account = account, let credentials = try? account.retrieveBasicCredentials() { + if case .basic(let username, let password) = credentials { + usernameTextField.stringValue = username + passwordTextField.stringValue = password + } + actionButton.title = NSLocalizedString("Update", comment: "Update") + } else { + actionButton.title = NSLocalizedString("Create", comment: "Create") + } + } + + // MARK: API + + func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) { + self.hostWindow = hostWindow + hostWindow.beginSheet(window!, completionHandler: handler) + } + + // MARK: Actions + + @IBAction func cancel(_ sender: Any) { + hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) + } + + @IBAction func action(_ sender: Any) { + + self.errorMessageLabel.stringValue = "" + + guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty && !apiURLTextField.stringValue.isEmpty else { + self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error") + return + } + + actionButton.isEnabled = false + progressIndicator.isHidden = false + progressIndicator.startAnimation(self) + + guard let apiURL = URL(string: apiURLTextField.stringValue) else { + self.errorMessageLabel.stringValue = NSLocalizedString("Invalie API URL.", comment: "Credentials Error") + return + } + + let credentials = Credentials.googleLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue, apiUrl: apiURL, apiKey: nil) + Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials) { [weak self] result in + + guard let self = self else { return } + + self.actionButton.isEnabled = true + self.progressIndicator.isHidden = true + self.progressIndicator.stopAnimation(self) + + switch result { + case .success(let authenticated): + + if authenticated { + + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .googleReaderCompatible) + newAccount = true + } + + do { + try self.account?.removeBasicCredentials() + try self.account?.storeCredentials(credentials) + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + NSApplication.shared.presentError(error) + } + } + } + self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) + } catch { + self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") + } + + } else { + self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + } + + case .failure: + + self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") + + } + + } + + } + +} diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index 4de8f17af..9edd0539e 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -107,6 +107,8 @@ extension AccountsPreferencesViewController: NSTableViewDelegate { cell.imageView?.image = AppAssets.accountLocal case .feedbin: cell.imageView?.image = NSImage(named: "accountFeedbin") + case .googleReaderCompatible: + cell.imageView?.image = AppAssets.accountLocal default: break } diff --git a/Mac/Scriptability/Account+Scriptability.swift b/Mac/Scriptability/Account+Scriptability.swift index 7c9e9c73c..726d03dc5 100644 --- a/Mac/Scriptability/Account+Scriptability.swift +++ b/Mac/Scriptability/Account+Scriptability.swift @@ -138,6 +138,8 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta osType = "FWrg" case .newsBlur: osType = "NBlr" + case .googleReaderCompatible: + osType = "Grdr" } return osType.fourCharCode() } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index bb3af139d..907a1d012 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -146,6 +146,8 @@ 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; }; 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; + 55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */; }; + 55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */; }; 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; @@ -739,6 +741,8 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; }; 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; + 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsGoogleReaderCompatible.xib; sourceTree = ""; }; + 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsGoogleReaderCompatibleWindowController.swift; sourceTree = ""; }; 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; @@ -1624,6 +1628,8 @@ 84C9FC6F22629E1200D921D6 /* Accounts */ = { isa = PBXGroup; children = ( + 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */, + 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */, 84C9FC7022629E1200D921D6 /* AccountsTableViewBackgroundView.swift */, 84C9FC7122629E1200D921D6 /* AccountsControlsBackgroundView.swift */, 84C9FC7222629E1200D921D6 /* AccountsPreferencesViewController.swift */, @@ -1942,12 +1948,12 @@ ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 6581C73220CED60000F4AD34 = { - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Manual; + DevelopmentTeam = 96VR936H35; + ProvisioningStyle = Automatic; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = 96VR936H35; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -1963,8 +1969,8 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Manual; + DevelopmentTeam = 96VR936H35; + ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.HardenedRuntime = { enabled = 1; @@ -1973,7 +1979,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = 96VR936H35; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -2249,6 +2255,7 @@ 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */, 8405DDA222168920008CE1BF /* TimelineTableView.xib in Resources */, 8483630E2262A3FE00DA1D35 /* MainWindow.storyboard in Resources */, + 55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */, 84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */, 84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */, 84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */, @@ -2467,6 +2474,7 @@ 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */, 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, + 55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */, 5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */, 84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */, 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */, From 171ebefc0f0333c7dffb118c930c8fbf1e91a265 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 28 May 2019 13:22:28 -0400 Subject: [PATCH 02/31] Use my submodule --- submodules/RSWeb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/submodules/RSWeb b/submodules/RSWeb index 59685e506..261feb753 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 59685e50640cd4629294bf2c0d63193ffa4ccc74 +Subproject commit 261feb7537eb7c6dc425fd1c9e24b22dea8fc982 From de82f718d5aa62e1eb469f7011cd3e87f216f88e Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 28 May 2019 13:26:07 -0400 Subject: [PATCH 03/31] Point to my updated version of RSWeb for now. --- .gitmodules | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index 5b7b8b710..1da2a5f2b 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,7 +3,8 @@ url = https://github.com/brentsimmons/RSCore [submodule "submodules/RSWeb"] path = submodules/RSWeb - url = https://github.com/brentsimmons/RSWeb + url = https://github.com/jbeker/RSWeb + branch = google_reader_compatible_syncing [submodule "submodules/RSParser"] path = submodules/RSParser url = https://github.com/brentsimmons/RSParser From 0df86e57616b8ef633b535fbbe038111d06c514f Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Wed, 29 May 2019 10:54:52 -0400 Subject: [PATCH 04/31] Authentication working * Updates to use new API style per discussion with Maurice * Credential validation functioning --- Frameworks/Account/Account.swift | 9 ++- Frameworks/Account/AccountDelegate.swift | 2 +- .../Account/Feedbin/FeedbinAPICaller.swift | 6 +- .../Feedbin/FeedbinAccountDelegate.swift | 2 +- .../GoogleReaderCompatibleAPICaller.swift | 55 ++++++++---------- ...oogleReaderCompatibleAccountDelegate.swift | 9 ++- .../LocalAccount/LocalAccountDelegate.swift | 4 +- .../AccountsFeedbinWindowController.swift | 54 +++++++++--------- ...ogleReaderCompatibleWindowController.swift | 57 +++++++++---------- submodules/RSWeb | 2 +- 10 files changed, 99 insertions(+), 101 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 6c436895c..32598e41a 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -252,6 +252,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, // MARK: - API public func storeCredentials(_ credentials: Credentials) throws { + // The delegate may need the credentials to determine the server + delegate.credentials = credentials + guard let server = delegate.server else { throw CredentialsError.incompleteCredentials } @@ -259,7 +262,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch credentials { case .basic(let username, _): self.username = username - case .googleLogin(let username, _, _, _): + case .googleBasicLogin(let username, _, _): + self.username = username + case .googleAuthLogin(let username, _, _): self.username = username } @@ -284,7 +289,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.username = nil } - public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completion: @escaping (Result) -> Void) { + public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completion: @escaping (Result) -> Void) { switch type { case .onMyMac: LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 314a944c5..4d9a312b4 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -46,6 +46,6 @@ protocol AccountDelegate { // Called at the end of account’s init method. func accountDidInitialize(_ account: Account) - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) + static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) } diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index ba1e0fa94..7c4c482c1 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -42,7 +42,7 @@ final class FeedbinAPICaller: NSObject { self.transport = transport } - func validateCredentials(completion: @escaping (Result) -> Void) { + func validateCredentials(completion: @escaping (Result) -> Void) { let callURL = feedbinBaseURL.appendingPathComponent("authentication.json") let request = URLRequest(url: callURL, credentials: credentials) @@ -50,12 +50,12 @@ final class FeedbinAPICaller: NSObject { transport.send(request: request) { result in switch result { case .success: - completion(.success(true)) + completion(.success(self.credentials)) case .failure(let error): switch error { case TransportError.httpError(let status): if status == 401 { - completion(.success(false)) + completion(.success(self.credentials)) } else { completion(.failure(error)) } diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index eddcd43b1..cf444ce4f 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -485,7 +485,7 @@ final class FeedbinAccountDelegate: AccountDelegate { accountMetadata = account.metadata } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { let caller = FeedbinAPICaller(transport: transport) caller.credentials = credentials diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 002da3d61..a5bdd5005 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -35,41 +35,43 @@ final class GoogleReaderCompatibleAPICaller: NSObject { private var transport: Transport! var credentials: Credentials? - var apiAuthToken: String? weak var accountMetadata: AccountMetadata? + var server: String? { + get { + guard let localCredentials = credentials else { + return nil + } + + switch localCredentials { + case .googleBasicLogin(_, _, let apiUrl): + return apiUrl.host + case .googleAuthLogin(_, _, let apiUrl): + return apiUrl.host + default: + return nil + } + } + } + + init(transport: Transport) { super.init() self.transport = transport } - func validateCredentials(completion: @escaping (Result) -> Void) { + func validateCredentials(completion: @escaping (Result) -> Void) { guard let credentials = credentials else { completion(.failure(CredentialsError.incompleteCredentials)) return } - guard case .googleLogin(let username, let password, let apiUrl, _) = credentials else { + guard case .googleBasicLogin(let username, _, let apiUrl) = credentials else { completion(.failure(CredentialsError.incompleteCredentials)) return } - guard var loginURL = URLComponents(url: apiUrl.appendingPathComponent("/accounts/ClientLogin"), resolvingAgainstBaseURL: false) else { - completion(.failure(CredentialsError.incompleteCredentials)) - return - } - - loginURL.queryItems = [ - URLQueryItem(name: "Email", value: username), - URLQueryItem(name: "Passwd", value: password) - ] - - guard let callURL = loginURL.url else { - completion(.failure(CredentialsError.incompleteCredentials)) - return - } - - let request = URLRequest(url: callURL, credentials: credentials) + let request = URLRequest(url: apiUrl.appendingPathComponent("/accounts/ClientLogin"), credentials: credentials) transport.send(request: request) { result in switch result { @@ -97,20 +99,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Save Auth Token for later use - self.apiAuthToken = authString + self.credentials = .googleAuthLogin(username: username, apiKey: authString, url: apiUrl) - completion(.success(true)) + completion(.success(self.credentials)) case .failure(let error): - switch error { - case TransportError.httpError(let status): - if status == 401 { - completion(.success(false)) - } else { - completion(.failure(error)) - } - default: - completion(.failure(error)) - } + completion(.failure(error)) } } diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 14dacd849..b27193885 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -31,7 +31,12 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "GoogleReaderCompatible") let supportsSubFolders = false - let server: String? = "api.GoogleReaderCompatible.com" + var server: String? { + get { + return caller.server + } + } + var opmlImportInProgress = false var credentials: Credentials? { @@ -485,7 +490,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { accountMetadata = account.metadata } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { let caller = GoogleReaderCompatibleAPICaller(transport: transport) caller.credentials = credentials diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index f9efdbc31..cf06a8074 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -182,8 +182,8 @@ final class LocalAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: (Result) -> Void) { - return completion(.success(false)) + static func validateCredentials(transport: Transport, credentials: Credentials, completion: (Result) -> Void) { + return completion(.success(nil)) } } diff --git a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift index 11cdcc109..191599c07 100644 --- a/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsFeedbinWindowController.swift @@ -74,38 +74,36 @@ class AccountsFeedbinWindowController: NSWindowController { self.progressIndicator.stopAnimation(self) switch result { - case .success(let authenticated): - - if authenticated { - - var newAccount = false - if self.account == nil { - self.account = AccountManager.shared.createAccount(type: .feedbin) - newAccount = true - } - - do { - try self.account?.removeBasicCredentials() - try self.account?.storeCredentials(credentials) - if newAccount { - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - NSApplication.shared.presentError(error) - } + case .success(let validatedCredentials): + + guard let validatedCredentials = validatedCredentials else { + self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + return + } + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .feedbin) + newAccount = true + } + + do { + try self.account?.removeBasicCredentials() + try self.account?.storeCredentials(validatedCredentials) + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + NSApplication.shared.presentError(error) } } - self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) - } catch { - self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") } - - } else { - self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) + } catch { + self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") } - + case .failure: self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift index a8471eba7..71072ae24 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift @@ -70,7 +70,7 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { return } - let credentials = Credentials.googleLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue, apiUrl: apiURL, apiKey: nil) + let credentials = Credentials.googleBasicLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue, url: apiURL) Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials) { [weak self] result in guard let self = self else { return } @@ -80,42 +80,39 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { self.progressIndicator.stopAnimation(self) switch result { - case .success(let authenticated): + case .success(let validatedCredentials): + guard let validatedCredentials = validatedCredentials else { + self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + return + } - if authenticated { - - var newAccount = false - if self.account == nil { - self.account = AccountManager.shared.createAccount(type: .googleReaderCompatible) - newAccount = true - } - - do { - try self.account?.removeBasicCredentials() - try self.account?.storeCredentials(credentials) - if newAccount { - self.account?.refreshAll() { result in - switch result { - case .success: - break - case .failure(let error): - NSApplication.shared.presentError(error) - } + + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: .googleReaderCompatible) + newAccount = true + } + + do { + try self.account?.removeBasicCredentials() + try self.account?.storeCredentials(validatedCredentials) + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + NSApplication.shared.presentError(error) } } - self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) - } catch { - self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") } - - } else { - self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") + self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) + } catch { + self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") } - + case .failure: - self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") - } } diff --git a/submodules/RSWeb b/submodules/RSWeb index 261feb753..07ec7f917 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 261feb7537eb7c6dc425fd1c9e24b22dea8fc982 +Subproject commit 07ec7f9179dfdf7e89fc97a4a90c4690fa71581a From aa6dfe8a0885f6a1cc60576496cca2898287aa9d Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Wed, 29 May 2019 15:16:09 -0400 Subject: [PATCH 05/31] Rework to store endpoint URL in metadata Endpoint URL did not belong with credentials. This breaks it out and stores it in the account metadata. Updates validation code to take it as a parameter. --- Frameworks/Account/Account.swift | 19 ++++++++-- Frameworks/Account/AccountDelegate.swift | 2 +- Frameworks/Account/AccountMetadata.swift | 9 +++++ .../Feedbin/FeedbinAccountDelegate.swift | 2 +- .../GoogleReaderCompatibleAPICaller.swift | 37 ++++++++++--------- ...oogleReaderCompatibleAccountDelegate.swift | 8 +++- .../LocalAccount/LocalAccountDelegate.swift | 2 +- ...ogleReaderCompatibleWindowController.swift | 8 +++- submodules/RSWeb | 2 +- 9 files changed, 60 insertions(+), 29 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 32598e41a..f2802c5d1 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -121,6 +121,17 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } } + public var endpointURL: URL? { + get { + return metadata.endpointURL + } + set { + if newValue != metadata.endpointURL { + metadata.endpointURL = newValue + } + } + } + private var fetchingAllUnreadCounts = false var isUnreadCountsInitialized = false @@ -262,9 +273,9 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch credentials { case .basic(let username, _): self.username = username - case .googleBasicLogin(let username, _, _): + case .googleBasicLogin(let username, _): self.username = username - case .googleAuthLogin(let username, _, _): + case .googleAuthLogin(let username, _): self.username = username } @@ -289,14 +300,14 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.username = nil } - public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completion: @escaping (Result) -> Void) { + public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { switch type { case .onMyMac: LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) case .feedbin: FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) case .googleReaderCompatible: - GoogleReaderCompatibleAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) + GoogleReaderCompatibleAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion) default: break } diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 4d9a312b4..8c349203c 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -46,6 +46,6 @@ protocol AccountDelegate { // Called at the end of account’s init method. func accountDidInitialize(_ account: Account) - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result) -> Void) } diff --git a/Frameworks/Account/AccountMetadata.swift b/Frameworks/Account/AccountMetadata.swift index 3229a4689..a741bfb12 100644 --- a/Frameworks/Account/AccountMetadata.swift +++ b/Frameworks/Account/AccountMetadata.swift @@ -21,6 +21,7 @@ final class AccountMetadata: Codable { case username case conditionalGetInfo case lastArticleFetch + case endpointURL } var name: String? { @@ -62,6 +63,14 @@ final class AccountMetadata: Codable { } } } + + var endpointURL: URL? { + didSet { + if endpointURL != oldValue { + valueDidChange(.endpointURL) + } + } + } weak var delegate: AccountMetadataDelegate? diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index cf444ce4f..a684fa611 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -485,7 +485,7 @@ final class FeedbinAccountDelegate: AccountDelegate { accountMetadata = account.metadata } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { let caller = FeedbinAPICaller(transport: transport) caller.credentials = credentials diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index a5bdd5005..70c8ddde1 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -39,18 +39,17 @@ final class GoogleReaderCompatibleAPICaller: NSObject { var server: String? { get { - guard let localCredentials = credentials else { - return nil - } - - switch localCredentials { - case .googleBasicLogin(_, _, let apiUrl): - return apiUrl.host - case .googleAuthLogin(_, _, let apiUrl): - return apiUrl.host - default: + return APIBaseURL?.host + } + } + + private var APIBaseURL: URL? { + get { + guard let accountMetadata = accountMetadata else { return nil } + + return accountMetadata.endpointURL } } @@ -60,18 +59,18 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.transport = transport } - func validateCredentials(completion: @escaping (Result) -> Void) { + func validateCredentials(endpoint: URL, completion: @escaping (Result) -> Void) { guard let credentials = credentials else { completion(.failure(CredentialsError.incompleteCredentials)) return } - guard case .googleBasicLogin(let username, _, let apiUrl) = credentials else { + guard case .googleBasicLogin(let username, _) = credentials else { completion(.failure(CredentialsError.incompleteCredentials)) return } - let request = URLRequest(url: apiUrl.appendingPathComponent("/accounts/ClientLogin"), credentials: credentials) + let request = URLRequest(url: endpoint.appendingPathComponent("/accounts/ClientLogin"), credentials: credentials) transport.send(request: request) { result in switch result { @@ -99,7 +98,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Save Auth Token for later use - self.credentials = .googleAuthLogin(username: username, apiKey: authString, url: apiUrl) + self.credentials = .googleAuthLogin(username: username, apiKey: authString) completion(.success(self.credentials)) case .failure(let error): @@ -159,10 +158,14 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func retrieveTags(completion: @escaping (Result<[GoogleReaderCompatibleTag]?, Error>) -> Void) { + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + let callURL = baseURL.appendingPathComponent("/reader/api/0/tag/list?output=json") + //let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] + let request = URLRequest(url: callURL, credentials: credentials) transport.send(request: request, resultType: [GoogleReaderCompatibleTag].self) { result in diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index b27193885..e865079d9 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -490,11 +490,15 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { accountMetadata = account.metadata } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: @escaping (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result) -> Void) { + guard let endpoint = endpoint else { + completion(.failure(TransportError.noURL)) + return + } let caller = GoogleReaderCompatibleAPICaller(transport: transport) caller.credentials = credentials - caller.validateCredentials() { result in + caller.validateCredentials(endpoint: endpoint) { result in DispatchQueue.main.async { completion(result) } diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index cf06a8074..eb5fee0d9 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -182,7 +182,7 @@ final class LocalAccountDelegate: AccountDelegate { func accountDidInitialize(_ account: Account) { } - static func validateCredentials(transport: Transport, credentials: Credentials, completion: (Result) -> Void) { + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: (Result) -> Void) { return completion(.success(nil)) } diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift index 71072ae24..4ed008fe2 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift @@ -70,8 +70,8 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { return } - let credentials = Credentials.googleBasicLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue, url: apiURL) - Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials) { [weak self] result in + let credentials = Credentials.googleBasicLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue) + Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials, endpoint: apiURL) { [weak self] result in guard let self = self else { return } @@ -94,8 +94,12 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { } do { + self.account?.endpointURL = apiURL + try self.account?.removeBasicCredentials() try self.account?.storeCredentials(validatedCredentials) + + if newAccount { self.account?.refreshAll() { result in switch result { diff --git a/submodules/RSWeb b/submodules/RSWeb index 07ec7f917..f3bcd0312 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 07ec7f9179dfdf7e89fc97a4a90c4690fa71581a +Subproject commit f3bcd0312d6797e2722760c1de622e2957114802 From fdc0374c4f5cd893ab216a2ed125864cd113faf3 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Thu, 30 May 2019 07:48:34 -0400 Subject: [PATCH 06/31] Ensure credentials are available for use Need to load metadad first --- Frameworks/Account/Account.swift | 18 ++++++++++-- .../GoogleReaderCompatibleAPICaller.swift | 16 ++++++++++- ...oogleReaderCompatibleAccountDelegate.swift | 28 +++++++++++-------- ...ogleReaderCompatibleWindowController.swift | 3 +- submodules/RSWeb | 2 +- 5 files changed, 48 insertions(+), 19 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index f2802c5d1..fd20c7bde 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -263,9 +263,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, // MARK: - API public func storeCredentials(_ credentials: Credentials) throws { - // The delegate may need the credentials to determine the server - delegate.credentials = credentials - guard let server = delegate.server else { throw CredentialsError.incompleteCredentials } @@ -300,6 +297,21 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.username = nil } + public func retrieveGoogleAuthCredentials() throws -> Credentials? { + guard let username = self.username, let server = delegate.server else { + return nil + } + return try CredentialsManager.retrieveGoogleAuthCredentials(server: server, username: username) + } + + public func removeGoogleAuthCredentials() throws { + guard let username = self.username, let server = delegate.server else { + return + } + try CredentialsManager.removeGoogleAuthCredentials(server: server, username: username) + self.username = nil + } + public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result) -> Void) { switch type { case .onMyMac: diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 70c8ddde1..b8617f5e8 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -163,7 +163,21 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let callURL = baseURL.appendingPathComponent("/reader/api/0/tag/list?output=json") + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/tag/list"), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "output", value: "json") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + //let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] let request = URLRequest(url: callURL, credentials: credentials) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index e865079d9..a5b905893 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -90,18 +90,22 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { refreshAccount(account) { result in switch result { case .success(): - - self.refreshArticles(account) { - self.refreshArticleStatus(for: account) { - self.refreshMissingArticles(account) { - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(())) - } - } - } + DispatchQueue.main.async { + completion(.success(())) } + +// self.refreshArticles(account) { +// self.refreshArticleStatus(for: account) { +// self.refreshMissingArticles(account) { +// self.refreshProgress.clear() +// DispatchQueue.main.async { +// completion(.success(())) +// } +// } +// } +// } +// case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() @@ -486,8 +490,8 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } func accountDidInitialize(_ account: Account) { - credentials = try? account.retrieveBasicCredentials() accountMetadata = account.metadata + credentials = try? account.retrieveGoogleAuthCredentials() } static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result) -> Void) { @@ -570,7 +574,7 @@ private extension GoogleReaderCompatibleAccountDelegate { os_log(.debug, log: log, "Syncing folders with %ld tags.", tags.count) - let tagNames = tags.map { $0.name } + let tagNames = tags.map { $0.tagID } // Delete any folders not at GoogleReaderCompatible if let folders = account.folders { diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift index 4ed008fe2..89db9f277 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift @@ -96,10 +96,9 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { do { self.account?.endpointURL = apiURL - try self.account?.removeBasicCredentials() + try self.account?.removeGoogleAuthCredentials() try self.account?.storeCredentials(validatedCredentials) - if newAccount { self.account?.refreshAll() { result in switch result { diff --git a/submodules/RSWeb b/submodules/RSWeb index f3bcd0312..cf3a30eb3 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit f3bcd0312d6797e2722760c1de622e2957114802 +Subproject commit cf3a30eb3833d9dd423fed003393e6e3c1a360d4 From 46e21f57e8c3bf5c831efbe0d0ee7aa47d922e55 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Thu, 30 May 2019 16:01:56 -0400 Subject: [PATCH 07/31] Decoding of tags working. --- .../GoogleReaderCompatibleAPICaller.swift | 6 +++--- .../GoogleReaderCompatibleAccountDelegate.swift | 2 ++ .../GoogleReaderCompatibleTag.swift | 14 +++++++++++--- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index b8617f5e8..ca16ea9e7 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -181,12 +181,12 @@ final class GoogleReaderCompatibleAPICaller: NSObject { //let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] let request = URLRequest(url: callURL, credentials: credentials) - transport.send(request: request, resultType: [GoogleReaderCompatibleTag].self) { result in + transport.send(request: request, resultType: GoogleReaderCompatibleTagWrapper.self) { result in switch result { - case .success(let (response, tags)): + case .success(let (response, wrapper)): self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) - completion(.success(tags)) + completion(.success(wrapper?.tags)) case .failure(let error): completion(.failure(error)) } diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index a5b905893..979a5c4bd 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -574,6 +574,8 @@ private extension GoogleReaderCompatibleAccountDelegate { os_log(.debug, log: log, "Syncing folders with %ld tags.", tags.count) + // TODO: filter on folder tag type + // TODO: filter names to get rid of prefixes let tagNames = tags.map { $0.tagID } // Delete any folders not at GoogleReaderCompatible diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift index 3daf9f0eb..f2b1252f8 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift @@ -8,14 +8,22 @@ import Foundation +struct GoogleReaderCompatibleTagWrapper: Codable { + let tags: [GoogleReaderCompatibleTag] + + enum CodingKeys: String, CodingKey { + case tags = "tags" + } +} + struct GoogleReaderCompatibleTag: Codable { - let tagID: Int - let name: String + let tagID: String + let type: String? enum CodingKeys: String, CodingKey { case tagID = "id" - case name = "name" + case type = "type" } } From 76d1daf12219c8959de849c43d9eb37d542ac887 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 1 Jun 2019 06:54:10 -0400 Subject: [PATCH 08/31] Cleanup merge issues/API changes --- ...oogleReaderCompatibleAccountDelegate.swift | 357 +++++++++--------- 1 file changed, 169 insertions(+), 188 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 979a5c4bd..c8e9f851b 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -31,6 +31,8 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "GoogleReaderCompatible") let supportsSubFolders = false + let usesTags = true + var server: String? { get { return caller.server @@ -239,6 +241,14 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } + func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { + if let folder = account.ensureFolder(with: name) { + completion(.success(folder)) + } else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + } + } + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { caller.renameTag(oldName: folder.name ?? "", newName: name) { result in @@ -258,51 +268,46 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } - func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { + func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - // GoogleReaderCompatible uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system + // Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system guard folder.hasAtLeastOneFeed() else { - account.deleteFolder(folder) + account.removeFolder(folder) return } - // After we successfully delete at GoogleReaderCompatible, we add all the feeds to the account to save them. We then - // delete the folder. We then sync the taggings we received on the delete to remove any feeds from - // the account that might be in another folder. - caller.deleteTag(name: folder.name ?? "") { result in - switch result { - case .success(let taggings): - DispatchQueue.main.sync { - BatchUpdate.shared.perform { - for feed in folder.topLevelFeeds { - account.addFeed(feed) - self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") - } - account.deleteFolder(folder) - } - completion(.success(())) - } - self.syncTaggings(account, taggings) - case .failure(let error): - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + let group = DispatchGroup() + + for feed in folder.topLevelFeeds { + group.enter() + removeFeed(for: account, with: feed, from: folder) { result in + group.leave() + switch result { + case .success: + break + case .failure(let error): + os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription) } } } + group.notify(queue: DispatchQueue.main) { + account.removeFolder(folder) + completion(.success(())) + } + } - func createFeed(for account: Account, url: String, completion: @escaping (Result) -> Void) { + func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { caller.createSubscription(url: url) { result in switch result { case .success(let subResult): switch subResult { case .created(let subscription): - self.createFeed(account: account, subscription: subscription, completion: completion) + self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) case .multipleChoice(let choices): - self.decideBestFeedChoice(account: account, url: url, choices: choices, completion: completion) + self.decideBestFeedChoice(account: account, url: url, name: name, container: container, choices: choices, completion: completion) case .alreadySubscribed: DispatchQueue.main.async { completion(.failure(AccountError.createErrorAlreadySubscribed)) @@ -318,16 +323,16 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { completion(.failure(wrappedError)) } } - + } } - + func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { // This error should never happen guard let subscriptionID = feed.subscriptionID else { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) return } @@ -347,38 +352,32 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } } - - func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result) -> Void) { - - // This error should never happen - guard let subscriptionID = feed.subscriptionID else { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) - return + + func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { + if feed.folderRelationship?.count ?? 0 > 1 { + deleteTagging(for: account, with: feed, from: container, completion: completion) + } else { + account.clearFeedMetadata(feed) + deleteSubscription(for: account, with: feed, from: container, completion: completion) } - - caller.deleteSubscription(subscriptionID: subscriptionID) { result in - switch result { - case .success: - DispatchQueue.main.async { - account.removeFeed(feed) - if let folders = account.folders { - for folder in folders { - folder.removeFeed(feed) - } - } - completion(.success(())) - } - case .failure(let error): - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + } + + func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { + if from is Account { + addFeed(for: account, with: feed, to: to, completion: completion) + } else { + deleteTagging(for: account, with: feed, from: from) { result in + switch result { + case .success: + self.addFeed(for: account, with: feed, to: to, completion: completion) + case .failure(let error): + completion(.failure(error)) } } } - } - func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { + func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { if let folder = container as? Folder, let feedID = Int(feed.feedID) { caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in @@ -386,6 +385,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { case .success(let taggingID): DispatchQueue.main.async { self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) + account.removeFeed(feed) folder.addFeed(feed) completion(.success(())) } @@ -397,55 +397,24 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } } } else { - if let account = container as? Account { - account.addFeed(feed) - } DispatchQueue.main.async { + if let account = container as? Account { + account.addFeedIfNotInAnyFolder(feed) + } completion(.success(())) } } } - func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result) -> Void) { - - if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { - caller.deleteTagging(taggingID: feedTaggingID) { result in - switch result { - case .success: - DispatchQueue.main.async { - folder.removeFeed(feed) - completion(.success(())) - } - case .failure(let error): - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) - } - } - } - } else { - if let account = container as? Account { - account.removeFeed(feed) - } - completion(.success(())) - } + func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { - } - - func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result) -> Void) { - - let editedName = feed.editedName - - createFeed(for: account, url: feed.url) { result in + createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in switch result { - case .success(let feed): - self.processRestoredFeed(for: account, feed: feed, editedName: editedName, folder: folder, completion: completion) + case .success: + completion(.success(())) case .failure(let error): - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) - } + completion(.failure(error)) } } @@ -459,7 +428,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { for feed in folder.topLevelFeeds { group.enter() - addFeed(for: account, to: folder, with: feed) { result in + addFeed(for: account, with: feed, to: folder) { result in if account.topLevelFeeds.contains(feed) { account.removeFeed(feed) } @@ -488,7 +457,6 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { return account.update(articles, statusKey: statusKey, flag: flag) } - func accountDidInitialize(_ account: Account) { accountMetadata = account.metadata credentials = try? account.retrieveGoogleAuthCredentials() @@ -587,7 +555,7 @@ private extension GoogleReaderCompatibleAccountDelegate { account.addFeed(feed) clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") } - account.deleteFolder(folder) + account.removeFolder(folder) } } } @@ -839,73 +807,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func processRestoredFeed(for account: Account, feed: Feed, editedName: String?, folder: Folder?, completion: @escaping (Result) -> Void) { - - if let folder = folder { - - addFeed(for: account, to: folder, with: feed) { result in - - switch result { - case .success: - - if editedName != nil { - DispatchQueue.main.async { - account.removeFeed(feed) - folder.addFeed(feed) - } - self.processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion) - } else { - DispatchQueue.main.async { - account.removeFeed(feed) - folder.addFeed(feed) - completion(.success(())) - } - } - - case .failure(let error): - DispatchQueue.main.async { - completion(.failure(error)) - } - } - - } - - } else { - - DispatchQueue.main.async { - account.addFeed(feed) - } - - if editedName != nil { - processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion) - } else { - DispatchQueue.main.async { - completion(.success(())) - } - } - - } - - } - func processRestoredFeedName(for account: Account, feed: Feed, editedName: String, completion: @escaping (Result) -> Void) { - - renameFeed(for: account, with: feed, to: editedName) { result in - switch result { - case .success: - DispatchQueue.main.async { - feed.editedName = editedName - completion(.success(())) - } - case .failure(let error): - DispatchQueue.main.async { - completion(.failure(error)) - } - } - - } - - } func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) { if var folderRelationship = feed.folderRelationship { @@ -923,7 +825,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } } - func decideBestFeedChoice(account: Account, url: String, choices: [GoogleReaderCompatibleSubscriptionChoice], completion: @escaping (Result) -> Void) { + func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [GoogleReaderCompatibleSubscriptionChoice], completion: @escaping (Result) -> Void) { let feedSpecifiers: [FeedSpecifier] = choices.map { choice in let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink @@ -933,7 +835,7 @@ private extension GoogleReaderCompatibleAccountDelegate { if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) { if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first { - createFeed(for: account, url: bestSubscription.url, completion: completion) + createFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion) } else { DispatchQueue.main.async { completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) @@ -947,44 +849,65 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func createFeed( account: Account, subscription sub: GoogleReaderCompatibleSubscription, completion: @escaping (Result) -> Void) { + func createFeed( account: Account, subscription sub: GoogleReaderCompatibleSubscription, name: String?, container: Container, completion: @escaping (Result) -> Void) { DispatchQueue.main.async { let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL) feed.subscriptionID = String(sub.subscriptionID) - - // Download the initial articles - self.caller.retrieveEntries(feedID: feed.feedID) { result in - + + account.addFeed(feed, to: container) { result in switch result { - case .success(let (entries, page)): - - self.processEntries(account: account, entries: entries) { - self.refreshArticles(account, page: page) { - self.refreshArticleStatus(for: account) { - self.refreshMissingArticles(account) { - DispatchQueue.main.async { - completion(.success(feed)) - } - } + case .success: + if let name = name { + account.renameFeed(feed, to: name) { result in + switch result { + case .success: + self.initialFeedDownload(account: account, feed: feed, completion: completion) + case .failure(let error): + completion(.failure(error)) } } + } else { + self.initialFeedDownload(account: account, feed: feed, completion: completion) } - case .failure(let error): - os_log(.error, log: self.log, "Initial articles download failed: %@.", error.localizedDescription) - DispatchQueue.main.async { - completion(.success(feed)) - } + completion(.failure(error)) } - } - + } } + func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result) -> Void) { + + // Download the initial articles + self.caller.retrieveEntries(feedID: feed.feedID) { result in + + switch result { + case .success(let (entries, page)): + + self.processEntries(account: account, entries: entries) { + self.refreshArticles(account, page: page) { + self.refreshArticleStatus(for: account) { + self.refreshMissingArticles(account) { + DispatchQueue.main.async { + completion(.success(feed)) + } + } + } + } + } + + case .failure(let error): + completion(.failure(error)) + } + + } + + } + func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) { os_log(.debug, log: log, "Refreshing articles...") @@ -1210,5 +1133,63 @@ private extension GoogleReaderCompatibleAccountDelegate { } } + + func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { + + if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { + caller.deleteTagging(taggingID: feedTaggingID) { result in + switch result { + case .success: + DispatchQueue.main.async { + self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + folder.removeFeed(feed) + account.addFeedIfNotInAnyFolder(feed) + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + } else { + if let account = container as? Account { + account.removeFeed(feed) + } + completion(.success(())) + } + + } + + func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { + + // This error should never happen + guard let subscriptionID = feed.subscriptionID else { + completion(.failure(FeedbinAccountDelegateError.invalidParameter)) + return + } + + caller.deleteSubscription(subscriptionID: subscriptionID) { result in + switch result { + case .success: + DispatchQueue.main.async { + account.removeFeed(feed) + if let folders = account.folders { + for folder in folders { + folder.removeFeed(feed) + } + } + completion(.success(())) + } + case .failure(let error): + DispatchQueue.main.async { + let wrappedError = AccountError.wrappedError(error: error, account: account) + completion(.failure(wrappedError)) + } + } + } + + } } From a0efc7fda9f845225cc62123284411643d1c7649 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 1 Jun 2019 08:08:19 -0400 Subject: [PATCH 09/31] Cleanup tag names, fetch subscriptions --- .../GoogleReaderCompatibleAPICaller.swift | 34 +++++++++---- ...oogleReaderCompatibleAccountDelegate.swift | 8 ++-- .../GoogleReaderCompatibleSubscription.swift | 48 ++++++++++++++++--- .../GoogleReaderCompatibleTag.swift | 2 +- 4 files changed, 72 insertions(+), 20 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index ca16ea9e7..05e365e72 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -178,10 +178,10 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - //let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] - let request = URLRequest(url: callURL, credentials: credentials) - - transport.send(request: request, resultType: GoogleReaderCompatibleTagWrapper.self) { result in + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + transport.send(request: request, resultType: GoogleReaderCompatibleTagContainer.self) { result in switch result { case .success(let (response, wrapper)): @@ -222,17 +222,35 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func retrieveSubscriptions(completion: @escaping (Result<[GoogleReaderCompatibleSubscription]?, Error>) -> Void) { + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/subscription/list"), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "output", value: "json") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: [GoogleReaderCompatibleSubscription].self) { result in + transport.send(request: request, resultType: GoogleReaderCompatibleSubscriptionContainer.self) { result in switch result { - case .success(let (response, subscriptions)): + case .success(let (response, container)): self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) - completion(.success(subscriptions)) + completion(.success(container?.subscriptions)) case .failure(let error): completion(.failure(error)) } diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index c8e9f851b..0c5e4692a 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -542,9 +542,7 @@ private extension GoogleReaderCompatibleAccountDelegate { os_log(.debug, log: log, "Syncing folders with %ld tags.", tags.count) - // TODO: filter on folder tag type - // TODO: filter names to get rid of prefixes - let tagNames = tags.map { $0.tagID } + let tagNames = tags.filter { $0.type == "folder" }.map { $0.tagID.replacingOccurrences(of: "user/-/label/", with: "") } // Delete any folders not at GoogleReaderCompatible if let folders = account.folders { @@ -665,7 +663,7 @@ private extension GoogleReaderCompatibleAccountDelegate { feed.homePageURL = subscription.homePageURL } else { let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) - feed.subscriptionID = String(subscription.subscriptionID) + feed.subscriptionID = String(subscription.feedID) account.addFeed(feed) } } @@ -854,7 +852,7 @@ private extension GoogleReaderCompatibleAccountDelegate { DispatchQueue.main.async { let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL) - feed.subscriptionID = String(sub.subscriptionID) + feed.subscriptionID = String(sub.feedID) account.addFeed(feed, to: container) { result in switch result { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift index 6850133fd..7c0c56827 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift @@ -10,24 +10,60 @@ import Foundation import RSCore import RSParser +struct GoogleReaderCompatibleSubscriptionContainer: Codable { + let subscriptions: [GoogleReaderCompatibleSubscription] + + enum CodingKeys: String, CodingKey { + case subscriptions = "subscriptions" + } +} + +/* +{ + "id": "feed/1", + "title": "Questionable Content", + "categories": [ + { + "id": "user/-/label/Comics", + "label": "Comics" + } + ], + "url": "http://www.questionablecontent.net/QCRSS.xml", + "htmlUrl": "http://www.questionablecontent.net", + "iconUrl": "https://rss.confusticate.com/f.php?24decabc" +} + +*/ struct GoogleReaderCompatibleSubscription: Codable { - let subscriptionID: Int - let feedID: Int + let feedID: String let name: String? + let categories: [GoogleReaderCompatibleCategory] let url: String let homePageURL: String? + let iconURL: String? enum CodingKeys: String, CodingKey { - case subscriptionID = "id" - case feedID = "feed_id" + case feedID = "id" case name = "title" - case url = "feed_url" - case homePageURL = "site_url" + case categories = "categories" + case url = "url" + case homePageURL = "htmlUrl" + case iconURL = "iconUrl" } } +struct GoogleReaderCompatibleCategory: Codable { + let categoryId: String + let categoryLabel: String + + enum CodingKeys: String, CodingKey { + case categoryId = "id" + case categoryLabel = "label" + } +} + struct GoogleReaderCompatibleCreateSubscription: Codable { let feedURL: String enum CodingKeys: String, CodingKey { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift index f2b1252f8..16fd2695f 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift @@ -8,7 +8,7 @@ import Foundation -struct GoogleReaderCompatibleTagWrapper: Codable { +struct GoogleReaderCompatibleTagContainer: Codable { let tags: [GoogleReaderCompatibleTag] enum CodingKeys: String, CodingKey { From 6b147e7dc97f091ea92b18f1a38cd38eb5aa422b Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 1 Jun 2019 17:05:55 -0400 Subject: [PATCH 10/31] Add feeds to tags. Set iconURLs remove unused functions. --- .../GoogleReaderCompatibleAPICaller.swift | 20 ---- ...oogleReaderCompatibleAccountDelegate.swift | 106 ++++++------------ 2 files changed, 36 insertions(+), 90 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 05e365e72..e8e206e83 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -344,26 +344,6 @@ final class GoogleReaderCompatibleAPICaller: NSObject { transport.send(request: request, method: HTTPMethod.delete, completion: completion) } - func retrieveTaggings(completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) { - - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.taggings] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - - transport.send(request: request, resultType: [GoogleReaderCompatibleTagging].self) { result in - - switch result { - case .success(let (response, taggings)): - self.storeConditionalGet(key: ConditionalGetKeys.taggings, headers: response.allHeaderFields) - completion(.success(taggings)) - case .failure(let error): - completion(.failure(error)) - } - - } - - } - func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 0c5e4692a..df677389d 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -97,17 +97,17 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } -// self.refreshArticles(account) { + self.refreshArticles(account) { // self.refreshArticleStatus(for: account) { // self.refreshMissingArticles(account) { -// self.refreshProgress.clear() -// DispatchQueue.main.async { -// completion(.success(())) -// } + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } // } // } -// } -// + } + case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() @@ -585,35 +585,15 @@ private extension GoogleReaderCompatibleAccountDelegate { case .success(let subscriptions): self.refreshProgress.completeTask() - self.caller.retrieveTaggings { result in - switch result { - case .success(let taggings): - - self.refreshProgress.completeTask() - self.caller.retrieveIcons { result in - switch result { - case .success(let icons): - BatchUpdate.shared.perform { - self.syncFeeds(account, subscriptions) - self.syncTaggings(account, taggings) - self.syncFavicons(account, icons) - } - - self.refreshProgress.completeTask() - completion(.success(())) - - case .failure(let error): - completion(.failure(error)) - } - - } - - case .failure(let error): - completion(.failure(error)) - } - + BatchUpdate.shared.perform { + self.syncFeeds(account, subscriptions) + self.syncTaggings(account, subscriptions) } + + self.refreshProgress.completeTask() + completion(.success(())) + case .failure(let error): completion(.failure(error)) @@ -663,6 +643,7 @@ private extension GoogleReaderCompatibleAccountDelegate { feed.homePageURL = subscription.homePageURL } else { let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) + feed.iconURL = subscription.iconURL feed.subscriptionID = String(subscription.feedID) account.addFeed(feed) } @@ -672,11 +653,11 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func syncTaggings(_ account: Account, _ taggings: [GoogleReaderCompatibleTagging]?) { + func syncTaggings(_ account: Account, _ subscriptions: [GoogleReaderCompatibleSubscription]?) { - guard let taggings = taggings else { return } + guard let subscriptions = subscriptions else { return } - os_log(.debug, log: log, "Syncing taggings with %ld taggings.", taggings.count) + os_log(.debug, log: log, "Syncing taggings with %ld subscriptions.", subscriptions.count) // Set up some structures to make syncing easier let folderDict: [String: Folder] = { @@ -687,14 +668,21 @@ private extension GoogleReaderCompatibleAccountDelegate { } }() - let taggingsDict = taggings.reduce([String: [GoogleReaderCompatibleTagging]]()) { (dict, tagging) in + let taggingsDict = subscriptions.reduce([String: [GoogleReaderCompatibleSubscription]]()) { (dict, subscription) in var taggedFeeds = dict - if var taggedFeed = taggedFeeds[tagging.name] { - taggedFeed.append(tagging) - taggedFeeds[tagging.name] = taggedFeed - } else { - taggedFeeds[tagging.name] = [tagging] - } + + // For each category that this feed belongs to, add the feed to that name in the dict + subscription.categories.forEach({ (category) in + let categoryName = category.categoryLabel.replacingOccurrences(of: "user/-/label/", with: "") + + if var taggedFeed = taggedFeeds[categoryName] { + taggedFeed.append(subscription) + taggedFeeds[categoryName] = taggedFeed + } else { + taggedFeeds[categoryName] = [subscription] + } + }) + return taggedFeeds } @@ -719,14 +707,14 @@ private extension GoogleReaderCompatibleAccountDelegate { // Add any feeds not in the folder let folderFeedIds = folder.topLevelFeeds.map { $0.feedID } - for tagging in groupedTaggings { - let taggingFeedID = String(tagging.feedID) + for subscription in groupedTaggings { + let taggingFeedID = String(subscription.feedID) if !folderFeedIds.contains(taggingFeedID) { guard let feed = account.idToFeedDictionary[taggingFeedID] else { continue } DispatchQueue.main.sync { - saveFolderRelationship(for: feed, withFolderName: folderName, id: String(tagging.taggingID)) + saveFolderRelationship(for: feed, withFolderName: folderName, id: String(subscription.feedID)) folder.addFeed(feed) } } @@ -734,7 +722,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - let taggedFeedIDs = Set(taggings.map { String($0.feedID) }) + let taggedFeedIDs = Set(subscriptions.map { String($0.feedID) }) // Remove all feeds from the account container that have a tag DispatchQueue.main.sync { @@ -747,28 +735,6 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func syncFavicons(_ account: Account, _ icons: [GoogleReaderCompatibleIcon]?) { - - guard let icons = icons else { return } - - os_log(.debug, log: log, "Syncing favicons with %ld icons.", icons.count) - - let iconDict = Dictionary(uniqueKeysWithValues: icons.map { ($0.host, $0.url) } ) - - for feed in account.flattenedFeeds() { - for (key, value) in iconDict { - if feed.homePageURL?.contains(key) ?? false { - DispatchQueue.main.sync { - feed.faviconURL = value - } - break - } - } - } - - } - - func sendArticleStatuses(_ statuses: [SyncStatus], apiCall: ([Int], @escaping (Result) -> Void) -> Void, completion: @escaping (() -> Void)) { From 9144ee71e53b0a982a1ec581183e4b3917f5fb93 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Mon, 10 Jun 2019 16:53:35 -0400 Subject: [PATCH 11/31] Request article IDs and content. --- .../GoogleReaderCompatibleAPICaller.swift | 230 +++++++++++++++--- ...oogleReaderCompatibleAccountDelegate.swift | 43 ++-- .../GoogleReaderCompatibleEntry.swift | 121 ++++++--- .../GoogleReaderCompatibleUnreadEntry.swift | 16 +- submodules/RSWeb | 2 +- 5 files changed, 314 insertions(+), 98 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index e8e206e83..d1bc13c56 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -108,6 +108,36 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } + func requestAuthorizationToken(endpoint: URL, completion: @escaping (Result) -> Void) { + guard let credentials = credentials else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + let request = URLRequest(url: endpoint.appendingPathComponent("/reader/api/0/token"), credentials: credentials) + + transport.send(request: request) { result in + switch result { + case .success(let (_, data)): + guard let resultData = data else { + completion(.failure(TransportError.noData)) + break + } + + // Convert the return data to UTF8 and then parse out the Auth token + guard let rawData = String(data: resultData, encoding: .utf8) else { + completion(.failure(TransportError.noData)) + break + } + + + completion(.success(rawData)) + case .failure(let error): + completion(.failure(error)) + } + } + } + func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json") @@ -412,24 +442,51 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let concatIDs = articleIDs.reduce("") { param, articleID in return param + ",\(articleID)" } - let paramIDs = String(concatIDs.dropFirst()) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } - var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)! - callComponents.queryItems = [URLQueryItem(name: "ids", value: paramIDs), URLQueryItem(name: "mode", value: "extended")] - let request = URLRequest(url: callComponents.url!, credentials: credentials) - - transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in - + self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { - case .success(let (_, entries)): - completion(.success((entries))) + case .success(let token): + // Do POST asking for data about all the new articles + var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/contents"), credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + // Get ids from above into hex representation of value + let idsToFetch = articleIDs.map({ (reference) -> String in + return "i=\(reference)" + }).joined(separator:"&") + + let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) + //let postData = "T=\(token)&output=json&i=1349530380539369".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in + switch result { + case .success(let (response, entryWrapper)): + guard let entryWrapper = entryWrapper else { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + return + } + + let dateInfo = HTTPDateInfo(urlResponse: response) + self.accountMetadata?.lastArticleFetch = dateInfo?.date + + + completion(.success((entryWrapper.entries))) + case .failure(let error): + completion(.failure(error)) + } + }) + + case .failure(let error): completion(.failure(error)) } - } - + } func retrieveEntries(feedID: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { @@ -459,30 +516,96 @@ final class GoogleReaderCompatibleAPICaller: NSObject { func retrieveEntries(completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?, Int?), Error>) -> Void) { + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + let since: Date = { - if let lastArticleFetch = accountMetadata?.lastArticleFetch { + if let lastArticleFetch = self.accountMetadata?.lastArticleFetch { return lastArticleFetch } else { return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() } }() - let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since) - var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("entries.json"), resolvingAgainstBaseURL: false)! - callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")] - let request = URLRequest(url: callComponents.url!, credentials: credentials) + let sinceString = since.timeIntervalSince1970 - transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "o", value: String(sinceString)), + URLQueryItem(name: "n", value: "10000"), + URLQueryItem(name: "output", value: "json"), + URLQueryItem(name: "xt", value: "user/-/state/com.google/read"), + URLQueryItem(name: "s", value: "user/-/state/com.google/reading-list") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + self.transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in switch result { - case .success(let (response, entries)): + case .success(let (_, entries)): - let dateInfo = HTTPDateInfo(urlResponse: response) - self.accountMetadata?.lastArticleFetch = dateInfo?.date + guard let entries = entries else { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + // Do POST asking for data about all the new articles + var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/contents"), credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + // Get ids from above into hex representation of value + let idsToFetch = entries.itemRefs.map({ (reference) -> String in + let idValue = Int(reference.itemId)! + let idHexString = String(idValue, radix: 16, uppercase: false) + return "i=\(idHexString)" + }).joined(separator:"&") + + let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) + //let postData = "T=\(token)&output=json&i=1349530380539369".data(using: String.Encoding.utf8) - let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) - let lastPageNumber = self.extractPageNumber(link: pagingInfo.lastPage) - completion(.success((entries, pagingInfo.nextPage, lastPageNumber))) + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in + switch result { + case .success(let (response, entryWrapper)): + guard let entryWrapper = entryWrapper else { + completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + return + } + + let dateInfo = HTTPDateInfo(urlResponse: response) + self.accountMetadata?.lastArticleFetch = dateInfo?.date + + + completion(.success((entryWrapper.entries, nil, nil))) + case .failure(let error): + completion(.failure(error)) + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } + + //completion(.success((entries, pagingInfo.nextPage, lastPageNumber))) case .failure(let error): self.accountMetadata?.lastArticleFetch = nil @@ -491,6 +614,13 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } + + + + + + + } func retrieveEntries(page: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { @@ -522,16 +652,46 @@ final class GoogleReaderCompatibleAPICaller: NSObject { func retrieveUnreadEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "s", value: "user/-/state/com.google/reading-list"), + URLQueryItem(name: "n", value: "10000"), + URLQueryItem(name: "xt", value: "user/-/state/com.google/read"), + URLQueryItem(name: "output", value: "json") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: [Int].self) { result in + transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in switch result { case .success(let (response, unreadEntries)): + + guard let itemRefs = unreadEntries?.itemRefs else { + completion(.success([])) + return + } + + let itemIds = itemRefs.map{ Int($0.itemId)! } + self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields) - completion(.success(unreadEntries)) + completion(.success(itemIds)) case .failure(let error): completion(.failure(error)) } @@ -541,17 +701,17 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) - transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") +// let request = URLRequest(url: callURL, credentials: credentials) +// let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) +// transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) } func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) - transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") +// let request = URLRequest(url: callURL, credentials: credentials) +// let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) +// transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) } func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index df677389d..8f192cd17 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -21,6 +21,7 @@ import os.log public enum GoogleReaderCompatibleAccountDelegateError: String, Error { case invalidParameter = "There was an invalid parameter passed." + case invalidResponse = "There was an invalid response from the server." } final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { @@ -98,14 +99,14 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { self.refreshArticles(account) { -// self.refreshArticleStatus(for: account) { -// self.refreshMissingArticles(account) { + self.refreshArticleStatus(for: account) { + self.refreshMissingArticles(account) { self.refreshProgress.clear() DispatchQueue.main.async { completion(.success(())) } -// } -// } + } + } } case .failure(let error): @@ -178,18 +179,18 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } - group.enter() - caller.retrieveStarredEntries() { result in - switch result { - case .success(let articleIDs): - self.syncArticleStarredState(account: account, articleIDs: articleIDs) - group.leave() - case .failure(let error): - os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) - group.leave() - } - - } +// group.enter() +// caller.retrieveStarredEntries() { result in +// switch result { +// case .success(let articleIDs): +// self.syncArticleStarredState(account: account, articleIDs: articleIDs) +// group.leave() +// case .failure(let error): +// os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) +// group.leave() +// } +// +// } group.notify(queue: DispatchQueue.main) { os_log(.debug, log: self.log, "Done refreshing article statuses.") @@ -970,7 +971,7 @@ private extension GoogleReaderCompatibleAccountDelegate { func processEntries(account: Account, entries: [GoogleReaderCompatibleEntry]?, completion: @escaping (() -> Void)) { - let parsedItems = mapEntriesToParsedItems(entries: entries) + let parsedItems = mapEntriesToParsedItems(account: account, entries: entries) let parsedMap = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ) let group = DispatchGroup() @@ -997,15 +998,17 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func mapEntriesToParsedItems(entries: [GoogleReaderCompatibleEntry]?) -> Set { + func mapEntriesToParsedItems(account: Account, entries: [GoogleReaderCompatibleEntry]?) -> Set { guard let entries = entries else { return Set() } let parsedItems: [ParsedItem] = entries.map { entry in - let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) - return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: String(entry.feedID), url: nil, externalURL: entry.url, title: entry.title, contentHTML: entry.contentHTML, contentText: nil, summary: entry.summary, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: authors, tags: nil, attachments: nil) + // let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) + // let feed = account.idToFeedDictionary[entry.origin.streamId!]! // TODO clean this up + + return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: entry.origin.streamId!, url: nil, externalURL: entry.alternates.first?.url, title: entry.title, contentHTML: entry.summary.content, contentText: nil, summary: entry.summary.content, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: nil, tags: nil, attachments: nil) } return Set(parsedItems) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift index 24fabd9dc..c98212774 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift @@ -10,58 +10,103 @@ import Foundation import RSParser import RSCore +struct GoogleReaderCompatibleEntryWrapper: Codable { + let id: String + let updated: Int + let entries: [GoogleReaderCompatibleEntry] + + + enum CodingKeys: String, CodingKey { + case id = "id" + case updated = "updated" + case entries = "items" + } +} + +/* { +"id": "tag:google.com,2005:reader/item/00058a3b5197197b", +"crawlTimeMsec": "1559362260113", +"timestampUsec": "1559362260113787", +"published": 1554845280, +"title": "", +"summary": { +"content": "\n

Found an old screenshot of NetNewsWire 1.0 for iPhone!

\n\n

\"Netnewswire

\n" +}, +"alternate": [ +{ +"href": "https://nnw.ranchero.com/2019/04/09/found-an-old.html" +} +], +"categories": [ +"user/-/state/com.google/reading-list", +"user/-/label/Uncategorized" +], +"origin": { +"streamId": "feed/130", +"title": "NetNewsWire" +} +} +*/ struct GoogleReaderCompatibleEntry: Codable { - let articleID: Int - let feedID: Int + let articleID: String let title: String? - let url: String? - let authorName: String? - let contentHTML: String? - let summary: String? - let datePublished: String? - let dateArrived: String? - let jsonFeed: GoogleReaderCompatibleEntryJSONFeed? + + let publishedTimestamp: Double? + let crawledTimestamp: String? + let timestampUsec: String? + + let summary: GoogleReaderCompatibleArticleSummary + let alternates: [GoogleReaderCompatibleAlternateLocation] + let categories: [String] + let origin: GoogleReaderCompatibleEntryOrigin enum CodingKeys: String, CodingKey { case articleID = "id" - case feedID = "feed_id" case title = "title" - case url = "url" - case authorName = "author" - case contentHTML = "content" case summary = "summary" - case datePublished = "published" - case dateArrived = "created_at" - case jsonFeed = "json_feed" - } - - // GoogleReaderCompatible dates can't be decoded by the JSONDecoding 8601 decoding strategy. GoogleReaderCompatible - // 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. - func parseDatePublished() -> Date? { - if datePublished != nil { - return GoogleReaderCompatibleDate.formatter.date(from: datePublished!) - } else { - return nil - } + case alternates = "alternate" + case categories = "categories" + case publishedTimestamp = "published" + case crawledTimestamp = "crawlTimeMsec" + case origin = "origin" + case timestampUsec = "timestampUsec" } -} - -struct GoogleReaderCompatibleEntryJSONFeed: Codable { - let jsonFeedAuthor: GoogleReaderCompatibleEntryJSONFeedAuthor? - enum CodingKeys: String, CodingKey { - case jsonFeedAuthor = "author" + func parseDatePublished() -> Date? { + + guard let unixTime = publishedTimestamp else { + return nil + } + + return Date(timeIntervalSince1970: unixTime) } } -struct GoogleReaderCompatibleEntryJSONFeedAuthor: Codable { +struct GoogleReaderCompatibleArticleSummary: Codable { + let content: String? + + enum CodingKeys: String, CodingKey { + case content = "content" + } +} + +struct GoogleReaderCompatibleAlternateLocation: Codable { let url: String? - let avatarURL: String? + enum CodingKeys: String, CodingKey { - case url = "url" - case avatarURL = "avatar" + case url = "href" } } + + +struct GoogleReaderCompatibleEntryOrigin: Codable { + let streamId: String? + let title: String? + + enum CodingKeys: String, CodingKey { + case streamId = "streamId" + case title = "title" + } +} + diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift index a5ce66ae0..b08073bfc 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift @@ -8,12 +8,20 @@ import Foundation -struct GoogleReaderCompatibleUnreadEntry: Codable { - - let unreadEntries: [Int] +struct GoogleReaderCompatibleReferenceWrapper: Codable { + let itemRefs: [GoogleReaderCompatibleReference] enum CodingKeys: String, CodingKey { - case unreadEntries = "unread_entries" + case itemRefs = "itemRefs" + } +} + +struct GoogleReaderCompatibleReference: Codable { + + let itemId: String + + enum CodingKeys: String, CodingKey { + case itemId = "id" } } diff --git a/submodules/RSWeb b/submodules/RSWeb index cf3a30eb3..142cb8ccc 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit cf3a30eb3833d9dd423fed003393e6e3c1a360d4 +Subproject commit 142cb8ccc491201e3de35c0b5d76d23d785f1978 From dc60ebf1f82e5ec56bb8bcd8cfa84a54aed63031 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 11 Jun 2019 15:37:21 -0400 Subject: [PATCH 12/31] Unread status is properly set when articles are downloaded from backend --- .../GoogleReaderCompatibleAPICaller.swift | 45 +++++++++++++------ ...oogleReaderCompatibleAccountDelegate.swift | 31 ++++++------- .../GoogleReaderCompatibleEntry.swift | 16 +++++++ 3 files changed, 61 insertions(+), 31 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index d1bc13c56..e3eb36e98 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -579,7 +579,6 @@ final class GoogleReaderCompatibleAPICaller: NSObject { }).joined(separator:"&") let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) - //let postData = "T=\(token)&output=json&i=1349530380539369".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in switch result { @@ -613,14 +612,6 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } } - - - - - - - - } func retrieveEntries(page: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { @@ -715,17 +706,45 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "s", value: "user/-/state/com.google/starred"), + URLQueryItem(name: "n", value: "10000"), + URLQueryItem(name: "output", value: "json") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: [Int].self) { result in + transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in switch result { - case .success(let (response, starredEntries)): + case .success(let (response, unreadEntries)): + + guard let itemRefs = unreadEntries?.itemRefs else { + completion(.success([])) + return + } + + let itemIds = itemRefs.map{ Int($0.itemId)! } + self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields) - completion(.success(starredEntries)) + completion(.success(itemIds)) case .failure(let error): completion(.failure(error)) } diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 8f192cd17..d9dcf46b3 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -93,11 +93,6 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { refreshAccount(account) { result in switch result { case .success(): - DispatchQueue.main.async { - completion(.success(())) - } - - self.refreshArticles(account) { self.refreshArticleStatus(for: account) { self.refreshMissingArticles(account) { @@ -179,18 +174,18 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } -// group.enter() -// caller.retrieveStarredEntries() { result in -// switch result { -// case .success(let articleIDs): -// self.syncArticleStarredState(account: account, articleIDs: articleIDs) -// group.leave() -// case .failure(let error): -// os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) -// group.leave() -// } -// -// } + group.enter() + caller.retrieveStarredEntries() { result in + switch result { + case .success(let articleIDs): + self.syncArticleStarredState(account: account, articleIDs: articleIDs) + group.leave() + case .failure(let error): + os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription) + group.leave() + } + + } group.notify(queue: DispatchQueue.main) { os_log(.debug, log: self.log, "Done refreshing article statuses.") @@ -1008,7 +1003,7 @@ private extension GoogleReaderCompatibleAccountDelegate { // let authors = Set([ParsedAuthor(name: entry.authorName, url: entry.jsonFeed?.jsonFeedAuthor?.url, avatarURL: entry.jsonFeed?.jsonFeedAuthor?.avatarURL, emailAddress: nil)]) // let feed = account.idToFeedDictionary[entry.origin.streamId!]! // TODO clean this up - return ParsedItem(syncServiceID: String(entry.articleID), uniqueID: String(entry.articleID), feedURL: entry.origin.streamId!, url: nil, externalURL: entry.alternates.first?.url, title: entry.title, contentHTML: entry.summary.content, contentText: nil, summary: entry.summary.content, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: nil, tags: nil, attachments: nil) + return ParsedItem(syncServiceID: entry.uniqueID(), uniqueID: entry.uniqueID(), feedURL: entry.origin.streamId!, url: nil, externalURL: entry.alternates.first?.url, title: entry.title, contentHTML: entry.summary.content, contentText: nil, summary: entry.summary.content, imageURL: nil, bannerImageURL: nil, datePublished: entry.parseDatePublished(), dateModified: nil, authors: nil, tags: nil, attachments: nil) } return Set(parsedItems) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift index c98212774..8757262ee 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift @@ -81,6 +81,22 @@ struct GoogleReaderCompatibleEntry: Codable { return Date(timeIntervalSince1970: unixTime) } + + func uniqueID() -> String { + // Should look something like "tag:google.com,2005:reader/item/00058b10ce338909" + // REGEX feels heavy, I should be able to just split on / and take the last element + + guard let idPart = articleID.components(separatedBy: "/").last else { + return articleID + } + + // Convert hex representation back to integer and then a string representation + guard let idNumber = Int(idPart, radix: 16) else { + return articleID + } + + return String(idNumber, radix: 10, uppercase: false) + } } struct GoogleReaderCompatibleArticleSummary: Codable { From 69c947bd65f7eb247944bab66adac50f1dcef2a1 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 11 Jun 2019 16:42:28 -0400 Subject: [PATCH 13/31] Mark read/unread structure. --- .../GoogleReaderCompatibleAPICaller.swift | 79 +++++++++++++++++-- ...oogleReaderCompatibleAccountDelegate.swift | 1 + 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index e3eb36e98..3b31d423a 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -692,17 +692,80 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") -// let request = URLRequest(url: callURL, credentials: credentials) -// let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) -// transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + // Do POST asking for data about all the new articles + var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/edit-tag"), credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + // Get ids from above into hex representation of value + let idsToFetch = entries.map({ (idValue) -> String in + let idHexString = String(format: "%.16x", idValue) + return "i=\(idHexString)" + }).joined(separator:"&") + + let postData = "T=\(token)&\(idsToFetch)&a=user/-/state/com.google/read".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } + } func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("unread_entries.json") -// let request = URLRequest(url: callURL, credentials: credentials) -// let payload = GoogleReaderCompatibleUnreadEntry(unreadEntries: entries) -// transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + // Do POST asking for data about all the new articles + var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/edit-tag"), credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + // Get ids from above into hex representation of value + let idsToFetch = entries.map({ (idValue) -> String in + let idHexString = String(format: "%.16x", idValue) + return "i=\(idHexString)" + }).joined(separator:"&") + + let postData = "T=\(token)&\(idsToFetch)&r=user/-/state/com.google/read".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + case .failure(let error): + completion(.failure(error)) + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index d9dcf46b3..3127a67d5 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -453,6 +453,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { return account.update(articles, statusKey: statusKey, flag: flag) } + func accountDidInitialize(_ account: Account) { accountMetadata = account.metadata credentials = try? account.retrieveGoogleAuthCredentials() From 45c15c96aa5ad44a4f00b1e58b8757d27ceeefc2 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Wed, 12 Jun 2019 16:41:44 -0400 Subject: [PATCH 14/31] Marking read/unread functioning. --- .../GoogleReaderCompatibleAPICaller.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 3b31d423a..cd67f2939 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -707,11 +707,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { // Get ids from above into hex representation of value let idsToFetch = entries.map({ (idValue) -> String in - let idHexString = String(format: "%.16x", idValue) + let idHexString = String(format: "%.16llx", idValue) return "i=\(idHexString)" }).joined(separator:"&") - let postData = "T=\(token)&\(idsToFetch)&a=user/-/state/com.google/read".data(using: String.Encoding.utf8) + let postData = "T=\(token)&\(idsToFetch)&r=user/-/state/com.google/read".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { @@ -746,11 +746,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { // Get ids from above into hex representation of value let idsToFetch = entries.map({ (idValue) -> String in - let idHexString = String(format: "%.16x", idValue) + let idHexString = String(format: "%.16llx", idValue) return "i=\(idHexString)" }).joined(separator:"&") - let postData = "T=\(token)&\(idsToFetch)&r=user/-/state/com.google/read".data(using: String.Encoding.utf8) + let postData = "T=\(token)&\(idsToFetch)&a=user/-/state/com.google/read".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { From dac166cf5852a1f2e7e774e0966cb31ce118735e Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Thu, 13 Jun 2019 09:35:01 -0400 Subject: [PATCH 15/31] Refactor status change calls. --- .../GoogleReaderCompatibleAPICaller.swift | 76 ++++++------------- 1 file changed, 24 insertions(+), 52 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index cd67f2939..c3c270d17 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -31,6 +31,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { static let starredEntries = "starredEntries" } + enum GoogleReaderState: String { + case read = "user/-/state/com.google/read" + case starred = "user/-/state/com.google/starred" + } + private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! private var transport: Transport! @@ -604,8 +609,6 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } } - //completion(.success((entries, pagingInfo.nextPage, lastPageNumber))) - case .failure(let error): self.accountMetadata?.lastArticleFetch = nil completion(.failure(error)) @@ -691,7 +694,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + func updateStateToEntries(entries: [Int], state: GoogleReaderState, add: Bool, completion: @escaping (Result) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return @@ -711,7 +714,9 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return "i=\(idHexString)" }).joined(separator:"&") - let postData = "T=\(token)&\(idsToFetch)&r=user/-/state/com.google/read".data(using: String.Encoding.utf8) + let actionIndicator = add ? "a" : "r" + + let postData = "T=\(token)&\(idsToFetch)&\(actionIndicator)=\(state)".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { @@ -727,45 +732,24 @@ final class GoogleReaderCompatibleAPICaller: NSObject { completion(.failure(error)) } } - + } + + func createUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .read, add: false, completion: completion) } func deleteUnreadEntries(entries: [Int], completion: @escaping (Result) -> Void) { - guard let baseURL = APIBaseURL else { - completion(.failure(CredentialsError.incompleteCredentials)) - return - } + updateStateToEntries(entries: entries, state: .read, add: true, completion: completion) + + } + + func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .starred, add: true, completion: completion) - self.requestAuthorizationToken(endpoint: baseURL) { (result) in - switch result { - case .success(let token): - // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/edit-tag"), credentials: self.credentials) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - - // Get ids from above into hex representation of value - let idsToFetch = entries.map({ (idValue) -> String in - let idHexString = String(format: "%.16llx", idValue) - return "i=\(idHexString)" - }).joined(separator:"&") - - let postData = "T=\(token)&\(idsToFetch)&a=user/-/state/com.google/read".data(using: String.Encoding.utf8) - - self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in - switch result { - case .success: - completion(.success(())) - case .failure(let error): - completion(.failure(error)) - } - }) - - - case .failure(let error): - completion(.failure(error)) - } - } + } + + func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .starred, add: false, completion: completion) } func retrieveStarredEntries(completion: @escaping (Result<[Int]?, Error>) -> Void) { @@ -816,19 +800,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func createStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries) - transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) - } - - func deleteStarredEntries(entries: [Int], completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("starred_entries.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleStarredEntry(starredEntries: entries) - transport.send(request: request, method: HTTPMethod.delete, payload: payload, completion: completion) - } + } From 2128afd4650189c81320b038556c005075c6b8d5 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Thu, 13 Jun 2019 12:44:20 -0400 Subject: [PATCH 16/31] Refactor endpoints into an enum --- .../GoogleReaderCompatibleAPICaller.swift | 31 ++++++++++++------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index c3c270d17..a0ff1d2bc 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -36,6 +36,16 @@ final class GoogleReaderCompatibleAPICaller: NSObject { case starred = "user/-/state/com.google/starred" } + enum GoogleReaderEndpoints: String { + case login = "/accounts/ClientLogin" + case token = "/reader/api/0/token" + case tagList = "/reader/api/0/tag/list" + case subscriptionList = "/reader/api/0/subscription/list" + case contents = "/reader/api/0/stream/items/contents" + case itemIds = "/reader/api/0/stream/items/ids" + case editTag = "/reader/api/0/edit-tag" + } + private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! private var transport: Transport! @@ -75,7 +85,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let request = URLRequest(url: endpoint.appendingPathComponent("/accounts/ClientLogin"), credentials: credentials) + let request = URLRequest(url: endpoint.appendingPathComponent(GoogleReaderEndpoints.login.rawValue), credentials: credentials) transport.send(request: request) { result in switch result { @@ -119,7 +129,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let request = URLRequest(url: endpoint.appendingPathComponent("/reader/api/0/token"), credentials: credentials) + let request = URLRequest(url: endpoint.appendingPathComponent(GoogleReaderEndpoints.token.rawValue), credentials: credentials) transport.send(request: request) { result in switch result { @@ -199,7 +209,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/tag/list"), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.tagList.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -263,7 +273,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/subscription/list"), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionList.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -456,7 +466,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/contents"), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.contents.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -537,7 +547,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let sinceString = since.timeIntervalSince1970 // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -572,7 +582,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/contents"), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.contents.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -652,7 +662,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -704,7 +714,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent("/reader/api/0/edit-tag"), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.editTag.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -758,8 +768,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent("/reader/api/0/stream/items/ids"), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } From 5490dac86d2f48207e6eb260bf132bec8c84a93a Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 08:21:13 -0400 Subject: [PATCH 17/31] Need to use rawValue to properly mark starred/read --- .../GoogleReaderCompatibleAPICaller.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index a0ff1d2bc..44e434bd4 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -726,7 +726,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let actionIndicator = add ? "a" : "r" - let postData = "T=\(token)&\(idsToFetch)&\(actionIndicator)=\(state)".data(using: String.Encoding.utf8) + let postData = "T=\(token)&\(idsToFetch)&\(actionIndicator)=\(state.rawValue)".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in switch result { From e5eb8df3339ebd97034024546b26d674d78233f2 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 12:27:21 -0400 Subject: [PATCH 18/31] Add feed working on backend, thread crash inside account code. --- .../GoogleReaderCompatibleAPICaller.swift | 133 ++++++++++-------- ...oogleReaderCompatibleAccountDelegate.swift | 2 - .../GoogleReaderCompatibleSubscription.swift | 22 ++- 3 files changed, 93 insertions(+), 64 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 44e434bd4..b8df98f3d 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -15,7 +15,7 @@ import RSWeb enum CreateGoogleReaderSubscriptionResult { case created(GoogleReaderCompatibleSubscription) - case multipleChoice([GoogleReaderCompatibleSubscriptionChoice]) + //case multipleChoice([GoogleReaderCompatibleSubscriptionChoice]) case alreadySubscribed case notFound } @@ -41,6 +41,8 @@ final class GoogleReaderCompatibleAPICaller: NSObject { case token = "/reader/api/0/token" 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" @@ -305,75 +307,84 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func createSubscription(url: String, completion: @escaping (Result) -> Void) { - - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions.json") - var request = URLRequest(url: callURL, credentials: credentials) - request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) - - let payload: Data - do { - payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateSubscription(feedURL: url)) - } catch { - completion(.failure(error)) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) return } - transport.send(request: request, method: HTTPMethod.post, payload: payload) { result in - + self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { - case .success(let (response, data)): - - switch response.forcedStatusCode { - case 201: - guard let subData = data else { - completion(.failure(TransportError.noData)) - break - } - do { - let subscription = try JSONDecoder().decode(GoogleReaderCompatibleSubscription.self, from: subData) - completion(.success(.created(subscription))) - } catch { - completion(.failure(error)) - } - case 300: - guard let subData = data else { - completion(.failure(TransportError.noData)) - break - } - do { - let subscriptions = try JSONDecoder().decode([GoogleReaderCompatibleSubscriptionChoice].self, from: subData) - completion(.success(.multipleChoice(subscriptions))) - } catch { - completion(.failure(error)) - } - case 302: - completion(.success(.alreadySubscribed)) - default: - completion(.failure(TransportError.httpError(status: response.forcedStatusCode))) + case .success(let token): + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return } + components.queryItems = [ + URLQueryItem(name: "quickadd", value: url) + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + + 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: GoogleReaderCompatibleQuickAddResult.self, completion: { (result) in + switch result { + case .success(let (_, subResult)): + + switch subResult?.numResults { + case 0: + completion(.success(.alreadySubscribed)) + default: + // We have a feed ID but need to get feed information + guard let streamId = subResult?.streamId else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + // 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 + } + + let newStreamId = "feed/\(streamId)" + + guard let subscription = subscriptions.first(where: { (sub) -> Bool in + sub.feedID == newStreamId + }) else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + completion(.success(.created(subscription))) + + case .failure(let error): + completion(.failure(error)) + } + }) + } + case .failure(let error): + completion(.failure(error)) + } + }) + + case .failure(let error): - - switch error { - case TransportError.httpError(let status): - switch status { - case 401: - // I don't know why we get 401's here. This looks like a GoogleReaderCompatible bug, but it only happens - // when you are already subscribed to the feed. - completion(.success(.alreadySubscribed)) - case 404: - completion(.success(.notFound)) - default: - completion(.failure(error)) - } - default: - completion(.failure(error)) - } - + completion(.failure(error)) } - } - } func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 3127a67d5..894de778e 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -302,8 +302,6 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { switch subResult { case .created(let subscription): self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion) - case .multipleChoice(let choices): - self.decideBestFeedChoice(account: account, url: url, name: name, container: container, choices: choices, completion: completion) case .alreadySubscribed: DispatchQueue.main.async { completion(.failure(AccountError.createErrorAlreadySubscribed)) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift index 7c0c56827..9736ef93d 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift @@ -10,6 +10,27 @@ import Foundation import RSCore import RSParser +/* + + { + "numResults":0, + "error": "Already subscribed! https://inessential.com/xml/rss.xml + } + +*/ + +struct GoogleReaderCompatibleQuickAddResult: Codable { + let numResults: Int + let error: String? + let streamId: String? + + enum CodingKeys: String, CodingKey { + case numResults = "numResults" + case error = "error" + case streamId = "streamId" + } +} + struct GoogleReaderCompatibleSubscriptionContainer: Codable { let subscriptions: [GoogleReaderCompatibleSubscription] @@ -35,7 +56,6 @@ struct GoogleReaderCompatibleSubscriptionContainer: Codable { */ struct GoogleReaderCompatibleSubscription: Codable { - let feedID: String let name: String? let categories: [GoogleReaderCompatibleCategory] From baf14f7379a6ce397c4f38f7b51ae61fa451e519 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 12:35:04 -0400 Subject: [PATCH 19/31] submodules --- submodules/RSCore | 2 +- submodules/RSDatabase | 2 +- submodules/RSParser | 2 +- submodules/RSTree | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/submodules/RSCore b/submodules/RSCore index aa7107080..97dc785d1 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit aa7107080e90d5be11ae54fd41ee4dd192468e30 +Subproject commit 97dc785d171c5ffa151d7df73135641da8f5bc17 diff --git a/submodules/RSDatabase b/submodules/RSDatabase index 7b4476830..c38a779de 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit 7b44768308dc6970ee78470d0ea1e5287badc2bc +Subproject commit c38a779de5f935c4d041e89066a0d15d490f3776 diff --git a/submodules/RSParser b/submodules/RSParser index 032edf89b..93b481897 160000 --- a/submodules/RSParser +++ b/submodules/RSParser @@ -1 +1 @@ -Subproject commit 032edf89b64ccbbfb6c05887b239a4bf81329b92 +Subproject commit 93b481897d84849345daa965bd8e11860c9422e7 diff --git a/submodules/RSTree b/submodules/RSTree index 350762104..b6cd62f04 160000 --- a/submodules/RSTree +++ b/submodules/RSTree @@ -1 +1 @@ -Subproject commit 350762104423aef963ca945d4388ca9d47f991ce +Subproject commit b6cd62f04c90922dbc58f2907a8db6c33b96b50e From 029bcbda96c635a70a69c043704c02cc40eb6581 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 13:30:12 -0400 Subject: [PATCH 20/31] Merge cleanup. --- Frameworks/Account/Account.swift | 1 - submodules/RSCore | 2 +- submodules/RSDatabase | 2 +- submodules/RSParser | 2 +- submodules/RSTree | 2 +- submodules/RSWeb | 2 +- 6 files changed, 5 insertions(+), 6 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 0c4e08e3f..fa1adca3c 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -278,7 +278,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, switch credentials { case .basic(let username, _): - default: self.username = username case .googleBasicLogin(let username, _): self.username = username diff --git a/submodules/RSCore b/submodules/RSCore index 97dc785d1..111690033 160000 --- a/submodules/RSCore +++ b/submodules/RSCore @@ -1 +1 @@ -Subproject commit 97dc785d171c5ffa151d7df73135641da8f5bc17 +Subproject commit 111690033354afc1cf57e37a326c344a0fe93b77 diff --git a/submodules/RSDatabase b/submodules/RSDatabase index c38a779de..7b4476830 160000 --- a/submodules/RSDatabase +++ b/submodules/RSDatabase @@ -1 +1 @@ -Subproject commit c38a779de5f935c4d041e89066a0d15d490f3776 +Subproject commit 7b44768308dc6970ee78470d0ea1e5287badc2bc diff --git a/submodules/RSParser b/submodules/RSParser index 93b481897..f2be15379 160000 --- a/submodules/RSParser +++ b/submodules/RSParser @@ -1 +1 @@ -Subproject commit 93b481897d84849345daa965bd8e11860c9422e7 +Subproject commit f2be15379d64e2f660735219bcbd77f7a759b057 diff --git a/submodules/RSTree b/submodules/RSTree index b6cd62f04..350762104 160000 --- a/submodules/RSTree +++ b/submodules/RSTree @@ -1 +1 @@ -Subproject commit b6cd62f04c90922dbc58f2907a8db6c33b96b50e +Subproject commit 350762104423aef963ca945d4388ca9d47f991ce diff --git a/submodules/RSWeb b/submodules/RSWeb index 142cb8ccc..59685e506 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 142cb8ccc491201e3de35c0b5d76d23d785f1978 +Subproject commit 59685e50640cd4629294bf2c0d63193ffa4ccc74 From b2bc941d2d7a2e59f5758e08953cc0460bb0009b Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 13:40:12 -0400 Subject: [PATCH 21/31] Move back to official submodule --- .gitmodules | 3 +-- submodules/RSWeb | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index 1da2a5f2b..5b7b8b710 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,8 +3,7 @@ url = https://github.com/brentsimmons/RSCore [submodule "submodules/RSWeb"] path = submodules/RSWeb - url = https://github.com/jbeker/RSWeb - branch = google_reader_compatible_syncing + url = https://github.com/brentsimmons/RSWeb [submodule "submodules/RSParser"] path = submodules/RSParser url = https://github.com/brentsimmons/RSParser diff --git a/submodules/RSWeb b/submodules/RSWeb index 59685e506..5d648e405 160000 --- a/submodules/RSWeb +++ b/submodules/RSWeb @@ -1 +1 @@ -Subproject commit 59685e50640cd4629294bf2c0d63193ffa4ccc74 +Subproject commit 5d648e4050b700bb20fc7ae3303f087edcb3228f From 62afd312f0ef58431667b62d9aa0715e1d7b3f14 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 15:46:13 -0400 Subject: [PATCH 22/31] Subscribing and loading initial articles functioning. --- .../GoogleReaderCompatibleAPICaller.swift | 58 ++++++++++++++----- 1 file changed, 45 insertions(+), 13 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index b8df98f3d..8b2c4768f 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -491,16 +491,12 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in switch result { - case .success(let (response, entryWrapper)): + case .success(let (_, entryWrapper)): guard let entryWrapper = entryWrapper else { completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) return } - let dateInfo = HTTPDateInfo(urlResponse: response) - self.accountMetadata?.lastArticleFetch = dateInfo?.date - - completion(.success((entryWrapper.entries))) case .failure(let error): completion(.failure(error)) @@ -518,19 +514,55 @@ final class GoogleReaderCompatibleAPICaller: NSObject { func retrieveEntries(feedID: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() - let sinceString = GoogleReaderCompatibleDate.formatter.string(from: since) - var callComponents = URLComponents(url: GoogleReaderCompatibleBaseURL.appendingPathComponent("feeds/\(feedID)/entries.json"), resolvingAgainstBaseURL: false)! - callComponents.queryItems = [URLQueryItem(name: "since", value: sinceString), URLQueryItem(name: "per_page", value: "100"), URLQueryItem(name: "mode", value: "extended")] - let request = URLRequest(url: callComponents.url!, credentials: credentials) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } - transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + // Add query string for getting JSON (probably should break this out as I will be doing it a lot) + guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "s", value: feedID), + URLQueryItem(name: "ot", value: String(since.timeIntervalSince1970)), + URLQueryItem(name: "output", value: "json") + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + + let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: nil) + + transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in switch result { - case .success(let (response, entries)): + case .success(let (_, unreadEntries)): - let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) - completion(.success((entries, pagingInfo.nextPage))) + guard let itemRefs = unreadEntries?.itemRefs else { + completion(.success(([], nil))) + return + } + + let itemIds = itemRefs.map { (reference) -> String in + // Convert the IDs to the (stupid) Google Hex Format + let idValue = Int(reference.itemId)! + return String(idValue, radix: 16, uppercase: false) + } + + self.retrieveEntries(articleIDs: itemIds) { (results) in + switch results { + case .success(let entries): + completion(.success((entries,nil))) + case .failure(let error): + completion(.failure(error)) + } + } case .failure(let error): completion(.failure(error)) From 41af536212cfba4ef833ae15306f50c0eb9e4fae Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sat, 15 Jun 2019 16:13:08 -0400 Subject: [PATCH 23/31] Rename feeds --- .../GoogleReaderCompatibleAPICaller.swift | 285 +++++++++++------- 1 file changed, 171 insertions(+), 114 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 8b2c4768f..31f9b42d1 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -48,7 +48,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { case editTag = "/reader/api/0/edit-tag" } - private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! + // private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! private var transport: Transport! var credentials: Credentials? @@ -157,50 +157,50 @@ final class GoogleReaderCompatibleAPICaller: NSObject { func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json") - var request = URLRequest(url: callURL, credentials: credentials) - request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) - - transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in - - switch result { - case .success(let (_, data)): - - guard let resultData = data else { - completion(.failure(TransportError.noData)) - break - } - - do { - let result = try JSONDecoder().decode(GoogleReaderCompatibleImportResult.self, from: resultData) - completion(.success(result)) - } catch { - completion(.failure(error)) - } - - case .failure(let error): - completion(.failure(error)) - } - - } +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json") +// var request = URLRequest(url: callURL, credentials: credentials) +// request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) +// +// transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in +// +// switch result { +// case .success(let (_, data)): +// +// guard let resultData = data else { +// completion(.failure(TransportError.noData)) +// break +// } +// +// do { +// let result = try JSONDecoder().decode(GoogleReaderCompatibleImportResult.self, from: resultData) +// completion(.success(result)) +// } catch { +// completion(.failure(error)) +// } +// +// case .failure(let error): +// completion(.failure(error)) +// } +// +// } } func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports/\(importID).json") - let request = URLRequest(url: callURL, credentials: credentials) - - transport.send(request: request, resultType: GoogleReaderCompatibleImportResult.self) { result in - - switch result { - case .success(let (_, importResult)): - completion(.success(importResult)) - case .failure(let error): - completion(.failure(error)) - } - - } +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports/\(importID).json") +// let request = URLRequest(url: callURL, credentials: credentials) +// +// transport.send(request: request, resultType: GoogleReaderCompatibleImportResult.self) { result in +// +// switch result { +// case .success(let (_, importResult)): +// completion(.success(importResult)) +// case .failure(let error): +// completion(.failure(error)) +// } +// +// } } @@ -243,28 +243,28 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleRenameTag(oldName: oldName, newName: newName) - transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") +// let request = URLRequest(url: callURL, credentials: credentials) +// let payload = GoogleReaderCompatibleRenameTag(oldName: oldName, newName: newName) +// transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) } func deleteTag(name: String, completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleDeleteTag(name: name) - - transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [GoogleReaderCompatibleTagging].self) { result in - - switch result { - case .success(let (_, taggings)): - completion(.success(taggings)) - case .failure(let error): - completion(.failure(error)) - } - - } +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") +// let request = URLRequest(url: callURL, credentials: credentials) +// let payload = GoogleReaderCompatibleDeleteTag(name: name) +// +// transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [GoogleReaderCompatibleTagging].self) { result in +// +// switch result { +// case .success(let (_, taggings)): +// completion(.success(taggings)) +// case .failure(let error): +// completion(.failure(error)) +// } +// +// } } @@ -388,77 +388,134 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID)/update.json") - let request = URLRequest(url: callURL, credentials: credentials) - let payload = GoogleReaderCompatibleUpdateSubscription(title: newName) - transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + + + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&t=\(newName)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + break + case .failure(let error): + completion(.failure(error)) + break + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } func deleteSubscription(subscriptionID: String, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("subscriptions/\(subscriptionID).json") - let request = URLRequest(url: callURL, credentials: credentials) - transport.send(request: request, method: HTTPMethod.delete, completion: completion) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + + + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let postData = "T=\(token)&s=\(subscriptionID)&ac=unsubscribe".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + break + case .failure(let error): + completion(.failure(error)) + break + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") - var request = URLRequest(url: callURL, credentials: credentials) - request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) - - let payload: Data - do { - payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateTagging(feedID: feedID, name: name)) - } catch { - completion(.failure(error)) - return - } - - transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in - - switch result { - case .success(let (response, _)): - if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location), - let lowerBound = taggingLocation.range(of: "v2/taggings/")?.upperBound, - let upperBound = taggingLocation.range(of: ".json")?.lowerBound, - let taggingID = Int(taggingLocation[lowerBound..) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings/\(taggingID).json") - var request = URLRequest(url: callURL, credentials: credentials) - request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) - transport.send(request: request, method: HTTPMethod.delete, completion: completion) +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings/\(taggingID).json") +// var request = URLRequest(url: callURL, credentials: credentials) +// request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) +// transport.send(request: request, method: HTTPMethod.delete, completion: completion) } func retrieveIcons(completion: @escaping (Result<[GoogleReaderCompatibleIcon]?, Error>) -> Void) { - let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("icons.json") - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons] - let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - - transport.send(request: request, resultType: [GoogleReaderCompatibleIcon].self) { result in - - switch result { - case .success(let (response, icons)): - self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields) - completion(.success(icons)) - case .failure(let error): - completion(.failure(error)) - } - - } - +// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("icons.json") +// let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons] +// let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) +// +// transport.send(request: request, resultType: [GoogleReaderCompatibleIcon].self) { result in +// +// switch result { +// case .success(let (response, icons)): +// self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields) +// completion(.success(icons)) +// case .failure(let error): +// completion(.failure(error)) +// } +// +// } +// } func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([GoogleReaderCompatibleEntry]?), Error>) -> Void) { From 17439c8c5e94ac1a7859f85a92def7d16829eca3 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sun, 16 Jun 2019 13:14:23 -0400 Subject: [PATCH 24/31] Disbale and rename tags functional. --- .../GoogleReaderCompatibleAPICaller.swift | 211 +++++++++++------- ...oogleReaderCompatibleAccountDelegate.swift | 35 +-- 2 files changed, 155 insertions(+), 91 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 31f9b42d1..83c6158f5 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -15,7 +15,6 @@ import RSWeb enum CreateGoogleReaderSubscriptionResult { case created(GoogleReaderCompatibleSubscription) - //case multipleChoice([GoogleReaderCompatibleSubscriptionChoice]) case alreadySubscribed case notFound } @@ -39,6 +38,8 @@ final class GoogleReaderCompatibleAPICaller: NSObject { enum GoogleReaderEndpoints: String { case login = "/accounts/ClientLogin" case token = "/reader/api/0/token" + case disableTag = "/reader/api/0/disable-tag" + case renameTag = "/reader/api/0/rename-tag" case tagList = "/reader/api/0/tag/list" case subscriptionList = "/reader/api/0/subscription/list" case subscriptionEdit = "/reader/api/0/subscription/edit" @@ -243,28 +244,76 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func renameTag(oldName: String, newName: String, completion: @escaping (Result) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") -// let request = URLRequest(url: callURL, credentials: credentials) -// let payload = GoogleReaderCompatibleRenameTag(oldName: oldName, newName: newName) -// transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion) + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.renameTag.rawValue), credentials: self.credentials) + + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let oldTagName = "user/-/label/\(oldName)" + let newTagName = "user/-/label/\(newName)" + let postData = "T=\(token)&s=\(oldTagName)&dest=\(newTagName)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + break + case .failure(let error): + completion(.failure(error)) + break + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } - func deleteTag(name: String, completion: @escaping (Result<[GoogleReaderCompatibleTagging]?, Error>) -> Void) { + func deleteTag(name: String, completion: @escaping (Result) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("tags.json") -// let request = URLRequest(url: callURL, credentials: credentials) -// let payload = GoogleReaderCompatibleDeleteTag(name: name) -// -// transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [GoogleReaderCompatibleTagging].self) { result in -// -// switch result { -// case .success(let (_, taggings)): -// completion(.success(taggings)) -// case .failure(let error): -// completion(.failure(error)) -// } -// -// } + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } + + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.disableTag.rawValue), credentials: self.credentials) + + + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let tagName = "user/-/label/\(name)" + let postData = "T=\(token)&s=\(tagName)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + break + case .failure(let error): + completion(.failure(error)) + break + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } @@ -457,65 +506,78 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } } - func createTagging(feedID: Int, name: String, completion: @escaping (Result) -> Void) { + func createTagging(subscriptionID: String, tagName: String, completion: @escaping (Result) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings.json") -// var request = URLRequest(url: callURL, credentials: credentials) -// request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) -// -// let payload: Data -// do { -// payload = try JSONEncoder().encode(GoogleReaderCompatibleCreateTagging(feedID: feedID, name: name)) -// } catch { -// completion(.failure(error)) -// return -// } -// -// transport.send(request: request, method: HTTPMethod.post, payload:payload) { result in -// -// switch result { -// case .success(let (response, _)): -// if let taggingLocation = response.valueForHTTPHeaderField(HTTPResponseHeader.location), -// let lowerBound = taggingLocation.range(of: "v2/taggings/")?.upperBound, -// let upperBound = taggingLocation.range(of: ".json")?.lowerBound, -// let taggingID = Int(taggingLocation[lowerBound..) -> Void) { -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("taggings/\(taggingID).json") -// var request = URLRequest(url: callURL, credentials: credentials) -// request.addValue("application/json; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) -// transport.send(request: request, method: HTTPMethod.delete, completion: completion) - } - - func retrieveIcons(completion: @escaping (Result<[GoogleReaderCompatibleIcon]?, Error>) -> Void) { + func deleteTagging(subscriptionID: String, tagName: String, completion: @escaping (Result) -> Void) { + guard let baseURL = APIBaseURL else { + completion(.failure(CredentialsError.incompleteCredentials)) + return + } -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("icons.json") -// let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.icons] -// let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) -// -// transport.send(request: request, resultType: [GoogleReaderCompatibleIcon].self) { result in -// -// switch result { -// case .success(let (response, icons)): -// self.storeConditionalGet(key: ConditionalGetKeys.icons, headers: response.allHeaderFields) -// completion(.success(icons)) -// case .failure(let error): -// completion(.failure(error)) -// } -// -// } -// + self.requestAuthorizationToken(endpoint: baseURL) { (result) in + switch result { + case .success(let token): + var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + + + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let tagName = "user/-/label/\(tagName)" + let postData = "T=\(token)&s=\(subscriptionID)&ac=edit&r=\(tagName)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, payload: postData!, completion: { (result) in + switch result { + case .success: + completion(.success(())) + break + case .failure(let error): + completion(.failure(error)) + break + } + }) + + + case .failure(let error): + completion(.failure(error)) + } + } } func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([GoogleReaderCompatibleEntry]?), Error>) -> Void) { @@ -544,7 +606,6 @@ final class GoogleReaderCompatibleAPICaller: NSObject { }).joined(separator:"&") let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) - //let postData = "T=\(token)&output=json&i=1349530380539369".data(using: String.Encoding.utf8) self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in switch result { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 894de778e..4c83ba17c 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -265,13 +265,6 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - - // Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system - guard folder.hasAtLeastOneFeed() else { - account.removeFolder(folder) - return - } - let group = DispatchGroup() for feed in folder.topLevelFeeds { @@ -288,8 +281,17 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { } group.notify(queue: DispatchQueue.main) { - account.removeFolder(folder) - completion(.success(())) + self.caller.deleteTag(name: folder.name!) { (result) in + switch result { + case .success: + account.removeFolder(folder) + completion(.success(())) + case .failure(let error): + os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription) + } + + } + } } @@ -373,12 +375,12 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { - if let folder = container as? Folder, let feedID = Int(feed.feedID) { - caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in + if let folder = container as? Folder, let feedName = feed.subscriptionID { + caller.createTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in switch result { - case .success(let taggingID): + case .success: DispatchQueue.main.async { - self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID)) + self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: feed.subscriptionID!) account.removeFeed(feed) folder.addFeed(feed) completion(.success(())) @@ -705,7 +707,8 @@ private extension GoogleReaderCompatibleAccountDelegate { for subscription in groupedTaggings { let taggingFeedID = String(subscription.feedID) if !folderFeedIds.contains(taggingFeedID) { - guard let feed = account.idToFeedDictionary[taggingFeedID] else { + let idDictionary = account.idToFeedDictionary + guard let feed = idDictionary[taggingFeedID] else { continue } DispatchQueue.main.sync { @@ -1097,8 +1100,8 @@ private extension GoogleReaderCompatibleAccountDelegate { func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { - if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] { - caller.deleteTagging(taggingID: feedTaggingID) { result in + if let folder = container as? Folder, let feedName = feed.subscriptionID { + caller.deleteTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in switch result { case .success: DispatchQueue.main.async { From adf53add3a5ff1fa9a1cde84c72c3545a508240f Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sun, 16 Jun 2019 13:15:36 -0400 Subject: [PATCH 25/31] removed unused page number function. --- .../GoogleReaderCompatibleAPICaller.swift | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 83c6158f5..6b914266d 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -984,24 +984,4 @@ extension GoogleReaderCompatibleAPICaller { accountMetadata?.conditionalGetInfo = conditionalGet } } - - func extractPageNumber(link: String?) -> Int? { - - guard let link = link else { - return nil - } - - if let lowerBound = link.range(of: "page=")?.upperBound { - if let upperBound = link.range(of: "&")?.lowerBound { - return Int(link[lowerBound..")?.lowerBound { - return Int(link[lowerBound.. Date: Sun, 16 Jun 2019 15:14:20 -0400 Subject: [PATCH 26/31] FIrst pass at OPML import. Broken. --- .../GoogleReaderCompatibleAPICaller.swift | 64 ++------ ...oogleReaderCompatibleAccountDelegate.swift | 155 ++++++++++++------ 2 files changed, 113 insertions(+), 106 deletions(-) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 6b914266d..5025f315d 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -49,10 +49,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { case editTag = "/reader/api/0/edit-tag" } - // private let GoogleReaderCompatibleBaseURL = URL(string: "https://api.GoogleReaderCompatible.com/v2/")! private var transport: Transport! var credentials: Credentials? + private var accessToken: String? + weak var accountMetadata: AccountMetadata? var server: String? { @@ -127,6 +128,13 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } func requestAuthorizationToken(endpoint: URL, completion: @escaping (Result) -> Void) { + // If we have a token already, use it + if let accessToken = accessToken { + completion(.success(accessToken)) + return + } + + // Otherwise request one. guard let credentials = credentials else { completion(.failure(CredentialsError.incompleteCredentials)) return @@ -143,67 +151,19 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Convert the return data to UTF8 and then parse out the Auth token - guard let rawData = String(data: resultData, encoding: .utf8) else { + guard let accessToken = String(data: resultData, encoding: .utf8) else { completion(.failure(TransportError.noData)) break } - - completion(.success(rawData)) + self.accessToken = accessToken + completion(.success(accessToken)) case .failure(let error): completion(.failure(error)) } } } - func importOPML(opmlData: Data, completion: @escaping (Result) -> Void) { - -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports.json") -// var request = URLRequest(url: callURL, credentials: credentials) -// request.addValue("text/xml; charset=utf-8", forHTTPHeaderField: HTTPRequestHeader.contentType) -// -// transport.send(request: request, method: HTTPMethod.post, payload: opmlData) { result in -// -// switch result { -// case .success(let (_, data)): -// -// guard let resultData = data else { -// completion(.failure(TransportError.noData)) -// break -// } -// -// do { -// let result = try JSONDecoder().decode(GoogleReaderCompatibleImportResult.self, from: resultData) -// completion(.success(result)) -// } catch { -// completion(.failure(error)) -// } -// -// case .failure(let error): -// completion(.failure(error)) -// } -// -// } - - } - - func retrieveOPMLImportResult(importID: Int, completion: @escaping (Result) -> Void) { - -// let callURL = GoogleReaderCompatibleBaseURL.appendingPathComponent("imports/\(importID).json") -// let request = URLRequest(url: callURL, credentials: credentials) -// -// transport.send(request: request, resultType: GoogleReaderCompatibleImportResult.self) { result in -// -// switch result { -// case .success(let (_, importResult)): -// completion(.success(importResult)) -// case .failure(let error): -// completion(.failure(error)) -// } -// -// } - - } func retrieveTags(completion: @escaping (Result<[GoogleReaderCompatibleTag]?, Error>) -> Void) { guard let baseURL = APIBaseURL else { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 4c83ba17c..3fa28faaf 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -210,31 +210,113 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { return } - os_log(.debug, log: log, "Begin importing OPML...") - opmlImportInProgress = true + let parserData = ParserData(url: opmlFile.absoluteString, data: opmlData) + var opmlDocument: RSOPMLDocument? - caller.importOPML(opmlData: opmlData) { result in - switch result { - case .success(let importResult): - if importResult.complete { - os_log(.debug, log: self.log, "Import OPML done.") - self.opmlImportInProgress = false - DispatchQueue.main.async { - completion(.success(())) - } - } else { - self.checkImportResult(opmlImportResultID: importResult.importResultID, completion: completion) + do { + opmlDocument = try RSOPMLParser.parseOPML(with: parserData) + } catch { + completion(.failure(error)) + return + } + + guard let loadDocument = opmlDocument else { + completion(.success(())) + return + } + + // We use the same mechanism to load local accounts as we do to load the subscription + // OPML all accounts. + BatchUpdate.shared.perform { + loadOPML(account: account, opmlDocument: loadDocument) + } + completion(.success(())) + + } + + func loadOPML(account: Account, opmlDocument: RSOPMLDocument) { + + guard let children = opmlDocument.children else { + return + } + loadOPMLItems(account: account, items: children, parentFolder: nil) + } + + func loadOPMLItems(account: Account, items: [RSOPMLItem], parentFolder: Folder?) { + + var feedsToAdd = Set() + + items.forEach { (item) in + + if let feedSpecifier = item.feedSpecifier { + feedsToAdd.insert(feedSpecifier.feedURL) + return + } + + guard let folderName = item.titleFromAttributes else { + // Folder doesn’t have a name, so it won’t be created, and its items will go one level up. + if let itemChildren = item.children { + loadOPMLItems(account: account, items: itemChildren, parentFolder: parentFolder) } - case .failure(let error): - os_log(.debug, log: self.log, "Import OPML failed.") - self.opmlImportInProgress = false - DispatchQueue.main.async { - let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + return + } + + if let itemChildren = item.children, let folder = account.ensureFolder(with: folderName) { + loadOPMLItems(account: account, items: itemChildren, parentFolder: folder) + } + } + + let group = DispatchGroup() + + if let parentFolder = parentFolder { + for url in feedsToAdd { + group.enter() + caller.createSubscription(url: url) { result in + group.leave() + switch result { + case .success(let subResult): + switch subResult { + case .created(let subscription): + let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: String(subscription.feedID), homePageURL: subscription.homePageURL) + feed.subscriptionID = String(subscription.feedID) + account.addFeed(feed, to: parentFolder) { _ in } + default: + break + } + case .failure(_): + break + } + + } + } + } else { + for url in feedsToAdd { + group.enter() + caller.createSubscription(url: url) { result in + group.leave() + switch result { + case .success(let subResult): + switch subResult { + case .created(let subscription): + let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: String(subscription.feedID), homePageURL: subscription.homePageURL) + feed.subscriptionID = String(subscription.feedID) + account.addFeed(feed) + default: + break + } + case .failure(_): + break + } } } } + group.notify(queue: DispatchQueue.main) { + + DispatchQueue.main.async { + self.refreshAll(for: account) { (_) in } + } + } } func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { @@ -498,41 +580,6 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func checkImportResult(opmlImportResultID: Int, completion: @escaping (Result) -> Void) { - - DispatchQueue.main.async { - - Timer.scheduledTimer(withTimeInterval: 15, repeats: true) { timer in - - os_log(.debug, log: self.log, "Checking status of OPML import...") - - self.caller.retrieveOPMLImportResult(importID: opmlImportResultID) { result in - switch result { - case .success(let importResult): - if let result = importResult, result.complete { - os_log(.debug, log: self.log, "Checking status of OPML import successfully completed.") - timer.invalidate() - self.opmlImportInProgress = false - DispatchQueue.main.async { - completion(.success(())) - } - } - case .failure(let error): - os_log(.debug, log: self.log, "Import OPML check failed.") - timer.invalidate() - self.opmlImportInProgress = false - DispatchQueue.main.async { - completion(.failure(error)) - } - } - } - - } - - } - - } - func syncFolders(_ account: Account, _ tags: [GoogleReaderCompatibleTag]?) { guard let tags = tags else { return } From 19395779c88f3f1b043cbfba81152ca4e3c54bda Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sun, 16 Jun 2019 15:21:35 -0400 Subject: [PATCH 27/31] Change label from email -> login --- .../AccountsGoogleReaderCompatible.xib | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib index e734fab2c..6e2f71ba7 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib @@ -1,7 +1,8 @@ - + - + + @@ -22,13 +23,13 @@ - + - + - + @@ -39,7 +40,7 @@ - + @@ -57,7 +58,7 @@ - + @@ -71,8 +72,8 @@ - - + + @@ -81,7 +82,7 @@ - + @@ -94,7 +95,7 @@ - + @@ -104,7 +105,7 @@ - + @@ -120,7 +121,7 @@ - + @@ -130,7 +131,7 @@ - + @@ -143,7 +144,7 @@ - + @@ -181,7 +182,7 @@ Gw From 125ea59cd6a271ff1e8f45e41b857dd712921594 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Sun, 16 Jun 2019 18:22:00 -0400 Subject: [PATCH 28/31] Wiring up iOS add account settings --- ...ogleReaderCompatibleWindowController.swift | 2 +- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/Settings/SettingsAddAccountView.swift | 3 + iOS/Settings/SettingsFeedbinAccountView.swift | 2 +- ...ngsGoogleReaderCompatibleAccountView.swift | 187 ++++++++++++++++++ .../UIKit/FeedbinAccountViewController.swift | 2 +- 6 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift index 89db9f277..b70b481b1 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift @@ -66,7 +66,7 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { progressIndicator.startAnimation(self) guard let apiURL = URL(string: apiURLTextField.stringValue) else { - self.errorMessageLabel.stringValue = NSLocalizedString("Invalie API URL.", comment: "Credentials Error") + self.errorMessageLabel.stringValue = NSLocalizedString("Invalid API URL.", comment: "Credentials Error") return } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 414f74d58..09871e8cd 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -154,6 +154,7 @@ 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; }; 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; + 557EE1AE22B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */; }; 55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */; }; 55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */; }; 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; @@ -750,6 +751,7 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; }; 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; + 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsGoogleReaderCompatibleAccountView.swift; sourceTree = ""; }; 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsGoogleReaderCompatible.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsGoogleReaderCompatibleWindowController.swift; sourceTree = ""; }; 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1050,6 +1052,7 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( + 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */, 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */, 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */, 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */, @@ -2375,6 +2378,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, + 557EE1AE22B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, diff --git a/iOS/Settings/SettingsAddAccountView.swift b/iOS/Settings/SettingsAddAccountView.swift index fde5175f0..212c62f04 100644 --- a/iOS/Settings/SettingsAddAccountView.swift +++ b/iOS/Settings/SettingsAddAccountView.swift @@ -16,6 +16,9 @@ struct SettingsAddAccountView : View { destination: SettingsLocalAccountView(name: "")).padding(.all, 4) PresentationButton(SettingsAccountLabelView(accountImage: "accountFeedbin", accountLabel: "Feedbin"), destination: SettingsFeedbinAccountView(viewModel: SettingsFeedbinAccountView.ViewModel())).padding(.all, 4) + PresentationButton(SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "Google Reader Compatible"), + destination: SettingsGoogleReaderCompatibleAccountView(viewModel: SettingsGoogleReaderCompatibleAccountView.ViewModel())).padding(.all, 4) + } .listStyle(.grouped) .navigationBarTitle(Text("Add Account"), displayMode: .inline) diff --git a/iOS/Settings/SettingsFeedbinAccountView.swift b/iOS/Settings/SettingsFeedbinAccountView.swift index c867c94d2..59d7a7094 100644 --- a/iOS/Settings/SettingsFeedbinAccountView.swift +++ b/iOS/Settings/SettingsFeedbinAccountView.swift @@ -80,7 +80,7 @@ struct SettingsFeedbinAccountView : View { switch result { case .success(let authenticated): - if authenticated { + if (authenticated != nil) { var newAccount = false let workAccount: Account diff --git a/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift b/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift new file mode 100644 index 000000000..c36d71023 --- /dev/null +++ b/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift @@ -0,0 +1,187 @@ +// +// SettingsGoogleReaderCompatibleAccountView.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 6/11/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import SwiftUI +import Combine +import Account +import RSWeb + +struct SettingsGoogleReaderCompatibleAccountView : View { + @Environment(\.isPresented) private var isPresented + @ObjectBinding var viewModel: ViewModel + @State var busy: Bool = false + @State var error: Text = Text("") + + var body: some View { + NavigationView { + List { + Section(header: + SettingsAccountLabelView(accountImage: "accountLocal", accountLabel: "Google Reader Compatible").padding() + ) { + HStack { + Text("Email:") + Divider() + TextField($viewModel.email) + .textContentType(.username) + } + HStack { + Text("Password:") + Divider() + SecureField($viewModel.password) + } + HStack { + Text("API URL:") + Divider() + TextField($viewModel.apiURL) + .textContentType(.URL) + } + } + Section(footer: + HStack { + Spacer() + error.color(.red) + Spacer() + } + ) { + HStack { + Spacer() + Button(action: { self.addAccount() }) { + if viewModel.isUpdate { + Text("Update Account") + } else { + Text("Add Account") + } + } + .disabled(!viewModel.isValid) + Spacer() + } + } + } + .disabled(busy) + .listStyle(.grouped) + .navigationBarTitle(Text(""), displayMode: .inline) + .navigationBarItems(leading: + Button(action: { self.dismiss() }) { Text("Cancel") } + ) + } + } + + private func addAccount() { + + busy = true + error = Text("") + + let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces) + let credentials = Credentials.basic(username: emailAddress, password: viewModel.password) + guard let apiURL = URL(string: viewModel.apiURL) else { + self.error = Text("Invalide API URL.") + return + } + + Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials, endpoint: apiURL) { result in + + self.busy = false + + switch result { + case .success(let authenticated): + + if (authenticated != nil) { + + var newAccount = false + let workAccount: Account + if self.viewModel.account == nil { + workAccount = AccountManager.shared.createAccount(type: .googleReaderCompatible) + newAccount = true + } else { + workAccount = self.viewModel.account! + } + + do { + + do { + try workAccount.removeBasicCredentials() + } catch {} + + workAccount.endpointURL = apiURL + + try workAccount.storeCredentials(credentials) + + if newAccount { + workAccount.refreshAll() { result in } + } + + self.dismiss() + + } catch { + self.error = Text("Keychain error while storing credentials.") + } + + } else { + self.error = Text("Invalid email/password combination.") + } + + case .failure: + self.error = Text("Network error. Try again later.") + } + + } + + } + + private func dismiss() { + isPresented?.value = false + } + + class ViewModel: BindableObject { + let didChange = PassthroughSubject() + var account: Account? = nil + + init() { + } + + init(account: Account) { + self.account = account + if case .basic(let username, let password) = try? account.retrieveBasicCredentials() { + self.email = username + self.password = password + } + } + + var email: String = "" { + didSet { + didChange.send(self) + } + } + var password: String = "" { + didSet { + didChange.send(self) + } + } + var apiURL: String = "" { + didSet { + didChange.send(self) + } + } + var isUpdate: Bool { + return account != nil + } + + var isValid: Bool { + return !email.isEmpty && !password.isEmpty + } + } + +} + +#if DEBUG +struct SettingsGoogleReaderCompatibleAccountView_Previews : PreviewProvider { + static var previews: some View { + SettingsGoogleReaderCompatibleAccountView(viewModel: SettingsGoogleReaderCompatibleAccountView.ViewModel()) + } +} +#endif diff --git a/iOS/Settings/UIKit/FeedbinAccountViewController.swift b/iOS/Settings/UIKit/FeedbinAccountViewController.swift index c12d3d941..3cbf24bea 100644 --- a/iOS/Settings/UIKit/FeedbinAccountViewController.swift +++ b/iOS/Settings/UIKit/FeedbinAccountViewController.swift @@ -67,7 +67,7 @@ class FeedbinAccountViewController: UIViewController { switch result { case .success(let authenticated): - if authenticated { + if (authenticated != nil) { var newAccount = false if self.account == nil { self.account = AccountManager.shared.createAccount(type: .feedbin) From 596e9c4537df03dafdf0b4299dabfb657751b778 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Tue, 18 Jun 2019 15:37:39 -0400 Subject: [PATCH 29/31] Use proper google credential. --- iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift b/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift index c36d71023..6ecde08dd 100644 --- a/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift +++ b/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift @@ -77,7 +77,7 @@ struct SettingsGoogleReaderCompatibleAccountView : View { error = Text("") let emailAddress = viewModel.email.trimmingCharacters(in: .whitespaces) - let credentials = Credentials.basic(username: emailAddress, password: viewModel.password) + let credentials = Credentials.googleBasicLogin(username: emailAddress, password: viewModel.password) guard let apiURL = URL(string: viewModel.apiURL) else { self.error = Text("Invalide API URL.") return From 52cb066dd4cddf45f73964231532e39b2caf7100 Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Wed, 19 Jun 2019 07:56:25 -0400 Subject: [PATCH 30/31] Cleanup unused objects --- .../Account/Account.xcodeproj/project.pbxproj | 16 ------------- .../GoogleReaderCompatibleAPICaller.swift | 18 +++++++------- ...oogleReaderCompatibleAccountDelegate.swift | 2 +- .../GoogleReaderCompatibleDate.swift | 21 ---------------- .../GoogleReaderCompatibleEntry.swift | 2 +- .../GoogleReaderCompatibleIcon.swift | 21 ---------------- .../GoogleReaderCompatibleImportResult.swift | 21 ---------------- .../GoogleReaderCompatibleStarredEntry.swift | 19 --------------- .../GoogleReaderCompatibleSubscription.swift | 2 +- .../GoogleReaderCompatibleTag.swift | 24 +------------------ .../GoogleReaderCompatibleTagging.swift | 2 +- .../GoogleReaderCompatibleUnreadEntry.swift | 2 +- 12 files changed, 15 insertions(+), 135 deletions(-) delete mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift delete mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift delete mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift delete mode 100644 Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index a9b840800..0003bb8a7 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -35,16 +35,12 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; }; 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; - 552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */; }; - 552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */; }; 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */; }; 552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */; }; - 552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */; }; 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */; }; 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */; }; 552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */; }; 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */; }; - 552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */; }; 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */; }; 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; }; 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; }; @@ -147,16 +143,12 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; - 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleDate.swift; sourceTree = ""; }; - 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleIcon.swift; sourceTree = ""; }; 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleEntry.swift; sourceTree = ""; }; 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleSubscription.swift; sourceTree = ""; }; - 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleStarredEntry.swift; sourceTree = ""; }; 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTag.swift; sourceTree = ""; }; 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleUnreadEntry.swift; sourceTree = ""; }; 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTagging.swift; sourceTree = ""; }; 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAccountDelegate.swift; sourceTree = ""; }; - 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleImportResult.swift; sourceTree = ""; }; 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAPICaller.swift; sourceTree = ""; }; 841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; @@ -247,16 +239,12 @@ 552032EA229D5D5A009559E0 /* GoogleReaderCompatible */ = { isa = PBXGroup; children = ( - 552032EB229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift */, - 552032EC229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift */, 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */, 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */, - 552032EF229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift */, 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */, 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */, 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */, 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */, - 552032F4229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift */, 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */, ); path = GoogleReaderCompatible; @@ -566,7 +554,6 @@ 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, - 552032FA229D5D5A009559E0 /* GoogleReaderCompatibleStarredEntry.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */, @@ -576,7 +563,6 @@ 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */, 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */, - 552032F6229D5D5A009559E0 /* GoogleReaderCompatibleDate.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */, 51E3EB41229AF61B00645299 /* AccountError.swift in Sources */, @@ -584,8 +570,6 @@ 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */, 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */, 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, - 552032F7229D5D5A009559E0 /* GoogleReaderCompatibleIcon.swift in Sources */, - 552032FF229D5D5A009559E0 /* GoogleReaderCompatibleImportResult.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */, 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */, diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift index 5025f315d..ec09e2976 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift @@ -2,14 +2,10 @@ // GoogleReaderCompatibleAPICaller.swift // Account // -// Created by Maurice Parker on 5/2/19. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // -// GoogleReaderCompatible currently has a maximum of 250 requests per second. If you begin to receive -// HTTP Response Codes of 403, you have exceeded this limit. Wait 5 minutes and your -// IP address will become unblocked and you can use the service again. - import Foundation import RSWeb @@ -35,6 +31,10 @@ final class GoogleReaderCompatibleAPICaller: NSObject { case starred = "user/-/state/com.google/starred" } + enum GoogleReaderStreams: String { + case readingList = "user/-/state/com.google/reading-list" + } + enum GoogleReaderEndpoints: String { case login = "/accounts/ClientLogin" case token = "/reader/api/0/token" @@ -677,8 +677,8 @@ final class GoogleReaderCompatibleAPICaller: NSObject { URLQueryItem(name: "o", value: String(sinceString)), URLQueryItem(name: "n", value: "10000"), URLQueryItem(name: "output", value: "json"), - URLQueryItem(name: "xt", value: "user/-/state/com.google/read"), - URLQueryItem(name: "s", value: "user/-/state/com.google/reading-list") + URLQueryItem(name: "xt", value: GoogleReaderState.read.rawValue), + URLQueryItem(name: "s", value: GoogleReaderStreams.readingList.rawValue) ] guard let callURL = components.url else { @@ -789,9 +789,9 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } components.queryItems = [ - URLQueryItem(name: "s", value: "user/-/state/com.google/reading-list"), + URLQueryItem(name: "s", value: GoogleReaderStreams.readingList.rawValue), URLQueryItem(name: "n", value: "10000"), - URLQueryItem(name: "xt", value: "user/-/state/com.google/read"), + URLQueryItem(name: "xt", value: GoogleReaderState.read.rawValue), URLQueryItem(name: "output", value: "json") ] diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift index 3fa28faaf..1a4f666c7 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleAccountDelegate.swift // Account // -// Created by Maurice Parker on 5/2/19. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift deleted file mode 100644 index c292eab35..000000000 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleDate.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GoogleReaderCompatibleDate.swift -// Account -// -// Created by Maurice Parker on 5/12/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct GoogleReaderCompatibleDate { - - public static var formatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" - formatter.locale = Locale(identifier: "en_US") - formatter.timeZone = TimeZone(abbreviation: "GMT") - return formatter - }() - -} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift index 8757262ee..34bf05c95 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleArticle.swift // Account // -// Created by Brent Simmons on 12/11/17. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2017 Ranchero Software, LLC. All rights reserved. // diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift deleted file mode 100644 index 69b2fc5d7..000000000 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleIcon.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GoogleReaderCompatibleIcon.swift -// Account -// -// Created by Maurice Parker on 5/6/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct GoogleReaderCompatibleIcon: Codable { - - let host: String - let url: String - - enum CodingKeys: String, CodingKey { - case host - case url - } - -} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift deleted file mode 100644 index fbc6a45e9..000000000 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleImportResult.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// GoogleReaderCompatibleImportResult.swift -// Account -// -// Created by Maurice Parker on 5/17/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct GoogleReaderCompatibleImportResult: Codable { - - let importResultID: Int - let complete: Bool - - enum CodingKeys: String, CodingKey { - case importResultID = "id" - case complete - } - -} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift deleted file mode 100644 index 8036f7b63..000000000 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleStarredEntry.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// GoogleReaderCompatibleStarredEntry.swift -// Account -// -// Created by Maurice Parker on 5/15/19. -// Copyright © 2019 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct GoogleReaderCompatibleStarredEntry: Codable { - - let starredEntries: [Int] - - enum CodingKeys: String, CodingKey { - case starredEntries = "starred_entries" - } - -} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift index 9736ef93d..6f5469225 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleFeed.swift // Account // -// Created by Brent Simmons on 12/10/17. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2017 Ranchero Software, LLC. All rights reserved. // diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift index 16fd2695f..538fdab2f 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleTag.swift // Account // -// Created by Maurice Parker on 5/5/19. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // @@ -27,25 +27,3 @@ struct GoogleReaderCompatibleTag: Codable { } } - -struct GoogleReaderCompatibleRenameTag: Codable { - - let oldName: String - let newName: String - - enum CodingKeys: String, CodingKey { - case oldName = "old_name" - case newName = "new_name" - } - -} - -struct GoogleReaderCompatibleDeleteTag: Codable { - - let name: String - - enum CodingKeys: String, CodingKey { - case name - } - -} diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift index b3a0f57aa..e35ee0e3b 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleTagging.swift // Account // -// Created by Brent Simmons on 10/14/18. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2018 Ranchero Software, LLC. All rights reserved. // diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift index b08073bfc..c4e42f8c0 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift +++ b/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift @@ -2,7 +2,7 @@ // GoogleReaderCompatibleUnreadEntry.swift // Account // -// Created by Maurice Parker on 5/15/19. +// Created by Jeremy Beker on 5/28/19. // Copyright © 2019 Ranchero Software, LLC. All rights reserved. // From d87f68a006040de555aba5da680b6a63058cbb1e Mon Sep 17 00:00:00 2001 From: Jeremy Beker Date: Wed, 19 Jun 2019 12:25:37 -0400 Subject: [PATCH 31/31] Renamed GoogleReaderCompatible to just Reader --- Frameworks/Account/Account.swift | 14 +-- .../Account/Account.xcodeproj/project.pbxproj | 62 +++++------ .../ReaderAPIAccountDelegate.swift} | 54 ++++----- .../ReaderAPICaller.swift} | 104 +++++++++--------- .../ReaderAPIEntry.swift} | 20 ++-- .../ReaderAPISubscription.swift} | 25 ++--- .../ReaderAPITag.swift} | 8 +- .../ReaderAPITagging.swift} | 6 +- .../ReaderAPIUnreadEntry.swift} | 8 +- .../Accounts/AccountsAddViewController.swift | 8 +- .../AccountsPreferencesViewController.swift | 2 +- ...erCompatible.xib => AccountsReaderAPI.xib} | 14 +-- ...> AccountsReaderAPIWindowController.swift} | 8 +- Mac/Scriptability/Account+Scriptability.swift | 2 +- NetNewsWire.xcodeproj/project.pbxproj | 24 ++-- ...ift => SettingsReaderAPIAccountView.swift} | 14 +-- 16 files changed, 183 insertions(+), 190 deletions(-) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift => ReaderAPI/ReaderAPIAccountDelegate.swift} (93%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift => ReaderAPI/ReaderAPICaller.swift} (86%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift => ReaderAPI/ReaderAPIEntry.swift} (83%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift => ReaderAPI/ReaderAPISubscription.swift} (71%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleTag.swift => ReaderAPI/ReaderAPITag.swift} (66%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift => ReaderAPI/ReaderAPITagging.swift} (76%) rename Frameworks/Account/{GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift => ReaderAPI/ReaderAPIUnreadEntry.swift} (61%) rename Mac/Preferences/Accounts/{AccountsGoogleReaderCompatible.xib => AccountsReaderAPI.xib} (97%) rename Mac/Preferences/Accounts/{AccountsGoogleReaderCompatibleWindowController.swift => AccountsReaderAPIWindowController.swift} (92%) rename iOS/Settings/{SettingsGoogleReaderCompatibleAccountView.swift => SettingsReaderAPIAccountView.swift} (88%) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index fa1adca3c..a498c04ce 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -33,7 +33,7 @@ public enum AccountType: Int { case feedbin = 17 case feedWrangler = 18 case newsBlur = 19 - case googleReaderCompatible = 20 + case googleReaderAPI = 20 // TODO: more } @@ -217,8 +217,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, self.delegate = LocalAccountDelegate() case .feedbin: self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport) - case .googleReaderCompatible: - self.delegate = GoogleReaderCompatibleAccountDelegate(dataFolder: dataFolder, transport: transport) + case .googleReaderAPI: + self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport) default: fatalError("Only Local and Feedbin accounts are supported") } @@ -246,8 +246,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, defaultName = "FeedWrangler" case .newsBlur: defaultName = "NewsBlur" - case .googleReaderCompatible: - defaultName = "Google Reader Compatible" + case .googleReaderAPI: + defaultName = "Reader" } NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil) @@ -327,8 +327,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) case .feedbin: FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion) - case .googleReaderCompatible: - GoogleReaderCompatibleAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion) + case .googleReaderAPI: + ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion) default: break } diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 0003bb8a7..3a6f1fd8b 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -35,13 +35,13 @@ 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E490352288C37100C791F0 /* FeedbinDate.swift */; }; 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */; }; 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */; }; - 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */; }; - 552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */; }; - 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */; }; - 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */; }; - 552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */; }; - 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */; }; - 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */; }; + 552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */; }; + 552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */; }; + 552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */; }; + 552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F1229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift */; }; + 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */; }; + 552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */; }; + 55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */; }; 841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; }; 841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; }; 841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; }; @@ -143,13 +143,13 @@ 51E490352288C37100C791F0 /* FeedbinDate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinDate.swift; sourceTree = ""; }; 51E59598228C77BC00FCC42B /* FeedbinUnreadEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinUnreadEntry.swift; sourceTree = ""; }; 51E5959A228C781500FCC42B /* FeedbinStarredEntry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinStarredEntry.swift; sourceTree = ""; }; - 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleEntry.swift; sourceTree = ""; }; - 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleSubscription.swift; sourceTree = ""; }; - 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTag.swift; sourceTree = ""; }; - 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleUnreadEntry.swift; sourceTree = ""; }; - 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleTagging.swift; sourceTree = ""; }; - 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAccountDelegate.swift; sourceTree = ""; }; - 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GoogleReaderCompatibleAPICaller.swift; sourceTree = ""; }; + 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIEntry.swift; sourceTree = ""; }; + 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPISubscription.swift; sourceTree = ""; }; + 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITag.swift; sourceTree = ""; }; + 552032F1229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIUnreadEntry.swift; sourceTree = ""; }; + 552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITagging.swift; sourceTree = ""; }; + 552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountDelegate.swift; sourceTree = ""; }; + 552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPICaller.swift; sourceTree = ""; }; 841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = ""; }; 841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = ""; }; 841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = ""; }; @@ -236,18 +236,18 @@ path = JSON; sourceTree = ""; }; - 552032EA229D5D5A009559E0 /* GoogleReaderCompatible */ = { + 552032EA229D5D5A009559E0 /* ReaderAPI */ = { isa = PBXGroup; children = ( - 552032ED229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift */, - 552032EE229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift */, - 552032F0229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift */, - 552032F1229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift */, - 552032F2229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift */, - 552032F3229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift */, - 552032F5229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift */, + 552032ED229D5D5A009559E0 /* ReaderAPIEntry.swift */, + 552032EE229D5D5A009559E0 /* ReaderAPISubscription.swift */, + 552032F0229D5D5A009559E0 /* ReaderAPITag.swift */, + 552032F1229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift */, + 552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */, + 552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */, + 552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */, ); - path = GoogleReaderCompatible; + path = ReaderAPI; sourceTree = ""; }; 841973E91F6DD19E006346C4 /* Products */ = { @@ -330,7 +330,7 @@ 5165D71F22835E9800D9D53D /* FeedFinder */, 8419742B1F6DDE84006346C4 /* LocalAccount */, 84245C7D1FDDD2580074AFBB /* Feedbin */, - 552032EA229D5D5A009559E0 /* GoogleReaderCompatible */, + 552032EA229D5D5A009559E0 /* ReaderAPI */, 848935031F62484F00CEBD24 /* AccountTests */, 848934F71F62484F00CEBD24 /* Products */, 8469F80F1F6DC3C10084783E /* Frameworks */, @@ -544,13 +544,13 @@ buildActionMask = 2147483647; files = ( 84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */, - 552032F9229D5D5A009559E0 /* GoogleReaderCompatibleSubscription.swift in Sources */, + 552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */, 84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */, 8469F81C1F6DD15E0084783E /* Account.swift in Sources */, 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */, 51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */, 846E77451F6EF9B900A165E2 /* Container.swift in Sources */, - 552032FD229D5D5A009559E0 /* GoogleReaderCompatibleTagging.swift in Sources */, + 552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */, 84F73CF1202788D90000BCEF /* ArticleFetcher.swift in Sources */, 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, @@ -564,15 +564,15 @@ 84B99C9F1FAE8D3200ECDEDB /* ContainerPath.swift in Sources */, 5133231122810EB200C30F19 /* FeedbinIcon.swift in Sources */, 846E77501F6EF9C400A165E2 /* LocalAccountRefresher.swift in Sources */, - 55203300229D5D5A009559E0 /* GoogleReaderCompatibleAPICaller.swift in Sources */, + 55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */, 51E3EB41229AF61B00645299 /* AccountError.swift in Sources */, 51E59599228C77BC00FCC42B /* FeedbinUnreadEntry.swift in Sources */, - 552032F8229D5D5A009559E0 /* GoogleReaderCompatibleEntry.swift in Sources */, - 552032FB229D5D5A009559E0 /* GoogleReaderCompatibleTag.swift in Sources */, + 552032F8229D5D5A009559E0 /* ReaderAPIEntry.swift in Sources */, + 552032FB229D5D5A009559E0 /* ReaderAPITag.swift in Sources */, 5165D72822835F7800D9D53D /* FeedFinder.swift in Sources */, 51D58755227F53BE00900287 /* FeedbinTag.swift in Sources */, - 552032FE229D5D5A009559E0 /* GoogleReaderCompatibleAccountDelegate.swift in Sources */, - 552032FC229D5D5A009559E0 /* GoogleReaderCompatibleUnreadEntry.swift in Sources */, + 552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */, + 552032FC229D5D5A009559E0 /* ReaderAPIUnreadEntry.swift in Sources */, 84D09623217418DC00D77525 /* FeedbinTagging.swift in Sources */, 84CAD7161FDF2E22000F0755 /* FeedbinEntry.swift in Sources */, 5165D72A22835F7D00D9D53D /* HTMLFeedFinder.swift in Sources */, diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift similarity index 93% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift rename to Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 1a4f666c7..6c2143f66 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAccountDelegate.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleAccountDelegate.swift +// ReaderAPIAccountDelegate.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -19,17 +19,17 @@ import RSWeb import SyncDatabase import os.log -public enum GoogleReaderCompatibleAccountDelegateError: String, Error { +public enum ReaderAPIAccountDelegateError: String, Error { case invalidParameter = "There was an invalid parameter passed." case invalidResponse = "There was an invalid response from the server." } -final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { +final class ReaderAPIAccountDelegate: AccountDelegate { private let database: SyncDatabase - private let caller: GoogleReaderCompatibleAPICaller - private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "GoogleReaderCompatible") + private let caller: ReaderAPICaller + private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI") let supportsSubFolders = false let usesTags = true @@ -61,7 +61,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { if transport != nil { - caller = GoogleReaderCompatibleAPICaller(transport: transport!) + caller = ReaderAPICaller(transport: transport!) } else { @@ -78,7 +78,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { sessionConfiguration.httpAdditionalHeaders = userAgentHeaders } - caller = GoogleReaderCompatibleAPICaller(transport: URLSession(configuration: sessionConfiguration)) + caller = ReaderAPICaller(transport: URLSession(configuration: sessionConfiguration)) } @@ -547,7 +547,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { return } - let caller = GoogleReaderCompatibleAPICaller(transport: transport) + let caller = ReaderAPICaller(transport: transport) caller.credentials = credentials caller.validateCredentials(endpoint: endpoint) { result in DispatchQueue.main.async { @@ -561,7 +561,7 @@ final class GoogleReaderCompatibleAccountDelegate: AccountDelegate { // MARK: Private -private extension GoogleReaderCompatibleAccountDelegate { +private extension ReaderAPIAccountDelegate { func refreshAccount(_ account: Account, completion: @escaping (Result) -> Void) { @@ -580,7 +580,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func syncFolders(_ account: Account, _ tags: [GoogleReaderCompatibleTag]?) { + func syncFolders(_ account: Account, _ tags: [ReaderAPITag]?) { guard let tags = tags else { return } @@ -588,7 +588,7 @@ private extension GoogleReaderCompatibleAccountDelegate { let tagNames = tags.filter { $0.type == "folder" }.map { $0.tagID.replacingOccurrences(of: "user/-/label/", with: "") } - // Delete any folders not at GoogleReaderCompatible + // Delete any folders not at Reader if let folders = account.folders { folders.forEach { folder in if !tagNames.contains(folder.name ?? "") { @@ -611,7 +611,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } }() - // Make any folders GoogleReaderCompatible has, but we don't + // Make any folders Reader has, but we don't tagNames.forEach { tagName in if !folderNames.contains(tagName) { DispatchQueue.main.sync { @@ -647,7 +647,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func syncFeeds(_ account: Account, _ subscriptions: [GoogleReaderCompatibleSubscription]?) { + func syncFeeds(_ account: Account, _ subscriptions: [ReaderAPISubscription]?) { guard let subscriptions = subscriptions else { return } @@ -697,7 +697,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func syncTaggings(_ account: Account, _ subscriptions: [GoogleReaderCompatibleSubscription]?) { + func syncTaggings(_ account: Account, _ subscriptions: [ReaderAPISubscription]?) { guard let subscriptions = subscriptions else { return } @@ -712,7 +712,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } }() - let taggingsDict = subscriptions.reduce([String: [GoogleReaderCompatibleSubscription]]()) { (dict, subscription) in + let taggingsDict = subscriptions.reduce([String: [ReaderAPISubscription]]()) { (dict, subscription) in var taggedFeeds = dict // For each category that this feed belongs to, add the feed to that name in the dict @@ -834,7 +834,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } } - func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [GoogleReaderCompatibleSubscriptionChoice], completion: @escaping (Result) -> Void) { + func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [ReaderAPISubscriptionChoice], completion: @escaping (Result) -> Void) { let feedSpecifiers: [FeedSpecifier] = choices.map { choice in let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink @@ -847,18 +847,18 @@ private extension GoogleReaderCompatibleAccountDelegate { createFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion) } else { DispatchQueue.main.async { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) } } } else { DispatchQueue.main.async { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidParameter)) + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) } } } - func createFeed( account: Account, subscription sub: GoogleReaderCompatibleSubscription, name: String?, container: Container, completion: @escaping (Result) -> Void) { + func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result) -> Void) { DispatchQueue.main.async { @@ -1013,7 +1013,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func processEntries(account: Account, entries: [GoogleReaderCompatibleEntry]?, completion: @escaping (() -> Void)) { + func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping (() -> Void)) { let parsedItems = mapEntriesToParsedItems(account: account, entries: entries) let parsedMap = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ) @@ -1042,7 +1042,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } - func mapEntriesToParsedItems(account: Account, entries: [GoogleReaderCompatibleEntry]?) -> Set { + func mapEntriesToParsedItems(account: Account, entries: [ReaderAPIEntry]?) -> Set { guard let entries = entries else { return Set() @@ -1065,11 +1065,11 @@ private extension GoogleReaderCompatibleAccountDelegate { return } - let GoogleReaderCompatibleUnreadArticleIDs = Set(articleIDs.map { String($0) } ) + let unreadArticleIDs = Set(articleIDs.map { String($0) } ) let currentUnreadArticleIDs = account.fetchUnreadArticleIDs() // Mark articles as unread - let deltaUnreadArticleIDs = GoogleReaderCompatibleUnreadArticleIDs.subtracting(currentUnreadArticleIDs) + let deltaUnreadArticleIDs = unreadArticleIDs.subtracting(currentUnreadArticleIDs) let markUnreadArticles = account.fetchArticles(forArticleIDs: deltaUnreadArticleIDs) DispatchQueue.main.async { _ = account.update(markUnreadArticles, statusKey: .read, flag: false) @@ -1085,7 +1085,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } // Mark articles as read - let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(GoogleReaderCompatibleUnreadArticleIDs) + let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(unreadArticleIDs) let markReadArticles = account.fetchArticles(forArticleIDs: deltaReadArticleIDs) DispatchQueue.main.async { _ = account.update(markReadArticles, statusKey: .read, flag: true) @@ -1108,11 +1108,11 @@ private extension GoogleReaderCompatibleAccountDelegate { return } - let GoogleReaderCompatibleStarredArticleIDs = Set(articleIDs.map { String($0) } ) + let starredArticleIDs = Set(articleIDs.map { String($0) } ) let currentStarredArticleIDs = account.fetchStarredArticleIDs() // Mark articles as starred - let deltaStarredArticleIDs = GoogleReaderCompatibleStarredArticleIDs.subtracting(currentStarredArticleIDs) + let deltaStarredArticleIDs = starredArticleIDs.subtracting(currentStarredArticleIDs) let markStarredArticles = account.fetchArticles(forArticleIDs: deltaStarredArticleIDs) DispatchQueue.main.async { _ = account.update(markStarredArticles, statusKey: .starred, flag: true) @@ -1128,7 +1128,7 @@ private extension GoogleReaderCompatibleAccountDelegate { } // Mark articles as unstarred - let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(GoogleReaderCompatibleStarredArticleIDs) + let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(starredArticleIDs) let markUnstarredArticles = account.fetchArticles(forArticleIDs: deltaUnstarredArticleIDs) DispatchQueue.main.async { _ = account.update(markUnstarredArticles, statusKey: .starred, flag: false) diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift b/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift similarity index 86% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift rename to Frameworks/Account/ReaderAPI/ReaderAPICaller.swift index ec09e2976..4384af936 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleAPICaller.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleAPICaller.swift +// ReaderAPICaller.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -9,13 +9,13 @@ import Foundation import RSWeb -enum CreateGoogleReaderSubscriptionResult { - case created(GoogleReaderCompatibleSubscription) +enum CreateReaderAPISubscriptionResult { + case created(ReaderAPISubscription) case alreadySubscribed case notFound } -final class GoogleReaderCompatibleAPICaller: NSObject { +final class ReaderAPICaller: NSObject { struct ConditionalGetKeys { static let subscriptions = "subscriptions" @@ -26,16 +26,16 @@ final class GoogleReaderCompatibleAPICaller: NSObject { static let starredEntries = "starredEntries" } - enum GoogleReaderState: String { + enum ReaderState: String { case read = "user/-/state/com.google/read" case starred = "user/-/state/com.google/starred" } - enum GoogleReaderStreams: String { + enum ReaderStreams: String { case readingList = "user/-/state/com.google/reading-list" } - enum GoogleReaderEndpoints: String { + enum ReaderAPIEndpoints: String { case login = "/accounts/ClientLogin" case token = "/reader/api/0/token" case disableTag = "/reader/api/0/disable-tag" @@ -89,7 +89,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let request = URLRequest(url: endpoint.appendingPathComponent(GoogleReaderEndpoints.login.rawValue), credentials: credentials) + let request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.login.rawValue), credentials: credentials) transport.send(request: request) { result in switch result { @@ -140,7 +140,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - let request = URLRequest(url: endpoint.appendingPathComponent(GoogleReaderEndpoints.token.rawValue), credentials: credentials) + let request = URLRequest(url: endpoint.appendingPathComponent(ReaderAPIEndpoints.token.rawValue), credentials: credentials) transport.send(request: request) { result in switch result { @@ -165,14 +165,14 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func retrieveTags(completion: @escaping (Result<[GoogleReaderCompatibleTag]?, Error>) -> Void) { + func retrieveTags(completion: @escaping (Result<[ReaderAPITag]?, Error>) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.tagList.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.tagList.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -189,7 +189,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: GoogleReaderCompatibleTagContainer.self) { result in + transport.send(request: request, resultType: ReaderAPITagContainer.self) { result in switch result { case .success(let (response, wrapper)): @@ -212,7 +212,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.renameTag.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.renameTag.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -249,7 +249,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.disableTag.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.disableTag.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -277,14 +277,14 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func retrieveSubscriptions(completion: @escaping (Result<[GoogleReaderCompatibleSubscription]?, Error>) -> Void) { + func retrieveSubscriptions(completion: @escaping (Result<[ReaderAPISubscription]?, Error>) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionList.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionList.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -301,7 +301,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: GoogleReaderCompatibleSubscriptionContainer.self) { result in + transport.send(request: request, resultType: ReaderAPISubscriptionContainer.self) { result in switch result { case .success(let (response, container)): @@ -315,7 +315,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func createSubscription(url: String, completion: @escaping (Result) -> Void) { + func createSubscription(url: String, completion: @escaping (Result) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return @@ -324,7 +324,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -344,7 +344,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let postData = "T=\(token)".data(using: String.Encoding.utf8) - self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleQuickAddResult.self, completion: { (result) in + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIQuickAddResult.self, completion: { (result) in switch result { case .success(let (_, subResult)): @@ -405,7 +405,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -440,7 +440,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -476,7 +476,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -513,7 +513,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionEdit.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") @@ -540,10 +540,10 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } } - func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([GoogleReaderCompatibleEntry]?), Error>) -> Void) { + func retrieveEntries(articleIDs: [String], completion: @escaping (Result<([ReaderAPIEntry]?), Error>) -> Void) { guard !articleIDs.isEmpty else { - completion(.success(([GoogleReaderCompatibleEntry]()))) + completion(.success(([ReaderAPIEntry]()))) return } @@ -556,7 +556,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.contents.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.contents.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -567,11 +567,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) - self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIEntryWrapper.self, completion: { (result) in switch result { case .success(let (_, entryWrapper)): guard let entryWrapper = entryWrapper else { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + completion(.failure(ReaderAPIAccountDelegateError.invalidResponse)) return } @@ -589,7 +589,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func retrieveEntries(feedID: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { + func retrieveEntries(feedID: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) { let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() @@ -599,7 +599,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -617,7 +617,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: nil) - transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in + transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in switch result { case .success(let (_, unreadEntries)): @@ -650,7 +650,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func retrieveEntries(completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?, Int?), Error>) -> Void) { + func retrieveEntries(completion: @escaping (Result<([ReaderAPIEntry]?, String?, Int?), Error>) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) @@ -668,7 +668,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let sinceString = since.timeIntervalSince1970 // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -677,8 +677,8 @@ final class GoogleReaderCompatibleAPICaller: NSObject { URLQueryItem(name: "o", value: String(sinceString)), URLQueryItem(name: "n", value: "10000"), URLQueryItem(name: "output", value: "json"), - URLQueryItem(name: "xt", value: GoogleReaderState.read.rawValue), - URLQueryItem(name: "s", value: GoogleReaderStreams.readingList.rawValue) + URLQueryItem(name: "xt", value: ReaderState.read.rawValue), + URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue) ] guard let callURL = components.url else { @@ -689,13 +689,13 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - self.transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in + self.transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in switch result { case .success(let (_, entries)): guard let entries = entries else { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + completion(.failure(ReaderAPIAccountDelegateError.invalidResponse)) return } @@ -703,7 +703,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.contents.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.contents.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -716,11 +716,11 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) - self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: GoogleReaderCompatibleEntryWrapper.self, completion: { (result) in + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIEntryWrapper.self, completion: { (result) in switch result { case .success(let (response, entryWrapper)): guard let entryWrapper = entryWrapper else { - completion(.failure(GoogleReaderCompatibleAccountDelegateError.invalidResponse)) + completion(.failure(ReaderAPIAccountDelegateError.invalidResponse)) return } @@ -748,7 +748,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } } - func retrieveEntries(page: String, completion: @escaping (Result<([GoogleReaderCompatibleEntry]?, String?), Error>) -> Void) { + func retrieveEntries(page: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) { guard let url = URL(string: page), var callComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { completion(.success((nil, nil))) @@ -758,7 +758,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended")) let request = URLRequest(url: callComponents.url!, credentials: credentials) - transport.send(request: request, resultType: [GoogleReaderCompatibleEntry].self) { result in + transport.send(request: request, resultType: [ReaderAPIEntry].self) { result in switch result { case .success(let (response, entries)): @@ -783,15 +783,15 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } // Add query string for getting JSON (probably should break this out as I will be doing it a lot) - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } components.queryItems = [ - URLQueryItem(name: "s", value: GoogleReaderStreams.readingList.rawValue), + URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue), URLQueryItem(name: "n", value: "10000"), - URLQueryItem(name: "xt", value: GoogleReaderState.read.rawValue), + URLQueryItem(name: "xt", value: ReaderState.read.rawValue), URLQueryItem(name: "output", value: "json") ] @@ -803,7 +803,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in + transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in switch result { case .success(let (response, unreadEntries)): @@ -825,7 +825,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { } - func updateStateToEntries(entries: [Int], state: GoogleReaderState, add: Bool, completion: @escaping (Result) -> Void) { + func updateStateToEntries(entries: [Int], state: ReaderState, add: Bool, completion: @escaping (Result) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return @@ -835,7 +835,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { switch result { case .success(let token): // Do POST asking for data about all the new articles - var request = URLRequest(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.editTag.rawValue), credentials: self.credentials) + var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.editTag.rawValue), credentials: self.credentials) request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") request.httpMethod = "POST" @@ -889,7 +889,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { return } - guard var components = URLComponents(url: baseURL.appendingPathComponent(GoogleReaderEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue), resolvingAgainstBaseURL: false) else { completion(.failure(TransportError.noURL)) return } @@ -908,7 +908,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] let request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - transport.send(request: request, resultType: GoogleReaderCompatibleReferenceWrapper.self) { result in + transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in switch result { case .success(let (response, unreadEntries)): @@ -936,7 +936,7 @@ final class GoogleReaderCompatibleAPICaller: NSObject { // MARK: Private -extension GoogleReaderCompatibleAPICaller { +extension ReaderAPICaller { func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { if var conditionalGet = accountMetadata?.conditionalGetInfo { diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift b/Frameworks/Account/ReaderAPI/ReaderAPIEntry.swift similarity index 83% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift rename to Frameworks/Account/ReaderAPI/ReaderAPIEntry.swift index 34bf05c95..d8f85ca6f 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleEntry.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIEntry.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleArticle.swift +// ReaderAPIArticle.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -10,10 +10,10 @@ import Foundation import RSParser import RSCore -struct GoogleReaderCompatibleEntryWrapper: Codable { +struct ReaderAPIEntryWrapper: Codable { let id: String let updated: Int - let entries: [GoogleReaderCompatibleEntry] + let entries: [ReaderAPIEntry] enum CodingKeys: String, CodingKey { @@ -47,7 +47,7 @@ struct GoogleReaderCompatibleEntryWrapper: Codable { } } */ -struct GoogleReaderCompatibleEntry: Codable { +struct ReaderAPIEntry: Codable { let articleID: String let title: String? @@ -56,10 +56,10 @@ struct GoogleReaderCompatibleEntry: Codable { let crawledTimestamp: String? let timestampUsec: String? - let summary: GoogleReaderCompatibleArticleSummary - let alternates: [GoogleReaderCompatibleAlternateLocation] + let summary: ReaderAPIArticleSummary + let alternates: [ReaderAPIAlternateLocation] let categories: [String] - let origin: GoogleReaderCompatibleEntryOrigin + let origin: ReaderAPIEntryOrigin enum CodingKeys: String, CodingKey { case articleID = "id" @@ -99,7 +99,7 @@ struct GoogleReaderCompatibleEntry: Codable { } } -struct GoogleReaderCompatibleArticleSummary: Codable { +struct ReaderAPIArticleSummary: Codable { let content: String? enum CodingKeys: String, CodingKey { @@ -107,7 +107,7 @@ struct GoogleReaderCompatibleArticleSummary: Codable { } } -struct GoogleReaderCompatibleAlternateLocation: Codable { +struct ReaderAPIAlternateLocation: Codable { let url: String? enum CodingKeys: String, CodingKey { @@ -116,7 +116,7 @@ struct GoogleReaderCompatibleAlternateLocation: Codable { } -struct GoogleReaderCompatibleEntryOrigin: Codable { +struct ReaderAPIEntryOrigin: Codable { let streamId: String? let title: String? diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift b/Frameworks/Account/ReaderAPI/ReaderAPISubscription.swift similarity index 71% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift rename to Frameworks/Account/ReaderAPI/ReaderAPISubscription.swift index 6f5469225..3072656ec 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleSubscription.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPISubscription.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleFeed.swift +// ReaderAPIFeed.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -19,7 +19,7 @@ import RSParser */ -struct GoogleReaderCompatibleQuickAddResult: Codable { +struct ReaderAPIQuickAddResult: Codable { let numResults: Int let error: String? let streamId: String? @@ -31,8 +31,8 @@ struct GoogleReaderCompatibleQuickAddResult: Codable { } } -struct GoogleReaderCompatibleSubscriptionContainer: Codable { - let subscriptions: [GoogleReaderCompatibleSubscription] +struct ReaderAPISubscriptionContainer: Codable { + let subscriptions: [ReaderAPISubscription] enum CodingKeys: String, CodingKey { case subscriptions = "subscriptions" @@ -55,10 +55,10 @@ struct GoogleReaderCompatibleSubscriptionContainer: Codable { } */ -struct GoogleReaderCompatibleSubscription: Codable { +struct ReaderAPISubscription: Codable { let feedID: String let name: String? - let categories: [GoogleReaderCompatibleCategory] + let categories: [ReaderAPICategory] let url: String let homePageURL: String? let iconURL: String? @@ -74,7 +74,7 @@ struct GoogleReaderCompatibleSubscription: Codable { } -struct GoogleReaderCompatibleCategory: Codable { +struct ReaderAPICategory: Codable { let categoryId: String let categoryLabel: String @@ -84,21 +84,14 @@ struct GoogleReaderCompatibleCategory: Codable { } } -struct GoogleReaderCompatibleCreateSubscription: Codable { +struct ReaderAPICreateSubscription: Codable { let feedURL: String enum CodingKeys: String, CodingKey { case feedURL = "feed_url" } } -struct GoogleReaderCompatibleUpdateSubscription: Codable { - let title: String - enum CodingKeys: String, CodingKey { - case title - } -} - -struct GoogleReaderCompatibleSubscriptionChoice: Codable { +struct ReaderAPISubscriptionChoice: Codable { let name: String? let url: String diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift b/Frameworks/Account/ReaderAPI/ReaderAPITag.swift similarity index 66% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift rename to Frameworks/Account/ReaderAPI/ReaderAPITag.swift index 538fdab2f..7f827e1a6 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTag.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPITag.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleTag.swift +// ReaderAPICompatibleTag.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -8,15 +8,15 @@ import Foundation -struct GoogleReaderCompatibleTagContainer: Codable { - let tags: [GoogleReaderCompatibleTag] +struct ReaderAPITagContainer: Codable { + let tags: [ReaderAPITag] enum CodingKeys: String, CodingKey { case tags = "tags" } } -struct GoogleReaderCompatibleTag: Codable { +struct ReaderAPITag: Codable { let tagID: String let type: String? diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift b/Frameworks/Account/ReaderAPI/ReaderAPITagging.swift similarity index 76% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift rename to Frameworks/Account/ReaderAPI/ReaderAPITagging.swift index e35ee0e3b..d907ca445 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleTagging.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPITagging.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleTagging.swift +// ReaderAPICompatibleTagging.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -8,7 +8,7 @@ import Foundation -struct GoogleReaderCompatibleTagging: Codable { +struct ReaderAPITagging: Codable { let taggingID: Int let feedID: Int @@ -22,7 +22,7 @@ struct GoogleReaderCompatibleTagging: Codable { } -struct GoogleReaderCompatibleCreateTagging: Codable { +struct ReaderAPICreateTagging: Codable { let feedID: Int let name: String diff --git a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift b/Frameworks/Account/ReaderAPI/ReaderAPIUnreadEntry.swift similarity index 61% rename from Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift rename to Frameworks/Account/ReaderAPI/ReaderAPIUnreadEntry.swift index c4e42f8c0..a69909c21 100644 --- a/Frameworks/Account/GoogleReaderCompatible/GoogleReaderCompatibleUnreadEntry.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIUnreadEntry.swift @@ -1,5 +1,5 @@ // -// GoogleReaderCompatibleUnreadEntry.swift +// ReaderAPIUnreadEntry.swift // Account // // Created by Jeremy Beker on 5/28/19. @@ -8,15 +8,15 @@ import Foundation -struct GoogleReaderCompatibleReferenceWrapper: Codable { - let itemRefs: [GoogleReaderCompatibleReference] +struct ReaderAPIReferenceWrapper: Codable { + let itemRefs: [ReaderAPIReference] enum CodingKeys: String, CodingKey { case itemRefs = "itemRefs" } } -struct GoogleReaderCompatibleReference: Codable { +struct ReaderAPIReference: Codable { let itemId: String diff --git a/Mac/Preferences/Accounts/AccountsAddViewController.swift b/Mac/Preferences/Accounts/AccountsAddViewController.swift index cf091fff1..64d94eafc 100644 --- a/Mac/Preferences/Accounts/AccountsAddViewController.swift +++ b/Mac/Preferences/Accounts/AccountsAddViewController.swift @@ -64,7 +64,7 @@ extension AccountsAddViewController: NSTableViewDelegate { cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin") cell.accountImageView?.image = AppAssets.accountFeedbin case 2: - cell.accountNameLabel?.stringValue = NSLocalizedString("Google Reader API", comment: "Google Reader API") + cell.accountNameLabel?.stringValue = NSLocalizedString("Reader", comment: "Reader") cell.accountImageView?.image = AppAssets.accountLocal default: break @@ -91,9 +91,9 @@ extension AccountsAddViewController: NSTableViewDelegate { accountsFeedbinWindowController.runSheetOnWindow(self.view.window!) accountsAddWindowController = accountsFeedbinWindowController case 2: - let accountsGoogleReaderCompatibleWindowController = AccountsGoogleReaderCompatibleWindowController() - accountsGoogleReaderCompatibleWindowController.runSheetOnWindow(self.view.window!) - accountsAddWindowController = accountsGoogleReaderCompatibleWindowController + let accountsReaderAPIWindowController = AccountsReaderAPIWindowController() + accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) + accountsAddWindowController = accountsReaderAPIWindowController default: break } diff --git a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift index 9edd0539e..84673da8b 100644 --- a/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift +++ b/Mac/Preferences/Accounts/AccountsPreferencesViewController.swift @@ -107,7 +107,7 @@ extension AccountsPreferencesViewController: NSTableViewDelegate { cell.imageView?.image = AppAssets.accountLocal case .feedbin: cell.imageView?.image = NSImage(named: "accountFeedbin") - case .googleReaderCompatible: + case .googleReaderAPI: cell.imageView?.image = AppAssets.accountLocal default: break diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib b/Mac/Preferences/Accounts/AccountsReaderAPI.xib similarity index 97% rename from Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib rename to Mac/Preferences/Accounts/AccountsReaderAPI.xib index 6e2f71ba7..1da180f56 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatible.xib +++ b/Mac/Preferences/Accounts/AccountsReaderAPI.xib @@ -6,7 +6,7 @@ - + @@ -23,13 +23,13 @@ - - - + + + - + @@ -40,8 +40,8 @@ - - + + diff --git a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift similarity index 92% rename from Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift rename to Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift index b70b481b1..ac74eef50 100644 --- a/Mac/Preferences/Accounts/AccountsGoogleReaderCompatibleWindowController.swift +++ b/Mac/Preferences/Accounts/AccountsReaderAPIWindowController.swift @@ -10,7 +10,7 @@ import AppKit import Account import RSWeb -class AccountsGoogleReaderCompatibleWindowController: NSWindowController { +class AccountsReaderAPIWindowController: NSWindowController { @IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var usernameTextField: NSTextField! @@ -24,7 +24,7 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { private weak var hostWindow: NSWindow? convenience init() { - self.init(windowNibName: NSNib.Name("AccountsGoogleReaderCompatible")) + self.init(windowNibName: NSNib.Name("AccountsReaderAPI")) } override func windowDidLoad() { @@ -71,7 +71,7 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { } let credentials = Credentials.googleBasicLogin(username: usernameTextField.stringValue, password: passwordTextField.stringValue) - Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials, endpoint: apiURL) { [weak self] result in + Account.validateCredentials(type: .googleReaderAPI, credentials: credentials, endpoint: apiURL) { [weak self] result in guard let self = self else { return } @@ -89,7 +89,7 @@ class AccountsGoogleReaderCompatibleWindowController: NSWindowController { var newAccount = false if self.account == nil { - self.account = AccountManager.shared.createAccount(type: .googleReaderCompatible) + self.account = AccountManager.shared.createAccount(type: .googleReaderAPI) newAccount = true } diff --git a/Mac/Scriptability/Account+Scriptability.swift b/Mac/Scriptability/Account+Scriptability.swift index 88cf8eb3a..445b7bc16 100644 --- a/Mac/Scriptability/Account+Scriptability.swift +++ b/Mac/Scriptability/Account+Scriptability.swift @@ -142,7 +142,7 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta osType = "FWrg" case .newsBlur: osType = "NBlr" - case .googleReaderCompatible: + case .googleReaderAPI: osType = "Grdr" } return osType.fourCharCode() diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index bbd4ae6b3..52c120d44 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -155,9 +155,9 @@ 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */; }; 51F85BFB2275D85000C787DC /* Array-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFA2275D85000C787DC /* Array-Extensions.swift */; }; 51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */; }; - 557EE1AE22B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */; }; - 55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */; }; - 55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */; }; + 557EE1AE22B6F4E1004206FA /* SettingsReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */; }; + 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; + 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; @@ -753,9 +753,9 @@ 51F85BF82274AA7B00C787DC /* UIBarButtonItem-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIBarButtonItem-Extensions.swift"; sourceTree = ""; }; 51F85BFA2275D85000C787DC /* Array-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Array-Extensions.swift"; sourceTree = ""; }; 51F85BFC2275DCA800C787DC /* SingleLineUILabelSizer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SingleLineUILabelSizer.swift; sourceTree = ""; }; - 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsGoogleReaderCompatibleAccountView.swift; sourceTree = ""; }; - 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsGoogleReaderCompatible.xib; sourceTree = ""; }; - 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsGoogleReaderCompatibleWindowController.swift; sourceTree = ""; }; + 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsReaderAPIAccountView.swift; sourceTree = ""; }; + 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; + 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariExtensionHandler.swift; sourceTree = ""; }; @@ -1066,7 +1066,7 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( - 557EE1A522B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift */, + 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */, 510D708122B041CC004E8F65 /* SettingsAccountLabelView.swift */, 510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */, 51F772EC22B2789B0087D9D1 /* SettingsDetailAccountView.swift */, @@ -1669,8 +1669,8 @@ 84C9FC6F22629E1200D921D6 /* Accounts */ = { isa = PBXGroup; children = ( - 55E15BC1229D65A900D6602A /* AccountsGoogleReaderCompatible.xib */, - 55E15BCA229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift */, + 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */, + 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */, 84C9FC7022629E1200D921D6 /* AccountsTableViewBackgroundView.swift */, 84C9FC7122629E1200D921D6 /* AccountsControlsBackgroundView.swift */, 84C9FC7222629E1200D921D6 /* AccountsPreferencesViewController.swift */, @@ -2265,7 +2265,7 @@ 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */, 8405DDA222168920008CE1BF /* TimelineTableView.xib in Resources */, 8483630E2262A3FE00DA1D35 /* MainWindow.storyboard in Resources */, - 55E15BCB229D65A900D6602A /* AccountsGoogleReaderCompatible.xib in Resources */, + 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */, 84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */, 84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */, 84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */, @@ -2390,7 +2390,7 @@ 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 515436882291D75D005E1CDF /* AddLocalAccountViewController.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, - 557EE1AE22B6F4E1004206FA /* SettingsGoogleReaderCompatibleAccountView.swift in Sources */, + 557EE1AE22B6F4E1004206FA /* SettingsReaderAPIAccountView.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, @@ -2487,7 +2487,7 @@ 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */, 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */, 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */, - 55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */, + 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */, 5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */, 84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */, 5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */, diff --git a/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift b/iOS/Settings/SettingsReaderAPIAccountView.swift similarity index 88% rename from iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift rename to iOS/Settings/SettingsReaderAPIAccountView.swift index 6ecde08dd..c21dd7891 100644 --- a/iOS/Settings/SettingsGoogleReaderCompatibleAccountView.swift +++ b/iOS/Settings/SettingsReaderAPIAccountView.swift @@ -1,8 +1,8 @@ // -// SettingsGoogleReaderCompatibleAccountView.swift +// SettingsReaderAPIAccountView.swift // NetNewsWire-iOS // -// Created by Maurice Parker on 6/11/19. +// Created by Jeremy Beker on 5/28/2019. // Copyright © 2019 Ranchero Software. All rights reserved. // @@ -11,7 +11,7 @@ import Combine import Account import RSWeb -struct SettingsGoogleReaderCompatibleAccountView : View { +struct SettingsReaderAPIAccountView : View { @Environment(\.isPresented) private var isPresented @ObjectBinding var viewModel: ViewModel @State var busy: Bool = false @@ -83,7 +83,7 @@ struct SettingsGoogleReaderCompatibleAccountView : View { return } - Account.validateCredentials(type: .googleReaderCompatible, credentials: credentials, endpoint: apiURL) { result in + Account.validateCredentials(type: .googleReaderAPI, credentials: credentials, endpoint: apiURL) { result in self.busy = false @@ -95,7 +95,7 @@ struct SettingsGoogleReaderCompatibleAccountView : View { var newAccount = false let workAccount: Account if self.viewModel.account == nil { - workAccount = AccountManager.shared.createAccount(type: .googleReaderCompatible) + workAccount = AccountManager.shared.createAccount(type: .googleReaderAPI) newAccount = true } else { workAccount = self.viewModel.account! @@ -179,9 +179,9 @@ struct SettingsGoogleReaderCompatibleAccountView : View { } #if DEBUG -struct SettingsGoogleReaderCompatibleAccountView_Previews : PreviewProvider { +struct SettingsReaderAPIAccountView_Previews : PreviewProvider { static var previews: some View { - SettingsGoogleReaderCompatibleAccountView(viewModel: SettingsGoogleReaderCompatibleAccountView.ViewModel()) + SettingsReaderAPIAccountView(viewModel: SettingsReaderAPIAccountView.ViewModel()) } } #endif