diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyAPICaller.swift b/Modules/Account/Sources/Account/Feedly/FeedlyAPICaller.swift index e84b801bd..31d3d2d3d 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyAPICaller.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyAPICaller.swift @@ -14,19 +14,19 @@ import Secrets protocol FeedlyAPICallerDelegate: AnyObject { /// Implemented by the `FeedlyAccountDelegate` reauthorize the client with a fresh OAuth token so the client can retry the unauthorized request. /// Pass `true` to the completion handler if the failing request should be retried with a fresh token or `false` if the unauthorized request should complete with the original failure error. - func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) + func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> Void) } final class FeedlyAPICaller { - + enum API { case sandbox case cloud - + var baseUrlComponents: URLComponents { var components = URLComponents() components.scheme = "https" - switch self{ + switch self { case .sandbox: // https://groups.google.com/forum/#!topic/feedly-cloud/WwQWMgDmOuw components.host = "sandbox7.feedly.com" @@ -36,7 +36,7 @@ final class FeedlyAPICaller { } return components } - + var oauthAuthorizationClient: OAuthAuthorizationClient { switch self { case .sandbox: @@ -46,83 +46,83 @@ final class FeedlyAPICaller { } } } - + private let transport: Transport private let baseUrlComponents: URLComponents private let uriComponentAllowed: CharacterSet - + init(transport: Transport, api: API) { self.transport = transport self.baseUrlComponents = api.baseUrlComponents - + var urlHostAllowed = CharacterSet.urlHostAllowed urlHostAllowed.remove("+") uriComponentAllowed = urlHostAllowed } - + weak var delegate: FeedlyAPICallerDelegate? - + var credentials: Credentials? - + var server: String? { return baseUrlComponents.host } - + func cancelAll() { transport.cancelAll() } - + private var isSuspended = false - + /// Cancels all pending requests rejects any that come in later func suspend() { transport.cancelAll() isSuspended = true } - + func resume() { isSuspended = false } - + func send(request: URLRequest, resultType: R.Type, dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601, keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) { transport.send(request: request, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding) { [weak self] result in assert(Thread.isMainThread) - + switch result { case .success: completion(result) case .failure(let error): switch error { case TransportError.httpError(let statusCode) where statusCode == 401: - + assert(self == nil ? true : self?.delegate != nil, "Check the delegate is set to \(FeedlyAccountDelegate.self).") - + guard let self = self, let delegate = self.delegate else { completion(result) return } - + /// Capture the credentials before the reauthorization to check for a change. let credentialsBefore = self.credentials - + delegate.reauthorizeFeedlyAPICaller(self) { [weak self] isReauthorizedAndShouldRetry in assert(Thread.isMainThread) - + guard isReauthorizedAndShouldRetry, let self = self else { completion(result) return } - + // Check for a change. Not only would it help debugging, but it'll also catch an infinitely recursive attempt to refresh. guard let accessToken = self.credentials?.secret, accessToken != credentialsBefore?.secret else { assertionFailure("Could not update the request with a new OAuth token. Did \(String(describing: self.delegate)) set them on \(self)?") completion(result) return } - + var reauthorizedRequest = request reauthorizedRequest.setValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + self.send(request: reauthorizedRequest, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding, completion: completion) } default: @@ -131,14 +131,14 @@ final class FeedlyAPICaller { } } } - - func importOpml(_ opmlData: Data, completion: @escaping (Result) -> ()) { + + func importOpml(_ opmlData: Data, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -146,18 +146,18 @@ final class FeedlyAPICaller { } var components = baseUrlComponents components.path = "/v3/opml" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("text/xml", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) request.httpBody = opmlData - + send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, _)): @@ -171,14 +171,14 @@ final class FeedlyAPICaller { } } } - - func createCollection(named label: String, completion: @escaping (Result) -> ()) { + + func createCollection(named label: String, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -186,17 +186,17 @@ final class FeedlyAPICaller { } var components = baseUrlComponents components.path = "/v3/collections" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + do { struct CreateCollectionBody: Encodable { var label: String @@ -209,7 +209,7 @@ final class FeedlyAPICaller { completion(.failure(error)) } } - + send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, collections)): @@ -223,14 +223,14 @@ final class FeedlyAPICaller { } } } - - func renameCollection(with id: String, to name: String, completion: @escaping (Result) -> ()) { + + func renameCollection(with id: String, to name: String, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -238,17 +238,17 @@ final class FeedlyAPICaller { } var components = baseUrlComponents components.path = "/v3/collections" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + do { struct RenameCollectionBody: Encodable { var id: String @@ -262,7 +262,7 @@ final class FeedlyAPICaller { completion(.failure(error)) } } - + send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, collections)): @@ -276,18 +276,18 @@ final class FeedlyAPICaller { } } } - + private func encodeForURLPath(_ pathComponent: String) -> String? { return pathComponent.addingPercentEncoding(withAllowedCharacters: uriComponentAllowed) } - - func deleteCollection(with id: String, completion: @escaping (Result) -> ()) { + + func deleteCollection(with id: String, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -300,17 +300,17 @@ final class FeedlyAPICaller { } var components = baseUrlComponents components.percentEncodedPath = "/v3/collections/\(encodedId)" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "DELETE" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: Optional.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, _)): @@ -324,14 +324,14 @@ final class FeedlyAPICaller { } } } - - func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completion: @escaping (Result) -> ()) { + + func removeFeed(_ feedId: String, fromCollectionWith collectionId: String, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -343,20 +343,20 @@ final class FeedlyAPICaller { completion(.failure(FeedlyAccountDelegateError.unexpectedResourceId(collectionId))) } } - + var components = baseUrlComponents components.percentEncodedPath = "/v3/collections/\(encodedCollectionId)/feeds/.mdelete" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "DELETE" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + do { struct RemovableFeed: Encodable { let id: String @@ -369,7 +369,7 @@ final class FeedlyAPICaller { completion(.failure(error)) } } - + // `resultType` is optional because the Feedly API has gone from returning an array of removed feeds to returning `null`. // https://developer.feedly.com/v3/collections/#remove-multiple-feeds-from-a-personal-collection send(request: request, resultType: Optional<[FeedlyFeed]>.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in @@ -388,14 +388,14 @@ final class FeedlyAPICaller { } extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { - - func addFeed(with feedId: FeedlyFeedResourceId, title: String? = nil, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) { + + func addFeed(with feedId: FeedlyFeedResourceId, title: String? = nil, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -409,17 +409,17 @@ extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { } var components = baseUrlComponents components.percentEncodedPath = "/v3/collections/\(encodedId)/feeds" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "PUT" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + do { struct AddFeedBody: Encodable { var id: String @@ -433,7 +433,7 @@ extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { completion(.failure(error)) } } - + send(request: request, resultType: [FeedlyFeed].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success((_, let collectionFeeds)): @@ -450,45 +450,45 @@ extension FeedlyAPICaller: FeedlyAddFeedToCollectionService { } extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { - + static func authorizationCodeUrlRequest(for request: OAuthAuthorizationRequest, baseUrlComponents: URLComponents) -> URLRequest { var components = baseUrlComponents components.path = "/v3/auth/auth" components.queryItems = request.queryItems - + guard let url = components.url else { assert(components.scheme != nil) assert(components.host != nil) fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - + return request } - + typealias AccessTokenResponse = FeedlyOAuthAccessTokenResponse - - func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> ()) { + + func requestAccessToken(_ authorizationRequest: OAuthAccessTokenRequest, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + var components = baseUrlComponents components.path = "/v3/auth/token" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - + do { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase @@ -499,7 +499,7 @@ extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { } return } - + send(request: request, resultType: AccessTokenResponse.self, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, tokenResponse)): @@ -516,26 +516,26 @@ extension FeedlyAPICaller: OAuthAuthorizationCodeGrantRequesting { } extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting { - - func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> ()) { + + func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + var components = baseUrlComponents components.path = "/v3/auth/token" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - + do { let encoder = JSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase @@ -546,7 +546,7 @@ extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting { } return } - + send(request: request, resultType: AccessTokenResponse.self, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, tokenResponse)): @@ -563,14 +563,14 @@ extension FeedlyAPICaller: OAuthAcessTokenRefreshRequesting { } extension FeedlyAPICaller: FeedlyGetCollectionsService { - - func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) { + + func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -578,16 +578,16 @@ extension FeedlyAPICaller: FeedlyGetCollectionsService { } var components = baseUrlComponents components.path = "/v3/collections" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: [FeedlyCollection].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, collections)): @@ -604,58 +604,58 @@ extension FeedlyAPICaller: FeedlyGetCollectionsService { } extension FeedlyAPICaller: FeedlyGetStreamContentsService { - - func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { + + func getStreamContents(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) } } - + var components = baseUrlComponents components.path = "/v3/streams/contents" - + var queryItems = [URLQueryItem]() - + if let date = newerThan { let value = String(Int(date.timeIntervalSince1970 * 1000)) let queryItem = URLQueryItem(name: "newerThan", value: value) queryItems.append(queryItem) } - + if let flag = unreadOnly { let value = flag ? "true" : "false" let queryItem = URLQueryItem(name: "unreadOnly", value: value) queryItems.append(queryItem) } - + if let value = continuation, !value.isEmpty { let queryItem = URLQueryItem(name: "continuation", value: value) queryItems.append(queryItem) } - + queryItems.append(contentsOf: [ URLQueryItem(name: "count", value: "1000"), - URLQueryItem(name: "streamId", value: resource.id), + URLQueryItem(name: "streamId", value: resource.id) ]) - + components.queryItems = queryItems - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: FeedlyStream.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, collections)): @@ -672,58 +672,58 @@ extension FeedlyAPICaller: FeedlyGetStreamContentsService { } extension FeedlyAPICaller: FeedlyGetStreamIdsService { - - func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) { + + func getStreamIds(for resource: FeedlyResourceId, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) } } - + var components = baseUrlComponents components.path = "/v3/streams/ids" var queryItems = [URLQueryItem]() - + if let date = newerThan { let value = String(Int(date.timeIntervalSince1970 * 1000)) let queryItem = URLQueryItem(name: "newerThan", value: value) queryItems.append(queryItem) } - + if let flag = unreadOnly { let value = flag ? "true" : "false" let queryItem = URLQueryItem(name: "unreadOnly", value: value) queryItems.append(queryItem) } - + if let value = continuation, !value.isEmpty { let queryItem = URLQueryItem(name: "continuation", value: value) queryItems.append(queryItem) } - + queryItems.append(contentsOf: [ URLQueryItem(name: "count", value: "10000"), - URLQueryItem(name: "streamId", value: resource.id), + URLQueryItem(name: "streamId", value: resource.id) ]) - + components.queryItems = queryItems - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: FeedlyStreamIds.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, collections)): @@ -740,29 +740,29 @@ extension FeedlyAPICaller: FeedlyGetStreamIdsService { } extension FeedlyAPICaller: FeedlyGetEntriesService { - - func getEntries(for ids: Set, completion: @escaping (Result<[FeedlyEntry], Error>) -> ()) { + + func getEntries(for ids: Set, completion: @escaping (Result<[FeedlyEntry], Error>) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) } } - + var components = baseUrlComponents components.path = "/v3/entries/.mget" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) - + do { let body = Array(ids) let encoder = JSONEncoder() @@ -773,12 +773,12 @@ extension FeedlyAPICaller: FeedlyGetEntriesService { completion(.failure(error)) } } - + request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: [FeedlyEntry].self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, entries)): @@ -795,20 +795,20 @@ extension FeedlyAPICaller: FeedlyGetEntriesService { } extension FeedlyAPICaller: FeedlyMarkArticlesService { - + private struct MarkerEntriesBody: Encodable { let type = "entries" var action: String var entryIds: [String] } - - func mark(_ articleIds: Set, as action: FeedlyMarkAction, completion: @escaping (Result) -> ()) { + + func mark(_ articleIds: Set, as action: FeedlyMarkAction, completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -816,23 +816,23 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { } var components = baseUrlComponents components.path = "/v3/markers" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + let articleIdChunks = Array(articleIds).chunked(into: 300) let dispatchGroup = DispatchGroup() - var groupError: Error? = nil + var groupError: Error? for articleIdChunk in articleIdChunks { - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + do { let body = MarkerEntriesBody(action: action.actionValue, entryIds: Array(articleIdChunk)) let encoder = JSONEncoder() @@ -843,7 +843,7 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { completion(.failure(error)) } } - + dispatchGroup.enter() send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { @@ -857,7 +857,7 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { dispatchGroup.leave() } } - + dispatchGroup.notify(queue: .main) { if let groupError = groupError { completion(.failure(groupError)) @@ -869,34 +869,33 @@ extension FeedlyAPICaller: FeedlyMarkArticlesService { } extension FeedlyAPICaller: FeedlySearchService { - - func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result) -> ()) { - + + func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result) -> Void) { + guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + var components = baseUrlComponents components.path = "/v3/search/feeds" - + components.queryItems = [ URLQueryItem(name: "query", value: query), URLQueryItem(name: "count", value: String(count)), URLQueryItem(name: "locale", value: locale) ] - - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "GET" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") - + send(request: request, resultType: FeedlyFeedsSearchResponse.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (_, searchResponse)): @@ -913,14 +912,14 @@ extension FeedlyAPICaller: FeedlySearchService { } extension FeedlyAPICaller: FeedlyLogoutService { - - func logout(completion: @escaping (Result) -> ()) { + + func logout(completion: @escaping (Result) -> Void) { guard !isSuspended else { return DispatchQueue.main.async { completion(.failure(TransportError.suspended)) } } - + guard let accessToken = credentials?.secret else { return DispatchQueue.main.async { completion(.failure(CredentialsError.incompleteCredentials)) @@ -928,17 +927,17 @@ extension FeedlyAPICaller: FeedlyLogoutService { } var components = baseUrlComponents components.path = "/v3/auth/logout" - + guard let url = components.url else { fatalError("\(components) does not produce a valid URL.") } - + var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: HTTPRequestHeader.contentType) request.addValue("application/json", forHTTPHeaderField: "Accept-Type") request.addValue("OAuth \(accessToken)", forHTTPHeaderField: HTTPRequestHeader.authorization) - + send(request: request, resultType: String.self, dateDecoding: .millisecondsSince1970, keyDecoding: .convertFromSnakeCase) { result in switch result { case .success(let (httpResponse, _)): diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate+OAuth.swift b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate+OAuth.swift index 9e29bbbe2..e8e490ea7 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate+OAuth.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate+OAuth.swift @@ -25,9 +25,9 @@ public struct FeedlyOAuthAccessTokenResponse: Decodable, OAuthAccessTokenRespons } extension FeedlyAccountDelegate: OAuthAuthorizationGranting { - + private static let oauthAuthorizationGrantScope = "https://cloud.feedly.com/subscriptions" - + static func oauthAuthorizationCodeGrantRequest() -> URLRequest { let client = environment.oauthAuthorizationClient let authorizationRequest = OAuthAuthorizationRequest(clientId: client.id, @@ -37,8 +37,8 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { let baseURLComponents = environment.baseUrlComponents return FeedlyAPICaller.authorizationCodeUrlRequest(for: authorizationRequest, baseUrlComponents: baseURLComponents) } - - static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result) -> ()) { + + static func requestOAuthAccessToken(with response: OAuthAuthorizationResponse, transport: Transport, completion: @escaping (Result) -> Void) { let client = environment.oauthAuthorizationClient let request = OAuthAccessTokenRequest(authorizationResponse: response, scope: oauthAuthorizationGrantScope, @@ -48,18 +48,18 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { switch result { case .success(let response): let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) - + let refreshToken: Credentials? = { guard let token = response.refreshToken else { return nil } return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) }() - + let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) - + completion(.success(grant)) - + case .failure(let error): completion(.failure(error)) } @@ -68,25 +68,25 @@ extension FeedlyAccountDelegate: OAuthAuthorizationGranting { } extension FeedlyAccountDelegate: OAuthAccessTokenRefreshing { - func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) { + func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> Void) { let request = OAuthRefreshAccessTokenRequest(refreshToken: refreshToken, scope: nil, client: client) - + caller.refreshAccessToken(request) { result in switch result { case .success(let response): let accessToken = Credentials(type: .oauthAccessToken, username: response.id, secret: response.accessToken) - + let refreshToken: Credentials? = { guard let token = response.refreshToken else { return nil } return Credentials(type: .oauthRefreshToken, username: response.id, secret: token) }() - + let grant = OAuthAuthorizationGrant(accessToken: accessToken, refreshToken: refreshToken) - + completion(.success(grant)) - + case .failure(let error): completion(.failure(error)) } diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift index a46176d1a..1ceea9365 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegate.swift @@ -28,15 +28,15 @@ final class FeedlyAccountDelegate: AccountDelegate { // TODO: Kiel, if you decide not to support OPML import you will have to disallow it in the behaviors // See https://developer.feedly.com/v3/opml/ var behaviors: AccountBehaviors = [.disallowFeedInRootFolder, .disallowMarkAsUnreadAfterPeriod(31)] - + let isOPMLImportSupported = false - + var isOPMLImportInProgress = false - + var server: String? { return caller.server } - + var credentials: Credentials? { didSet { #if DEBUG @@ -49,36 +49,36 @@ final class FeedlyAccountDelegate: AccountDelegate { caller.credentials = credentials } } - + let oauthAuthorizationClient: OAuthAuthorizationClient - + var accountMetadata: AccountMetadata? - + var refreshProgress = DownloadProgress(numberOfTasks: 0) - + /// Set on `accountDidInitialize` for the purposes of refreshing OAuth tokens when they expire. /// See the implementation for `FeedlyAPICallerDelegate`. private weak var initializedAccount: Account? - + internal let caller: FeedlyAPICaller - + private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedly") private let database: SyncDatabase - + private weak var currentSyncAllOperation: MainThreadOperation? private let operationQueue = MainThreadOperationQueue() - + init(dataFolder: String, transport: Transport?, api: FeedlyAPICaller.API) { // Many operations have their own operation queues, such as the sync all operation. // Making this a serial queue at this higher level of abstraction means we can ensure, // for example, a `FeedlyRefreshAccessTokenOperation` occurs before a `FeedlySyncAllOperation`, // improving our ability to debug, reason about and predict the behaviour of the code. - + if let transport = transport { self.caller = FeedlyAPICaller(transport: transport, api: api) - + } else { - + let sessionConfiguration = URLSessionConfiguration.default sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData sessionConfiguration.timeoutIntervalForRequest = 60.0 @@ -87,43 +87,43 @@ final class FeedlyAccountDelegate: AccountDelegate { sessionConfiguration.httpMaximumConnectionsPerHost = 1 sessionConfiguration.httpCookieStorage = nil sessionConfiguration.urlCache = nil - + if let userAgentHeaders = UserAgent.headers() { sessionConfiguration.httpAdditionalHeaders = userAgentHeaders } - + let session = URLSession(configuration: sessionConfiguration) self.caller = FeedlyAPICaller(transport: session, api: api) } - + let databaseFilePath = (dataFolder as NSString).appendingPathComponent("Sync.sqlite3") self.database = SyncDatabase(databaseFilePath: databaseFilePath) self.oauthAuthorizationClient = api.oauthAuthorizationClient - + self.caller.delegate = self } - + // MARK: Account API - - func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) { + + func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable: Any], completion: @escaping () -> Void) { completion() } func refreshAll(for account: Account, completion: @escaping (Result) -> Void) { assert(Thread.isMainThread) - + guard currentSyncAllOperation == nil else { os_log(.debug, log: log, "Ignoring refreshAll: Feedly sync already in progress.") completion(.success(())) return } - + guard let credentials = credentials else { os_log(.debug, log: log, "Ignoring refreshAll: Feedly account has no credentials.") completion(.failure(FeedlyAccountDelegateError.notLoggedIn)) return } - + refreshProgress.reset() let log = self.log @@ -131,24 +131,24 @@ final class FeedlyAccountDelegate: AccountDelegate { let syncAllOperation = FeedlySyncAllOperation(account: account, feedlyUserId: credentials.username, caller: caller, database: database, lastSuccessfulFetchStartDate: accountMetadata?.lastArticleFetchStartTime, downloadProgress: refreshProgress, log: log) syncAllOperation.downloadProgress = refreshProgress - + let date = Date() syncAllOperation.syncCompletionHandler = { [weak self] result in if case .success = result { self?.accountMetadata?.lastArticleFetchStartTime = date self?.accountMetadata?.lastArticleFetchEndTime = Date() } - + os_log(.debug, log: log, "Sync took %{public}.3f seconds", -date.timeIntervalSinceNow) completion(result) self?.refreshProgress.reset() } - + currentSyncAllOperation = syncAllOperation - + operationQueue.add(syncAllOperation) } - + func syncArticleStatus(for account: Account, completion: ((Result) -> Void)? = nil) { sendArticleStatus(for: account) { result in switch result { @@ -166,11 +166,11 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } - + func sendArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { // Ensure remote articles have the same status as they do locally. let send = FeedlySendArticleStatusesOperation(database: database, service: caller, log: log) - send.completionBlock = { operation in + send.completionBlock = { _ in // TODO: not call with success if operation was canceled? Not sure. DispatchQueue.main.async { completion(.success(())) @@ -178,7 +178,7 @@ final class FeedlyAccountDelegate: AccountDelegate { } operationQueue.add(send) } - + /// Attempts to ensure local articles have the same status as they do remotely. /// So if the user is using another client roughly simultaneously with this app, /// this app does its part to ensure the articles have a consistent status between both. @@ -189,45 +189,45 @@ final class FeedlyAccountDelegate: AccountDelegate { guard let credentials = credentials else { return completion(.success(())) } - + let group = DispatchGroup() - + let ingestUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log) - + group.enter() ingestUnread.completionBlock = { _ in group.leave() - + } - + let ingestStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: credentials.username, service: caller, database: database, newerThan: nil, log: log) - + group.enter() ingestStarred.completionBlock = { _ in group.leave() } - + group.notify(queue: .main) { completion(.success(())) } - + operationQueue.addOperations([ingestUnread, ingestStarred]) } - + func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result) -> Void) { let data: Data - + do { data = try Data(contentsOf: opmlFile) } catch { completion(.failure(error)) return } - + os_log(.debug, log: log, "Begin importing OPML...") isOPMLImportInProgress = true refreshProgress.addToNumberOfTasksAndRemaining(1) - + caller.importOpml(data) { result in switch result { case .success: @@ -248,15 +248,15 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } - + func createFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) { - + let progress = refreshProgress progress.addToNumberOfTasksAndRemaining(1) - + caller.createCollection(named: name) { result in progress.completeTask() - + switch result { case .success(let collection): if let folder = account.ensureFolder(with: collection.label) { @@ -271,16 +271,16 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } - + func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { guard let id = folder.externalID else { return DispatchQueue.main.async { completion(.failure(FeedlyAccountDelegateError.unableToRenameFolder(folder.nameForDisplay, name))) } } - + let nameBefore = folder.name - + caller.renameCollection(with: id, to: name) { result in switch result { case .success(let collection): @@ -291,23 +291,23 @@ final class FeedlyAccountDelegate: AccountDelegate { completion(.failure(error)) } } - + folder.name = name } - + func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { guard let id = folder.externalID else { return DispatchQueue.main.async { completion(.failure(FeedlyAccountDelegateError.unableToRemoveFolder(folder.nameForDisplay))) } } - + let progress = refreshProgress progress.addToNumberOfTasksAndRemaining(1) - + caller.deleteCollection(with: id) { result in progress.completeTask() - + switch result { case .success: account.removeFolder(folder) @@ -317,14 +317,14 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } - + func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result) -> Void) { - + do { guard let credentials = credentials else { throw FeedlyAccountDelegateError.notLoggedIn } - + let addNewFeed = try FeedlyAddNewFeedOperation(account: account, credentials: credentials, url: url, @@ -337,54 +337,54 @@ final class FeedlyAccountDelegate: AccountDelegate { container: container, progress: refreshProgress, log: log) - + addNewFeed.addCompletionHandler = { result in completion(result) } - + operationQueue.add(addNewFeed) - + } catch { DispatchQueue.main.async { completion(.failure(error)) } } } - + func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) { let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID } guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else { completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name))) return } - + let feedId = FeedlyFeedResourceId(id: feed.feedID) let editedNameBefore = feed.editedName - + // Adding an existing feed updates it. // Updating feed name in one folder/collection updates it for all folders/collections. caller.addFeed(with: feedId, title: name, toCollectionWith: collectionId) { result in switch result { case .success: completion(.success(())) - + case .failure(let error): feed.editedName = editedNameBefore completion(.failure(error)) } } - + // optimistically set the name feed.editedName = name } - + func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result) -> Void) { - + do { guard let credentials = credentials else { throw FeedlyAccountDelegateError.notLoggedIn } - + let resource = FeedlyFeedResourceId(id: feed.feedID) let addExistingFeed = try FeedlyAddExistingFeedOperation(account: account, credentials: credentials, @@ -394,28 +394,27 @@ final class FeedlyAccountDelegate: AccountDelegate { progress: refreshProgress, log: log, customFeedName: feed.editedName) - - + addExistingFeed.addCompletionHandler = { result in completion(result) } - + operationQueue.add(addExistingFeed) - + } catch { DispatchQueue.main.async { completion(.failure(error)) } } } - + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { guard let folder = container as? Folder, let collectionId = folder.externalID else { return DispatchQueue.main.async { completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed))) } } - + caller.removeFeed(feed.feedID, fromCollectionWith: collectionId) { result in switch result { case .success: @@ -425,17 +424,17 @@ final class FeedlyAccountDelegate: AccountDelegate { completion(.failure(error)) } } - + folder.removeFeed(feed) } - + func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) { guard let from = from as? Folder, let to = to as? Folder else { return DispatchQueue.main.async { completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder)) } } - + addFeed(for: account, with: feed, to: to) { [weak self] addResult in switch addResult { // now that we have added the feed, remove it from the other collection @@ -454,14 +453,14 @@ final class FeedlyAccountDelegate: AccountDelegate { to.removeFeed(feed) completion(.failure(error)) } - + } - + // optimistically move the feed, undoing as appropriate to the failure from.removeFeed(feed) to.addFeed(feed) } - + func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) { if let existingFeed = account.existingFeed(withURL: feed.url) { account.addFeed(existingFeed, to: container) { result in @@ -483,14 +482,14 @@ final class FeedlyAccountDelegate: AccountDelegate { } } } - + func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { let group = DispatchGroup() - + for feed in folder.topLevelFeeds { - + folder.topLevelFeeds.remove(feed) - + group.enter() restoreFeed(for: account, feed: feed, container: folder) { result in group.leave() @@ -501,15 +500,15 @@ final class FeedlyAccountDelegate: AccountDelegate { os_log(.error, log: self.log, "Restore folder feed error: %@.", error.localizedDescription) } } - + } - + group.notify(queue: .main) { account.addFolder(folder) completion(.success(())) } } - + func markArticles(for account: Account, articles: Set
, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result) -> Void) { account.update(articles, statusKey: statusKey, flag: flag) { result in switch result { @@ -536,13 +535,13 @@ final class FeedlyAccountDelegate: AccountDelegate { initializedAccount = account credentials = try? account.retrieveCredentials(type: .oauthAccessToken) } - + func accountWillBeDeleted(_ account: Account) { let logout = FeedlyLogoutOperation(account: account, service: caller, log: log) // Dispatch on the shared queue because the lifetime of the account delegate is uncertain. MainThreadOperationQueue.shared.add(logout) } - + static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL?, completion: @escaping (Result) -> Void) { assertionFailure("An `account` instance should enqueue an \(FeedlyRefreshAccessTokenOperation.self) instead.") completion(.success(credentials)) @@ -555,12 +554,12 @@ final class FeedlyAccountDelegate: AccountDelegate { caller.suspend() operationQueue.cancelAllOperations() } - + /// Suspend the SQLLite databases func suspendDatabase() { database.suspend() } - + /// Make sure no SQLite databases are open and we are ready to issue network requests. func resume() { database.resume() @@ -569,35 +568,35 @@ final class FeedlyAccountDelegate: AccountDelegate { } extension FeedlyAccountDelegate: FeedlyAPICallerDelegate { - - func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> ()) { + + func reauthorizeFeedlyAPICaller(_ caller: FeedlyAPICaller, completionHandler: @escaping (Bool) -> Void) { guard let account = initializedAccount else { completionHandler(false) return } - + /// Captures a failure to refresh a token, assuming that it was refreshed unless told otherwise. final class RefreshAccessTokenOperationDelegate: FeedlyOperationDelegate { - + private(set) var didReauthorize = true - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { didReauthorize = false } } - + let refreshAccessToken = FeedlyRefreshAccessTokenOperation(account: account, service: self, oauthClient: oauthAuthorizationClient, log: log) refreshAccessToken.downloadProgress = refreshProgress - + /// This must be strongly referenced by the completionBlock of the `FeedlyRefreshAccessTokenOperation`. let refreshAccessTokenDelegate = RefreshAccessTokenOperationDelegate() refreshAccessToken.delegate = refreshAccessTokenDelegate - + refreshAccessToken.completionBlock = { operation in assert(Thread.isMainThread) completionHandler(refreshAccessTokenDelegate.didReauthorize && !operation.isCanceled) } - + MainThreadOperationQueue.shared.add(refreshAccessToken) } } diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift index 0ed5cac99..efe28b73e 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyAccountDelegateError.swift @@ -19,80 +19,80 @@ enum FeedlyAccountDelegateError: LocalizedError { case addFeedInvalidFolder(Folder) case unableToRenameFeed(String, String) case unableToRemoveFeed(Feed) - + var errorDescription: String? { switch self { case .notLoggedIn: return NSLocalizedString("Please add the Feedly account again. If this problem persists, open Keychain Access and delete all feedly.com entries, then try again.", comment: "Feedly – Credentials not found.") - + case .unexpectedResourceId(let resourceId): let template = NSLocalizedString("Could not encode the identifier “%@”.", comment: "Feedly – Could not encode resource id to send to Feedly.") return String(format: template, resourceId) - + case .unableToAddFolder(let name): let template = NSLocalizedString("Could not create a folder named “%@”.", comment: "Feedly – Could not create a folder/collection.") return String(format: template, name) - + case .unableToRenameFolder(let from, let to): let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a folder/collection.") return String(format: template, from, to) - + case .unableToRemoveFolder(let name): let template = NSLocalizedString("Could not remove the folder named “%@”.", comment: "Feedly – Could not remove a folder/collection.") return String(format: template, name) - + case .unableToMoveFeedBetweenFolders(let feed, _, let to): let template = NSLocalizedString("Could not move “%@” to “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") return String(format: template, feed.nameForDisplay, to.nameForDisplay) - + case .addFeedChooseFolder: return NSLocalizedString("Please choose a folder to contain the feed.", comment: "Feedly – Feed can only be added to folders.") - + case .addFeedInvalidFolder(let invalidFolder): let template = NSLocalizedString("Feeds cannot be added to the “%@” folder.", comment: "Feedly – Feed can only be added to folders.") return String(format: template, invalidFolder.nameForDisplay) - + case .unableToRenameFeed(let from, let to): let template = NSLocalizedString("Could not rename “%@” to “%@”.", comment: "Feedly – Could not rename a feed.") return String(format: template, from, to) - + case .unableToRemoveFeed(let feed): let template = NSLocalizedString("Could not remove “%@”.", comment: "Feedly – Could not remove a feed.") return String(format: template, feed.nameForDisplay) } } - + var recoverySuggestion: String? { switch self { case .notLoggedIn: return nil - + case .unexpectedResourceId: let template = NSLocalizedString("Please contact NetNewsWire support.", comment: "Feedly – Recovery suggestion for not being able to encode a resource id to send to Feedly..") return String(format: template) - + case .unableToAddFolder: return nil - + case .unableToRenameFolder: return nil - + case .unableToRemoveFolder: return nil - + case .unableToMoveFeedBetweenFolders(let feed, let from, let to): let template = NSLocalizedString("“%@” may be in both “%@” and “%@”.", comment: "Feedly – Could not move a feed between folders/collections.") return String(format: template, feed.nameForDisplay, from.nameForDisplay, to.nameForDisplay) - + case .addFeedChooseFolder: return nil - + case .addFeedInvalidFolder: return NSLocalizedString("Please choose a different folder to contain the feed.", comment: "Feedly – Feed can only be added to folders recovery suggestion.") - + case .unableToRemoveFeed: return nil - + case .unableToRenameFeed: return nil } diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift b/Modules/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift index da4e6fb81..1aa87d07f 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyFeedContainerValidator.swift @@ -10,16 +10,16 @@ import Foundation struct FeedlyFeedContainerValidator { var container: Container - + func getValidContainer() throws -> (Folder, String) { guard let folder = container as? Folder else { throw FeedlyAccountDelegateError.addFeedChooseFolder } - + guard let collectionId = folder.externalID else { throw FeedlyAccountDelegateError.addFeedInvalidFolder(folder) } - + return (folder, collectionId) } } diff --git a/Modules/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift b/Modules/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift index c58d25a28..3251f2d0f 100644 --- a/Modules/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift +++ b/Modules/Account/Sources/Account/Feedly/FeedlyResourceProviding.swift @@ -13,7 +13,7 @@ protocol FeedlyResourceProviding { } extension FeedlyFeedResourceId: FeedlyResourceProviding { - + var resource: FeedlyResourceId { return self } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyCollectionParser.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyCollectionParser.swift index 229498592..c346b94f3 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyCollectionParser.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyCollectionParser.swift @@ -12,11 +12,11 @@ struct FeedlyCollectionParser { let collection: FeedlyCollection private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer() - + var folderName: String { return rightToLeftTextSantizer.sanitize(collection.label) ?? "" } - + var externalID: String { return collection.id } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntry.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntry.swift index cb38fd2e6..18f4a5bce 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntry.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntry.swift @@ -11,30 +11,30 @@ import Foundation struct FeedlyEntry: Decodable { /// the unique, immutable ID for this particular article. let id: String - + /// the article’s title. This string does not contain any HTML markup. let title: String? - + struct Content: Decodable { - + enum Direction: String, Decodable { case leftToRight = "ltr" case rightToLeft = "rtl" } - + let content: String? let direction: Direction? } - + /// This object typically has two values: “content” for the content itself, and “direction” (“ltr” for left-to-right, “rtl” for right-to-left). The content itself contains sanitized HTML markup. let content: Content? - + /// content object the article summary. See the content object above. let summary: Content? - + /// the author’s name let author: String? - + /// the immutable timestamp, in ms, when this article was processed by the feedly Cloud servers. let crawled: Date @@ -43,7 +43,7 @@ struct FeedlyEntry: Decodable { /// the feed from which this article was crawled. If present, “streamId” will contain the feed id, “title” will contain the feed title, and “htmlUrl” will contain the feed’s website. let origin: FeedlyOrigin? - + /// Used to help find the URL to visit an article on a web site. /// See https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ let canonical: [FeedlyLink]? diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift index 694ccd07b..0077e32d4 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryIdentifierProviding.swift @@ -14,15 +14,15 @@ protocol FeedlyEntryIdentifierProviding: AnyObject { final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding { private(set) var entryIds: Set - + init(entryIds: Set = Set()) { self.entryIds = entryIds } - + func addEntryIds(from provider: FeedlyEntryIdentifierProviding) { entryIds.formUnion(provider.entryIds) } - + func addEntryIds(in articleIds: [String]) { entryIds.formUnion(articleIds) } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryParser.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryParser.swift index 3b0b9bc35..6d225c858 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryParser.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyEntryParser.swift @@ -12,13 +12,13 @@ import Parser struct FeedlyEntryParser { let entry: FeedlyEntry - + private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer() - + var id: String { return entry.id } - + /// When ingesting articles, the feedURL must match a feed's `feedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key. var feedUrl: String? { guard let id = entry.origin?.streamId else { @@ -28,7 +28,7 @@ struct FeedlyEntryParser { } return id } - + /// Convoluted external URL logic "documented" here: /// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ var externalUrl: String? { @@ -38,39 +38,39 @@ struct FeedlyEntryParser { let webPageLinks = flattened.filter { $0.type == nil || $0.type == "text/html" } return webPageLinks.first?.href } - + var title: String? { return rightToLeftTextSantizer.sanitize(entry.title) } - + var contentHMTL: String? { return entry.content?.content ?? entry.summary?.content } - + var contentText: String? { // We could strip HTML from contentHTML? return nil } - + var summary: String? { return rightToLeftTextSantizer.sanitize(entry.summary?.content) } - + var datePublished: Date { return entry.crawled } - + var dateModified: Date? { return entry.recrawled } - + var authors: Set? { guard let name = entry.author else { return nil } return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)]) } - + /// While there is not yet a tagging interface, articles can still be searched for by tags. var tags: Set? { guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else { @@ -78,7 +78,7 @@ struct FeedlyEntryParser { } return Set(labels) } - + var attachments: Set? { guard let enclosure = entry.enclosure, !enclosure.isEmpty else { return nil @@ -86,12 +86,12 @@ struct FeedlyEntryParser { let attachments = enclosure.compactMap { ParsedAttachment(url: $0.href, mimeType: $0.type, title: nil, sizeInBytes: nil, durationInSeconds: nil) } return attachments.isEmpty ? nil : Set(attachments) } - + var parsedItemRepresentation: ParsedItem? { guard let feedUrl = feedUrl else { return nil } - + return ParsedItem(syncServiceID: id, uniqueID: id, // This value seems to get ignored or replaced. feedURL: feedUrl, diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedParser.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedParser.swift index a9faf3f25..e69d2b5e8 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedParser.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedParser.swift @@ -12,20 +12,20 @@ struct FeedlyFeedParser { let feed: FeedlyFeed private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer() - + var title: String? { return rightToLeftTextSantizer.sanitize(feed.title) ?? "" } - + var feedID: String { return feed.id } - + var url: String { let resource = FeedlyFeedResourceId(id: feed.id) return resource.url } - + var homePageURL: String? { return feed.website } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift index 8ddbd563b..ca54d7b3e 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyFeedsSearchResponse.swift @@ -9,11 +9,11 @@ import Foundation struct FeedlyFeedsSearchResponse: Decodable { - + struct Feed: Decodable { let title: String let feedId: String } - + let results: [Feed] } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyLink.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyLink.swift index 879341a08..74c009c6f 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyLink.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyLink.swift @@ -10,7 +10,7 @@ import Foundation struct FeedlyLink: Decodable { let href: String - + /// The mime type of the resource located by `href`. /// When `nil`, it's probably a web page? /// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyRTLTextSanitizer.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyRTLTextSanitizer.swift index 11eb2dcb5..55400dd5c 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyRTLTextSanitizer.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyRTLTextSanitizer.swift @@ -11,16 +11,16 @@ import Foundation struct FeedlyRTLTextSanitizer { private let rightToLeftPrefix = "
" private let rightToLeftSuffix = "
" - + func sanitize(_ sourceText: String?) -> String? { guard let source = sourceText, !source.isEmpty else { return sourceText } - + guard source.hasPrefix(rightToLeftPrefix) && source.hasSuffix(rightToLeftSuffix) else { return source } - + let start = source.index(source.startIndex, offsetBy: rightToLeftPrefix.indices.count) let end = source.index(source.endIndex, offsetBy: -rightToLeftSuffix.indices.count) return String(source[start.. FeedlyCategoryResourceId { // https://developer.feedly.com/cloud/#global-resource-ids let id = "user/\(userId)/category/global.uncategorized" return FeedlyCategoryResourceId(id: id) } - + /// All articles from all the feeds the user subscribes to. static func all(for userId: String) -> FeedlyCategoryResourceId { // https://developer.feedly.com/cloud/#global-resource-ids let id = "user/\(userId)/category/global.all" return FeedlyCategoryResourceId(id: id) } - + /// All articles from all the feeds the user loves most. static func mustRead(for userId: String) -> FeedlyCategoryResourceId { // https://developer.feedly.com/cloud/#global-resource-ids @@ -73,9 +73,9 @@ struct FeedlyCategoryResourceId: FeedlyResourceId { struct FeedlyTagResourceId: FeedlyResourceId { let id: String - + enum Global { - + static func saved(for userId: String) -> FeedlyTagResourceId { // https://developer.feedly.com/cloud/#global-resource-ids let id = "user/\(userId)/tag/global.saved" diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyStream.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyStream.swift index fc3e92f38..38dece8a5 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyStream.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyStream.swift @@ -10,16 +10,16 @@ import Foundation struct FeedlyStream: Decodable { let id: String - + /// Of the most recent entry for this stream (regardless of continuation, newerThan, etc). let updated: Date? - + /// the continuation id to pass to the next stream call, for pagination. /// This id guarantees that no entry will be duplicated in a stream (meaning, there is no need to de-duplicate entries returned by this call). /// If this value is not returned, it means the end of the stream has been reached. let continuation: String? let items: [FeedlyEntry] - + var isStreamEnd: Bool { return continuation == nil } diff --git a/Modules/Account/Sources/Account/Feedly/Models/FeedlyStreamIds.swift b/Modules/Account/Sources/Account/Feedly/Models/FeedlyStreamIds.swift index a31e1005c..5e44fee50 100644 --- a/Modules/Account/Sources/Account/Feedly/Models/FeedlyStreamIds.swift +++ b/Modules/Account/Sources/Account/Feedly/Models/FeedlyStreamIds.swift @@ -11,7 +11,7 @@ import Foundation struct FeedlyStreamIds: Decodable { let continuation: String? let ids: [String] - + var isStreamEnd: Bool { return continuation == nil } diff --git a/Modules/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift b/Modules/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift index f16029ce8..edd418874 100644 --- a/Modules/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/OAuthAccountAuthorizationOperation.swift @@ -17,7 +17,7 @@ public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject { public enum OAuthAccountAuthorizationOperationError: LocalizedError { case duplicateAccount - + public var errorDescription: String? { return NSLocalizedString("There is already a Feedly account with that username created.", comment: "Duplicate Error") } @@ -38,44 +38,44 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { public weak var presentationAnchor: ASPresentationAnchor? public weak var delegate: OAuthAccountAuthorizationOperationDelegate? - + private let accountType: AccountType private let oauthClient: OAuthAuthorizationClient private var session: ASWebAuthenticationSession? - + public init(accountType: AccountType) { self.accountType = accountType self.oauthClient = Account.oauthAuthorizationClient(for: accountType) } - + public func run() { assert(presentationAnchor != nil, "\(self) outlived presentation anchor.") - + let request = Account.oauthAuthorizationCodeGrantRequest(for: accountType) - + guard let url = request.url else { return DispatchQueue.main.async { self.didEndAuthentication(url: nil, error: URLError(.badURL)) } } - + guard let redirectUri = URL(string: oauthClient.redirectUri), let scheme = redirectUri.scheme else { assertionFailure("Could not get callback URL scheme from \(oauthClient.redirectUri)") return DispatchQueue.main.async { self.didEndAuthentication(url: nil, error: URLError(.badURL)) } } - + let session = ASWebAuthenticationSession(url: url, callbackURLScheme: scheme) { url, error in DispatchQueue.main.async { [weak self] in self?.didEndAuthentication(url: url, error: error) } } - + session.presentationContextProvider = self - + guard session.start() else { - + /// Documentation does not say on why `ASWebAuthenticationSession.start` or `canStart` might return false. /// Perhaps it has something to do with an inter-process communication failure? No browsers installed? No browsers that support web authentication? struct UnableToStartASWebAuthenticationSessionError: LocalizedError { @@ -84,25 +84,25 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { let recoverySuggestion: String? = NSLocalizedString("Check your default web browser in System Preferences or change it to Safari and try again.", comment: "OAuth - recovery suggestion - ensure browser selected supports web authentication.") } - + didFinish(UnableToStartASWebAuthenticationSessionError()) - + return } - + self.session = session } - + public func cancel() { session?.cancel() } - + private func didEndAuthentication(url: URL?, error: Error?) { guard !isCanceled else { didFinish() return } - + do { guard let url = url else { if let error = error { @@ -110,32 +110,32 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { } throw URLError(.badURL) } - + let response = try OAuthAuthorizationResponse(url: url, client: oauthClient) - + Account.requestOAuthAccessToken(with: response, client: oauthClient, accountType: accountType, completion: didEndRequestingAccessToken(_:)) - + } catch is ASWebAuthenticationSessionError { didFinish() // Primarily, cancellation. - + } catch { didFinish(error) } } - + public func presentationAnchor(for session: ASWebAuthenticationSession) -> ASPresentationAnchor { guard let anchor = presentationAnchor else { fatalError("\(self) has outlived presentation anchor.") } return anchor } - + private func didEndRequestingAccessToken(_ result: Result) { guard !isCanceled else { didFinish() return } - + switch result { case .success(let tokenResponse): saveAccount(for: tokenResponse) @@ -143,39 +143,39 @@ public enum OAuthAccountAuthorizationOperationError: LocalizedError { didFinish(error) } } - + private func saveAccount(for grant: OAuthAuthorizationGrant) { guard !AccountManager.shared.duplicateServiceAccount(type: .feedly, username: grant.accessToken.username) else { didFinish(OAuthAccountAuthorizationOperationError.duplicateAccount) return } - + let account = AccountManager.shared.createAccount(type: .feedly) do { - + // Store the refresh token first because it sends this token to the account delegate. if let token = grant.refreshToken { try account.storeCredentials(token) } - + // Now store the access token because we want the account delegate to use it. try account.storeCredentials(grant.accessToken) - + delegate?.oauthAccountAuthorizationOperation(self, didCreate: account) - + didFinish() } catch { didFinish(error) } } - + // MARK: Managing Operation State - + private func didFinish() { assert(Thread.isMainThread) operationDelegate?.operationDidComplete(self) } - + private func didFinish(_ error: Error) { assert(Thread.isMainThread) delegate?.oauthAccountAuthorizationOperation(self, didFailWith: error) diff --git a/Modules/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift b/Modules/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift index 306815c77..457996c0e 100644 --- a/Modules/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift +++ b/Modules/Account/Sources/Account/Feedly/OAuthAcessTokenRefreshing.swift @@ -15,11 +15,11 @@ public struct OAuthRefreshAccessTokenRequest: Encodable { public let grantType = "refresh_token" public var refreshToken: String public var scope: String? - + // Possibly not part of the standard but specific to certain implementations (e.g.: Feedly). public var clientId: String public var clientSecret: String - + public init(refreshToken: String, scope: String?, client: OAuthAuthorizationClient) { self.refreshToken = refreshToken self.scope = scope @@ -32,15 +32,15 @@ public struct OAuthRefreshAccessTokenRequest: Encodable { /// https://tools.ietf.org/html/rfc6749#section-6 public protocol OAuthAcessTokenRefreshRequesting { associatedtype AccessTokenResponse: OAuthAccessTokenResponse - + /// Access tokens expire. Perform a request for a fresh access token given the long life refresh token received when authorization was granted. /// - Parameter refreshRequest: The refresh token and other information the authorization server requires to grant the client fresh access tokens on the user's behalf. /// - Parameter completion: On success, the access token response appropriate for concrete type's service. Both the access and refresh token should be stored, preferably on the Keychain. On failure, possibly a `URLError` or `OAuthAuthorizationErrorResponse` value. - func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> ()) + func refreshAccessToken(_ refreshRequest: OAuthRefreshAccessTokenRequest, completion: @escaping (Result) -> Void) } /// Implemented by concrete types to perform the actual request. protocol OAuthAccessTokenRefreshing: AnyObject { - - func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> ()) + + func refreshAccessToken(with refreshToken: String, client: OAuthAuthorizationClient, completion: @escaping (Result) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationClient+Feedly.swift b/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationClient+Feedly.swift index 5300b0cf1..d9efeeb16 100644 --- a/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationClient+Feedly.swift +++ b/Modules/Account/Sources/Account/Feedly/OAuthAuthorizationClient+Feedly.swift @@ -10,7 +10,7 @@ import Foundation import Secrets extension OAuthAuthorizationClient { - + static var feedlyCloudClient: OAuthAuthorizationClient { /// Models private NetNewsWire client secrets. /// These placeholders are substituted at build time using a Run Script phase with build settings. @@ -20,7 +20,7 @@ extension OAuthAuthorizationClient { state: nil, secret: SecretKey.feedlyClientSecret) } - + static var feedlySandboxClient: OAuthAuthorizationClient { /// We use this funky redirect URI because ASWebAuthenticationSession will try to load http://localhost URLs. /// See https://developer.feedly.com/v3/sandbox/ for more information. diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift index ed0824663..9b172a0a9 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddExistingFeedOperation.swift @@ -15,36 +15,36 @@ import Secrets class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate { private let operationQueue = MainThreadOperationQueue() - var addCompletionHandler: ((Result) -> ())? + var addCompletionHandler: ((Result) -> Void)? init(account: Account, credentials: Credentials, resource: FeedlyFeedResourceId, service: FeedlyAddFeedToCollectionService, container: Container, progress: DownloadProgress, log: OSLog, customFeedName: String? = nil) throws { - + let validator = FeedlyFeedContainerValidator(container: container) let (folder, collectionId) = try validator.getValidContainer() - + self.operationQueue.suspend() super.init() - + self.downloadProgress = progress - + let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: resource, feedName: customFeedName, collectionId: collectionId, service: service) addRequest.delegate = self addRequest.downloadProgress = progress self.operationQueue.add(addRequest) - + let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) createFeeds.downloadProgress = progress createFeeds.addDependency(addRequest) self.operationQueue.add(createFeeds) - + let finishOperation = FeedlyCheckpointOperation() finishOperation.checkpointDelegate = self finishOperation.downloadProgress = progress finishOperation.addDependency(createFeeds) self.operationQueue.add(finishOperation) } - + override func run() { operationQueue.resume() } @@ -54,22 +54,22 @@ class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, addCompletionHandler = nil super.didCancel() } - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { addCompletionHandler?(.failure(error)) addCompletionHandler = nil - + cancel() } - + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { guard !isCanceled else { return } - + addCompletionHandler?(.success(())) addCompletionHandler = nil - + didFinish() } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift index ce749a83d..fb27db90e 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddFeedToCollectionOperation.swift @@ -9,7 +9,7 @@ import Foundation protocol FeedlyAddFeedToCollectionService { - func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) + func addFeed(with feedId: FeedlyFeedResourceId, title: String?, toCollectionWith collectionId: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> Void) } final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding, FeedlyResourceProviding { @@ -29,13 +29,13 @@ final class FeedlyAddFeedToCollectionOperation: FeedlyOperation, FeedlyFeedsAndF self.collectionId = collectionId self.service = service } - + private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]() - + var resource: FeedlyResourceId { return feedResource } - + override func run() { service.addFeed(with: feedResource, title: feedName, toCollectionWith: collectionId) { [weak self] result in guard let self = self else { @@ -56,15 +56,15 @@ private extension FeedlyAddFeedToCollectionOperation { switch result { case .success(let feedlyFeeds): feedsAndFolders = [(feedlyFeeds, folder)] - + let feedsWithCreatedFeedId = feedlyFeeds.filter { $0.id == resource.id } - + if feedsWithCreatedFeedId.isEmpty { didFinish(with: AccountError.createErrorNotFound) } else { didFinish() } - + case .failure(let error): didFinish(with: error) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift index 6a97c1fc8..0e718f7b0 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyAddNewFeedOperation.swift @@ -28,14 +28,13 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl private let getStreamContentsService: FeedlyGetStreamContentsService private let log: OSLog private var feedResourceId: FeedlyFeedResourceId? - var addCompletionHandler: ((Result) -> ())? + var addCompletionHandler: ((Result) -> Void)? init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws { - let validator = FeedlyFeedContainerValidator(container: container) (self.folder, self.collectionId) = try validator.getValidContainer() - + self.url = url self.operationQueue.suspend() self.account = account @@ -50,14 +49,14 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl super.init() self.downloadProgress = progress - + let search = FeedlySearchOperation(query: url, locale: .current, service: searchService) search.delegate = self search.searchDelegate = self search.downloadProgress = progress self.operationQueue.add(search) } - + override func run() { operationQueue.resume() } @@ -67,7 +66,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl addCompletionHandler = nil super.didCancel() } - + override func didFinish(with error: Error) { assert(Thread.isMainThread) addCompletionHandler?(.failure(error)) @@ -82,33 +81,33 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl guard let first = response.results.first else { return didFinish(with: AccountError.createErrorNotFound) } - + let feedResourceId = FeedlyFeedResourceId(id: first.feedId) self.feedResourceId = feedResourceId - + let addRequest = FeedlyAddFeedToCollectionOperation(account: account, folder: folder, feedResource: feedResourceId, feedName: feedName, collectionId: collectionId, service: addToCollectionService) addRequest.delegate = self addRequest.downloadProgress = downloadProgress operationQueue.add(addRequest) - + let createFeeds = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: addRequest, log: log) createFeeds.delegate = self createFeeds.addDependency(addRequest) createFeeds.downloadProgress = downloadProgress operationQueue.add(createFeeds) - + let syncUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: credentials.username, service: syncUnreadIdsService, database: database, newerThan: nil, log: log) syncUnread.addDependency(createFeeds) syncUnread.downloadProgress = downloadProgress syncUnread.delegate = self operationQueue.add(syncUnread) - + let syncFeed = FeedlySyncStreamContentsOperation(account: account, resource: feedResourceId, service: getStreamContentsService, isPagingEnabled: false, newerThan: nil, log: log) syncFeed.addDependency(syncUnread) syncFeed.downloadProgress = downloadProgress syncFeed.delegate = self operationQueue.add(syncFeed) - + let finishOperation = FeedlyCheckpointOperation() finishOperation.checkpointDelegate = self finishOperation.downloadProgress = downloadProgress @@ -116,16 +115,16 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl finishOperation.delegate = self operationQueue.add(finishOperation) } - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { addCompletionHandler?(.failure(error)) addCompletionHandler = nil - + os_log(.debug, log: log, "Unable to add new feed: %{public}@.", error as NSError) - + cancel() } - + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { guard !isCanceled else { return @@ -133,14 +132,13 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl defer { didFinish() } - + guard let handler = addCompletionHandler else { return } if let feedResource = feedResourceId, let feed = folder.existingFeed(withFeedID: feedResource.id) { handler(.success(feed)) - } - else { + } else { handler(.failure(AccountError.createErrorNotFound)) } addCompletionHandler = nil diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift index 13d0d9dac..69f36c2a1 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyCreateFeedsForCollectionFoldersOperation.swift @@ -11,7 +11,7 @@ import os.log /// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds. final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { - + let account: Account let feedsAndFoldersProvider: FeedlyFeedsAndFoldersProviding let log: OSLog @@ -21,18 +21,18 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { self.account = account self.log = log } - + override func run() { defer { didFinish() } let pairs = feedsAndFoldersProvider.feedsAndFolders - + let feedsBefore = Set(pairs .map { $0.1 } .flatMap { $0.topLevelFeeds }) - + // Remove feeds in a folder which are not in the corresponding collection. for (collectionFeeds, folder) in pairs { let feedsInFolder = folder.topLevelFeeds @@ -42,12 +42,12 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { folder.removeFeeds(feedsToRemove) // os_log(.debug, log: log, "\"%@\" - removed: %@", collection.label, feedsToRemove.map { $0.feedID }, feedsInCollection) } - + } - + // Pair each Feed with its Folder. var feedsAdded = Set() - + let feedsAndFolders = pairs .map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in return collectionFeeds.map { feed -> (FeedlyFeed, Folder) in @@ -59,11 +59,11 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { // find an existing feed previously added to the account if let feed = account.existingFeed(withFeedID: collectionFeed.id) { - + // If the feed was renamed on Feedly, ensure we ingest the new name. if feed.nameForDisplay != collectionFeed.title { feed.name = collectionFeed.title - + // Let the rest of the app (e.g.: the sidebar) know the feed name changed // `editedName` would post this if its value is changing. // Setting the `name` property has no side effects like this. @@ -87,25 +87,25 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation { url: parser.url, feedID: parser.feedID, homePageURL: parser.homePageURL) - + // So the same feed isn't created more than once. feedsAdded.insert(feed) - + return (feed, folder) } - + os_log(.debug, log: log, "Processing %i feeds.", feedsAndFolders.count) for (feed, folder) in feedsAndFolders { if !folder.has(feed) { folder.addFeed(feed) } } - + // Remove feeds without folders/collections. let feedsAfter = Set(feedsAndFolders.map { $0.0 }) let feedsWithoutCollections = feedsBefore.subtracting(feedsAfter) account.removeFeeds(feedsWithoutCollections) - + if !feedsWithoutCollections.isEmpty { os_log(.debug, log: log, "Removed %i feeds", feedsWithoutCollections.count) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift index 550ee34d7..fa12bb8b3 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyDownloadArticlesOperation.swift @@ -20,7 +20,7 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation { private let getEntriesService: FeedlyGetEntriesService private let operationQueue = MainThreadOperationQueue() private let finishOperation: FeedlyCheckpointOperation - + init(account: Account, missingArticleEntryIdProvider: FeedlyEntryIdentifierProviding, updatedArticleEntryIdProvider: FeedlyEntryIdentifierProviding, getEntriesService: FeedlyGetEntriesService, log: OSLog) { self.account = account self.operationQueue.suspend() @@ -33,65 +33,65 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation { self.finishOperation.checkpointDelegate = self self.operationQueue.add(self.finishOperation) } - + override func run() { var articleIds = missingArticleEntryIdProvider.entryIds articleIds.formUnion(updatedArticleEntryIdProvider.entryIds) - + os_log(.debug, log: log, "Requesting %{public}i articles.", articleIds.count) - + let feedlyAPILimitBatchSize = 1000 for articleIds in Array(articleIds).chunked(into: feedlyAPILimitBatchSize) { - + let provider = FeedlyEntryIdentifierProvider(entryIds: Set(articleIds)) let getEntries = FeedlyGetEntriesOperation(account: account, service: getEntriesService, provider: provider, log: log) getEntries.delegate = self self.operationQueue.add(getEntries) - + let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getEntries, log: log) organiseByFeed.delegate = self organiseByFeed.addDependency(getEntries) self.operationQueue.add(organiseByFeed) - + let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: organiseByFeed, log: log) - + updateAccount.delegate = self updateAccount.addDependency(organiseByFeed) self.operationQueue.add(updateAccount) finishOperation.addDependency(updateAccount) } - + operationQueue.resume() } override func didCancel() { // TODO: fix error on below line: "Expression type '()' is ambiguous without more context" - //os_log(.debug, log: log, "Cancelling %{public}@.", self) + // os_log(.debug, log: log, "Cancelling %{public}@.", self) operationQueue.cancelAllOperations() super.didCancel() } } extension FeedlyDownloadArticlesOperation: FeedlyCheckpointOperationDelegate { - + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { didFinish() } } extension FeedlyDownloadArticlesOperation: FeedlyOperationDelegate { - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { assert(Thread.isMainThread) - + // Having this log is useful for debugging missing required JSON keys in the response from Feedly, for example. os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", String(describing: operation), error as NSError) - + cancel() } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift index bc37dfdb7..229ebe6ee 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyFetchIdsForMissingArticlesOperation.swift @@ -13,21 +13,21 @@ final class FeedlyFetchIdsForMissingArticlesOperation: FeedlyOperation, FeedlyEn private let account: Account private let log: OSLog - + private(set) var entryIds = Set() - + init(account: Account, log: OSLog) { self.account = account self.log = log } - + override func run() { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in switch result { case .success(let articleIds): self.entryIds.formUnion(articleIds) self.didFinish() - + case .failure(let error): self.didFinish(with: error) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetCollectionsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetCollectionsOperation.swift index 151fc2e28..21a86ec0b 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetCollectionsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetCollectionsOperation.swift @@ -15,27 +15,27 @@ protocol FeedlyCollectionProviding: AnyObject { /// Get Collections from Feedly. final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding { - + let service: FeedlyGetCollectionsService let log: OSLog - + private(set) var collections = [FeedlyCollection]() init(service: FeedlyGetCollectionsService, log: OSLog) { self.service = service self.log = log } - + override func run() { os_log(.debug, log: log, "Requesting collections.") - + service.getCollections { result in switch result { case .success(let collections): os_log(.debug, log: self.log, "Received collections: %{public}@", collections.map { $0.id }) self.collections = collections self.didFinish() - + case .failure(let error): os_log(.debug, log: self.log, "Unable to request collections: %{public}@.", error as NSError) self.didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift index 46e78a087..516427e12 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetEntriesOperation.swift @@ -24,20 +24,20 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, Fe self.provider = provider self.log = log } - + private(set) var entries = [FeedlyEntry]() - + private var storedParsedEntries: Set? - + var parsedEntries: Set { if let entries = storedParsedEntries { return entries } - + let parsed = Set(entries.compactMap { FeedlyEntryParser(entry: $0).parsedItemRepresentation }) - + // TODO: Fix the below. There’s an error on the os.log line: "Expression type '()' is ambiguous without more context" // if parsed.count != entries.count { // let entryIds = Set(entries.map { $0.id }) @@ -45,23 +45,23 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, Fe // let difference = entryIds.subtracting(parsedIds) // os_log(.debug, log: log, "%{public}@ dropping articles with ids: %{public}@.", self, difference) // } - + storedParsedEntries = parsed - + return parsed } - + var parsedItemProviderName: String { return name ?? String(describing: Self.self) } - + override func run() { service.getEntries(for: provider.entryIds) { result in switch result { case .success(let entries): self.entries = entries self.didFinish() - + case .failure(let error): os_log(.debug, log: self.log, "Unable to get entries: %{public}@.", error as NSError) self.didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift index 4d2b7ba7b..fb305f7d4 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamContentsOperation.swift @@ -25,17 +25,17 @@ protocol FeedlyGetStreamContentsOperationDelegate: AnyObject { /// Get the stream content of a Collection from Feedly. final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding { - + struct ResourceProvider: FeedlyResourceProviding { var resource: FeedlyResourceId } - + let resourceProvider: FeedlyResourceProviding - + var parsedItemProviderName: String { return resourceProvider.resource.id } - + var entries: [FeedlyEntry] { guard let entries = stream?.items else { // assert(isFinished, "This should only be called when the operation finishes without error.") @@ -44,43 +44,43 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid } return entries } - + var parsedEntries: Set { if let entries = storedParsedEntries { return entries } - + let parsed = Set(entries.compactMap { FeedlyEntryParser(entry: $0).parsedItemRepresentation }) - + if parsed.count != entries.count { let entryIds = Set(entries.map { $0.id }) let parsedIds = Set(parsed.map { $0.uniqueID }) let difference = entryIds.subtracting(parsedIds) os_log(.debug, log: log, "Dropping articles with ids: %{public}@.", difference) } - + storedParsedEntries = parsed - + return parsed } - + private(set) var stream: FeedlyStream? { didSet { storedParsedEntries = nil } } - + private var storedParsedEntries: Set? - + let account: Account let service: FeedlyGetStreamContentsService let unreadOnly: Bool? let newerThan: Date? let continuation: String? let log: OSLog - + weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate? init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) { @@ -92,21 +92,21 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid self.newerThan = newerThan self.log = log } - + convenience init(account: Account, resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) { self.init(account: account, resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log) } - + override func run() { service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in switch result { case .success(let stream): self.stream = stream - + self.streamDelegate?.feedlyGetStreamContentsOperation(self, didGetContentsOf: stream) - + self.didFinish() - + case .failure(let error): os_log(.debug, log: self.log, "Unable to get stream contents: %{public}@.", error as NSError) self.didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift index 602520720..0676cf9e3 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetStreamIdsOperation.swift @@ -15,7 +15,7 @@ protocol FeedlyGetStreamIdsOperationDelegate: AnyObject { /// Single responsibility is to get the stream ids from Feedly. final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding { - + var entryIds: Set { guard let ids = streamIds?.ids else { assertionFailure("Has this operation been addeded as a dependency on the caller?") @@ -23,9 +23,9 @@ final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierP } return Set(ids) } - + private(set) var streamIds: FeedlyStreamIds? - + let account: Account let service: FeedlyGetStreamIdsService let continuation: String? @@ -43,19 +43,19 @@ final class FeedlyGetStreamIdsOperation: FeedlyOperation, FeedlyEntryIdentifierP self.unreadOnly = unreadOnly self.log = log } - + weak var streamIdsDelegate: FeedlyGetStreamIdsOperationDelegate? - + override func run() { service.getStreamIds(for: resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in switch result { case .success(let stream): self.streamIds = stream - + self.streamIdsDelegate?.feedlyGetStreamIdsOperation(self, didGet: stream) - + self.didFinish() - + case .failure(let error): os_log(.debug, log: self.log, "Unable to get stream ids: %{public}@.", error as NSError) self.didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift index 09968d750..25c089aa6 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyGetUpdatedArticleIdsOperation.swift @@ -21,7 +21,7 @@ class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifie private let service: FeedlyGetStreamIdsService private let newerThan: Date? private let log: OSLog - + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { self.account = account self.resource = resource @@ -29,50 +29,50 @@ class FeedlyGetUpdatedArticleIdsOperation: FeedlyOperation, FeedlyEntryIdentifie self.newerThan = newerThan self.log = log } - + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, newerThan: Date?, log: OSLog) { let all = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log) } - + var entryIds: Set { return storedUpdatedArticleIds } - + private var storedUpdatedArticleIds = Set() - + override func run() { getStreamIds(nil) } - + private func getStreamIds(_ continuation: String?) { guard let date = newerThan else { os_log(.debug, log: log, "No date provided so everything must be new (nothing is updated).") didFinish() return } - + service.getStreamIds(for: resource, continuation: continuation, newerThan: date, unreadOnly: nil, completion: didGetStreamIds(_:)) } - + private func didGetStreamIds(_ result: Result) { guard !isCanceled else { didFinish() return } - + switch result { case .success(let streamIds): storedUpdatedArticleIds.formUnion(streamIds.ids) - + guard let continuation = streamIds.continuation else { os_log(.debug, log: log, "%{public}i articles updated since last successful sync start date.", storedUpdatedArticleIds.count) didFinish() return } - + getStreamIds(continuation) - + case .failure(let error): didFinish(with: error) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift index 02dc9af61..15763ce1b 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestStreamArticleIdsOperation.swift @@ -21,48 +21,48 @@ class FeedlyIngestStreamArticleIdsOperation: FeedlyOperation { private let resource: FeedlyResourceId private let service: FeedlyGetStreamIdsService private let log: OSLog - + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, log: OSLog) { self.account = account self.resource = resource self.service = service self.log = log } - + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, log: OSLog) { let all = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: all, service: service, log: log) } - + override func run() { getStreamIds(nil) } - + private func getStreamIds(_ continuation: String?) { service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: nil, completion: didGetStreamIds(_:)) } - + private func didGetStreamIds(_ result: Result) { guard !isCanceled else { didFinish() return } - + switch result { case .success(let streamIds): account.createStatusesIfNeeded(articleIDs: Set(streamIds.ids)) { databaseError in - + if let error = databaseError { self.didFinish(with: error) return } - + guard let continuation = streamIds.continuation else { os_log(.debug, log: self.log, "Reached end of stream for %@", self.resource.id) self.didFinish() return } - + self.getStreamIds(continuation) } case .failure(let error): diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift index fbadb88e9..f8a0b08e6 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyIngestUnreadArticleIdsOperation.swift @@ -26,12 +26,12 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { private let database: SyncDatabase private var remoteEntryIds = Set() private let log: OSLog - + convenience init(account: Account, userId: String, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { let resource = FeedlyCategoryResourceId.Global.all(for: userId) self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log) } - + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamIdsService, database: SyncDatabase, newerThan: Date?, log: OSLog) { self.account = account self.resource = resource @@ -39,75 +39,75 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { self.database = database self.log = log } - + override func run() { getStreamIds(nil) } - + private func getStreamIds(_ continuation: String?) { service.getStreamIds(for: resource, continuation: continuation, newerThan: nil, unreadOnly: true, completion: didGetStreamIds(_:)) } - + private func didGetStreamIds(_ result: Result) { guard !isCanceled else { didFinish() return } - + switch result { case .success(let streamIds): - + remoteEntryIds.formUnion(streamIds.ids) - + guard let continuation = streamIds.continuation else { removeEntryIdsWithPendingStatus() return } - + getStreamIds(continuation) - + case .failure(let error): didFinish(with: error) } } - + /// Do not override pending statuses with the remote statuses of the same articles, otherwise an article will temporarily re-acquire the remote status before the pending status is pushed and subseqently pulled. private func removeEntryIdsWithPendingStatus() { guard !isCanceled else { didFinish() return } - + database.selectPendingReadStatusArticleIDs { result in switch result { case .success(let pendingArticleIds): self.remoteEntryIds.subtract(pendingArticleIds) - + self.updateUnreadStatuses() - + case .failure(let error): self.didFinish(with: error) } } } - + private func updateUnreadStatuses() { guard !isCanceled else { didFinish() return } - + account.fetchUnreadArticleIDs { result in switch result { case .success(let localUnreadArticleIDs): self.processUnreadArticleIDs(localUnreadArticleIDs) - + case .failure(let error): self.didFinish(with: error) } } } - + private func processUnreadArticleIDs(_ localUnreadArticleIDs: Set) { guard !isCanceled else { didFinish() @@ -116,14 +116,14 @@ final class FeedlyIngestUnreadArticleIdsOperation: FeedlyOperation { let remoteUnreadArticleIDs = remoteEntryIds let group = DispatchGroup() - + final class ReadStatusResults { var markAsUnreadError: Error? var markAsReadError: Error? } - + let results = ReadStatusResults() - + group.enter() account.markAsUnread(remoteUnreadArticleIDs) { result in if case .failure(let error) = result { diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift index 29fb7eed6..f7bb7d4e1 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyLogoutOperation.swift @@ -10,7 +10,7 @@ import Foundation import os.log protocol FeedlyLogoutService { - func logout(completion: @escaping (Result) -> ()) + func logout(completion: @escaping (Result) -> Void) } final class FeedlyLogoutOperation: FeedlyOperation { @@ -18,18 +18,18 @@ final class FeedlyLogoutOperation: FeedlyOperation { let service: FeedlyLogoutService let account: Account let log: OSLog - + init(account: Account, service: FeedlyLogoutService, log: OSLog) { self.service = service self.account = account self.log = log } - + override func run() { os_log("Requesting logout of %{public}@ account.", "\(account.type)") service.logout(completion: didCompleteLogout(_:)) } - + func didCompleteLogout(_ result: Result) { assert(Thread.isMainThread) switch result { @@ -42,7 +42,7 @@ final class FeedlyLogoutOperation: FeedlyOperation { // oh well, we tried our best. } didFinish() - + case .failure(let error): os_log("Logout failed because %{public}@.", error as NSError) didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift index 376459ff6..4bf6da0f9 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyMirrorCollectionsAsFoldersOperation.swift @@ -15,11 +15,11 @@ protocol FeedlyFeedsAndFoldersProviding { /// Reflect Collections from Feedly as Folders. final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyFeedsAndFoldersProviding { - + let account: Account let collectionsProvider: FeedlyCollectionProviding let log: OSLog - + private(set) var feedsAndFolders = [([FeedlyFeed], Folder)]() init(account: Account, collectionsProvider: FeedlyCollectionProviding, log: OSLog) { @@ -27,15 +27,15 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyFe self.account = account self.log = log } - + override func run() { defer { didFinish() } - + let localFolders = account.folders ?? Set() let collections = collectionsProvider.collections - + feedsAndFolders = collections.compactMap { collection -> ([FeedlyFeed], Folder)? in let parser = FeedlyCollectionParser(collection: collection) guard let folder = account.ensureFolder(with: parser.folderName) else { @@ -45,18 +45,18 @@ final class FeedlyMirrorCollectionsAsFoldersOperation: FeedlyOperation, FeedlyFe folder.externalID = parser.externalID return (collection.feeds, folder) } - + os_log(.debug, log: log, "Ensured %i folders for %i collections.", feedsAndFolders.count, collections.count) - + // Remove folders without a corresponding collection let collectionFolders = Set(feedsAndFolders.map { $0.1 }) let foldersWithoutCollections = localFolders.subtracting(collectionFolders) - + if !foldersWithoutCollections.isEmpty { for unmatched in foldersWithoutCollections { account.removeFolder(unmatched) } - + os_log(.debug, log: log, "Removed %i folders: %@", foldersWithoutCollections.count, foldersWithoutCollections.map { $0.externalID ?? $0.nameForDisplay }) } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift index 9246e38f0..b696ac4d7 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyOrganiseParsedItemsByFeedOperation.swift @@ -21,24 +21,24 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar private let account: Account private let parsedItemProvider: FeedlyParsedItemProviding private let log: OSLog - + var parsedItemsByFeedProviderName: String { return name ?? String(describing: Self.self) } - - var parsedItemsKeyedByFeedId: [String : Set] { + + var parsedItemsKeyedByFeedId: [String: Set] { precondition(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type. return itemsKeyedByFeedId } - + private var itemsKeyedByFeedId = [String: Set]() - + init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) { self.account = account self.parsedItemProvider = parsedItemProvider self.log = log } - + override func run() { defer { didFinish() @@ -46,7 +46,7 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar let items = parsedItemProvider.parsedEntries var dict = [String: Set](minimumCapacity: items.count) - + for item in items { let key = item.feedURL let value: Set = { @@ -59,9 +59,9 @@ final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyPar }() dict[key] = value } - + os_log(.debug, log: log, "Grouped %i items by %i feeds for %@", items.count, dict.count, parsedItemProvider.parsedItemProviderName) - + itemsKeyedByFeedId = dict } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift index ad8271d12..9525e7a95 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRefreshAccessTokenOperation.swift @@ -17,41 +17,41 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { let oauthClient: OAuthAuthorizationClient let account: Account let log: OSLog - + init(account: Account, service: OAuthAccessTokenRefreshing, oauthClient: OAuthAuthorizationClient, log: OSLog) { self.oauthClient = oauthClient self.service = service self.account = account self.log = log } - + override func run() { let refreshToken: Credentials - + do { guard let credentials = try account.retrieveCredentials(type: .oauthRefreshToken) else { os_log(.debug, log: log, "Could not find a refresh token in the keychain. Check the refresh token is added to the Keychain, remove the account and add it again.") throw TransportError.httpError(status: 403) } - + refreshToken = credentials - + } catch { didFinish(with: error) return } - + os_log(.debug, log: log, "Refreshing access token.") - + // Ignore cancellation after the request is resumed otherwise we may continue storing a potentially invalid token! service.refreshAccessToken(with: refreshToken.secret, client: oauthClient) { result in self.didRefreshAccessToken(result) } } - + private func didRefreshAccessToken(_ result: Result) { assert(Thread.isMainThread) - + switch result { case .success(let grant): do { @@ -60,16 +60,16 @@ final class FeedlyRefreshAccessTokenOperation: FeedlyOperation { if let token = grant.refreshToken { try account.storeCredentials(token) } - + os_log(.debug, log: log, "Storing access token.") // Now store the access token because we want the account delegate to use it. try account.storeCredentials(grant.accessToken) - + didFinish() } catch { didFinish(with: error) } - + case .failure(let error): didFinish(with: error) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRequestStreamsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRequestStreamsOperation.swift index 688546abb..32c723fe9 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRequestStreamsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyRequestStreamsOperation.swift @@ -16,9 +16,9 @@ protocol FeedlyRequestStreamsOperationDelegate: AnyObject { /// Create one stream request operation for one Feedly collection. /// This is the start of the process of refreshing the entire contents of a Folder. final class FeedlyRequestStreamsOperation: FeedlyOperation { - + weak var queueDelegate: FeedlyRequestStreamsOperationDelegate? - + let collectionsProvider: FeedlyCollectionProviding let service: FeedlyGetStreamContentsService let account: Account @@ -34,16 +34,16 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation { self.unreadOnly = unreadOnly self.log = log } - + override func run() { defer { didFinish() } - + assert(queueDelegate != nil, "This is not particularly useful unless the `queueDelegate` is non-nil.") - + // TODO: Prioritise the must read collection/category before others so the most important content for the user loads first. - + for collection in collectionsProvider.collections { let resource = FeedlyCategoryResourceId(id: collection.id) let operation = FeedlyGetStreamContentsOperation(account: account, @@ -54,7 +54,7 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation { log: log) queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation) } - + os_log(.debug, log: log, "Requested %i collection streams", collectionsProvider.collections.count) } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift index 40d6c76ec..d372e8e60 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySearchOperation.swift @@ -9,7 +9,7 @@ import Foundation protocol FeedlySearchService: AnyObject { - func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result) -> ()) + func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result) -> Void) } protocol FeedlySearchOperationDelegate: AnyObject { @@ -30,7 +30,7 @@ class FeedlySearchOperation: FeedlyOperation { self.locale = locale self.searchService = service } - + override func run() { searchService.getFeeds(for: query, count: 1, locale: locale.identifier) { result in switch result { @@ -38,7 +38,7 @@ class FeedlySearchOperation: FeedlyOperation { assert(Thread.isMainThread) self.searchDelegate?.feedlySearchOperation(self, didGet: response) self.didFinish() - + case .failure(let error): self.didFinish(with: error) } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift index 6cbde0443..1a2d9bc97 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncAllOperation.swift @@ -19,9 +19,9 @@ final class FeedlySyncAllOperation: FeedlyOperation { private let operationQueue = MainThreadOperationQueue() private let log: OSLog let syncUUID: UUID - - var syncCompletionHandler: ((Result) -> ())? - + + var syncCompletionHandler: ((Result) -> Void)? + /// These requests to Feedly determine which articles to download: /// 1. The set of all article ids we might need or show. /// 2. The set of all unread article ids we might need or show (a subset of 1). @@ -38,49 +38,49 @@ final class FeedlySyncAllOperation: FeedlyOperation { self.syncUUID = UUID() self.log = log self.operationQueue.suspend() - + super.init() - + self.downloadProgress = downloadProgress - + // Send any read/unread/starred article statuses to Feedly before anything else. let sendArticleStatuses = FeedlySendArticleStatusesOperation(database: database, service: markArticlesService, log: log) sendArticleStatuses.delegate = self sendArticleStatuses.downloadProgress = downloadProgress self.operationQueue.add(sendArticleStatuses) - + // Get all the Collections the user has. let getCollections = FeedlyGetCollectionsOperation(service: getCollectionsService, log: log) getCollections.delegate = self getCollections.downloadProgress = downloadProgress getCollections.addDependency(sendArticleStatuses) self.operationQueue.add(getCollections) - + // Ensure a folder exists for each Collection, removing Folders without a corresponding Collection. let mirrorCollectionsAsFolders = FeedlyMirrorCollectionsAsFoldersOperation(account: account, collectionsProvider: getCollections, log: log) mirrorCollectionsAsFolders.delegate = self mirrorCollectionsAsFolders.addDependency(getCollections) self.operationQueue.add(mirrorCollectionsAsFolders) - + // Ensure feeds are created and grouped by their folders. let createFeedsOperation = FeedlyCreateFeedsForCollectionFoldersOperation(account: account, feedsAndFoldersProvider: mirrorCollectionsAsFolders, log: log) createFeedsOperation.delegate = self createFeedsOperation.addDependency(mirrorCollectionsAsFolders) self.operationQueue.add(createFeedsOperation) - + let getAllArticleIds = FeedlyIngestStreamArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, log: log) getAllArticleIds.delegate = self getAllArticleIds.downloadProgress = downloadProgress getAllArticleIds.addDependency(createFeedsOperation) self.operationQueue.add(getAllArticleIds) - + // Get each page of unread article ids in the global.all stream for the last 31 days (nil = Feedly API default). let getUnread = FeedlyIngestUnreadArticleIdsOperation(account: account, userId: feedlyUserId, service: getUnreadService, database: database, newerThan: nil, log: log) getUnread.delegate = self getUnread.addDependency(getAllArticleIds) getUnread.downloadProgress = downloadProgress self.operationQueue.add(getUnread) - + // Get each page of the article ids which have been update since the last successful fetch start date. // If the date is nil, this operation provides an empty set (everything is new, nothing is updated). let getUpdated = FeedlyGetUpdatedArticleIdsOperation(account: account, userId: feedlyUserId, service: getStreamIdsService, newerThan: lastSuccessfulFetchStartDate, log: log) @@ -88,14 +88,14 @@ final class FeedlySyncAllOperation: FeedlyOperation { getUpdated.downloadProgress = downloadProgress getUpdated.addDependency(createFeedsOperation) self.operationQueue.add(getUpdated) - + // Get each page of the article ids for starred articles. let getStarred = FeedlyIngestStarredArticleIdsOperation(account: account, userId: feedlyUserId, service: getStarredService, database: database, newerThan: nil, log: log) getStarred.delegate = self getStarred.downloadProgress = downloadProgress getStarred.addDependency(createFeedsOperation) self.operationQueue.add(getStarred) - + // Now all the possible article ids we need have a status, fetch the article ids for missing articles. let getMissingIds = FeedlyFetchIdsForMissingArticlesOperation(account: account, log: log) getMissingIds.delegate = self @@ -105,7 +105,7 @@ final class FeedlySyncAllOperation: FeedlyOperation { getMissingIds.addDependency(getStarred) getMissingIds.addDependency(getUpdated) self.operationQueue.add(getMissingIds) - + // Download all the missing and updated articles let downloadMissingArticles = FeedlyDownloadArticlesOperation(account: account, missingArticleEntryIdProvider: getMissingIds, @@ -117,7 +117,7 @@ final class FeedlySyncAllOperation: FeedlyOperation { downloadMissingArticles.addDependency(getMissingIds) downloadMissingArticles.addDependency(getUpdated) self.operationQueue.add(downloadMissingArticles) - + // Once this operation's dependencies, their dependencies etc finish, we can finish. let finishOperation = FeedlyCheckpointOperation() finishOperation.checkpointDelegate = self @@ -125,11 +125,11 @@ final class FeedlySyncAllOperation: FeedlyOperation { finishOperation.addDependency(downloadMissingArticles) self.operationQueue.add(finishOperation) } - + convenience init(account: Account, feedlyUserId: String, caller: FeedlyAPICaller, database: SyncDatabase, lastSuccessfulFetchStartDate: Date?, downloadProgress: DownloadProgress, log: OSLog) { self.init(account: account, feedlyUserId: feedlyUserId, lastSuccessfulFetchStartDate: lastSuccessfulFetchStartDate, markArticlesService: caller, getUnreadService: caller, getCollectionsService: caller, getStreamContentsService: caller, getStarredService: caller, getStreamIdsService: caller, getEntriesService: caller, database: database, downloadProgress: downloadProgress, log: log) } - + override func run() { os_log(.debug, log: log, "Starting sync %{public}@", syncUUID.uuidString) operationQueue.resume() @@ -144,29 +144,29 @@ final class FeedlySyncAllOperation: FeedlyOperation { } extension FeedlySyncAllOperation: FeedlyCheckpointOperationDelegate { - + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { assert(Thread.isMainThread) os_log(.debug, log: self.log, "Sync completed: %{public}@", syncUUID.uuidString) - + syncCompletionHandler?(.success(())) syncCompletionHandler = nil - + didFinish() } } extension FeedlySyncAllOperation: FeedlyOperationDelegate { - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { assert(Thread.isMainThread) - + // Having this log is useful for debugging missing required JSON keys in the response from Feedly, for example. os_log(.debug, log: log, "%{public}@ failed with error: %{public}@.", String(describing: operation), error as NSError) - + syncCompletionHandler?(.failure(error)) syncCompletionHandler = nil - + cancel() } } diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift index 3dadd607d..8af47f50d 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlySyncStreamContentsOperation.swift @@ -23,7 +23,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD private let isPagingEnabled: Bool private let log: OSLog private let finishOperation: FeedlyCheckpointOperation - + init(account: Account, resource: FeedlyResourceId, service: FeedlyGetStreamContentsService, isPagingEnabled: Bool, newerThan: Date?, log: OSLog) { self.account = account self.resource = resource @@ -33,19 +33,19 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD self.newerThan = newerThan self.log = log self.finishOperation = FeedlyCheckpointOperation() - + super.init() - + self.operationQueue.add(self.finishOperation) self.finishOperation.checkpointDelegate = self enqueueOperations(for: nil) } - + convenience init(account: Account, credentials: Credentials, service: FeedlyGetStreamContentsService, newerThan: Date?, log: OSLog) { let all = FeedlyCategoryResourceId.Global.all(for: credentials.username) self.init(account: account, resource: all, service: service, isPagingEnabled: true, newerThan: newerThan, log: log) } - + override func run() { operationQueue.resume() } @@ -61,7 +61,7 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD let operations = pageOperations(for: continuation) operationQueue.addOperations(operations) } - + func pageOperations(for continuation: String?) -> [MainThreadOperation] { let getPage = FeedlyGetStreamContentsOperation(account: account, resource: resource, @@ -70,11 +70,10 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD newerThan: newerThan, log: log) - let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getPage, log: log) - + let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: organiseByFeed, log: log) - + getPage.delegate = self getPage.streamDelegate = self @@ -88,28 +87,28 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD return [getPage, organiseByFeed, updateAccount] } - + func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream) { guard !isCanceled else { os_log(.debug, log: log, "Cancelled requesting page for %{public}@", resource.id) return } - + os_log(.debug, log: log, "Ingesting %i items from %{public}@", stream.items.count, stream.id) - + guard isPagingEnabled, let continuation = stream.continuation else { os_log(.debug, log: log, "Reached end of stream for %{public}@", stream.id) return } - + enqueueOperations(for: continuation) } - + func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation) { os_log(.debug, log: log, "Completed ingesting items from %{public}@", resource.id) didFinish() } - + func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error) { operationQueue.cancelAllOperations() didFinish(with: error) diff --git a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift index 9a38a46c9..4c3e1b650 100644 --- a/Modules/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift +++ b/Modules/Account/Sources/Account/Feedly/Operations/FeedlyUpdateAccountFeedsWithItemsOperation.swift @@ -22,16 +22,16 @@ final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation { self.organisedItemsProvider = organisedItemsProvider self.log = log } - + override func run() { let feedIDsAndItems = organisedItemsProvider.parsedItemsKeyedByFeedId - + account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { databaseError in if let error = databaseError { self.didFinish(with: error) return } - + os_log(.debug, log: self.log, "Updated %i feeds for \"%@\"", feedIDsAndItems.count, self.organisedItemsProvider.parsedItemsByFeedProviderName) self.didFinish() } diff --git a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetCollectionsService.swift b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetCollectionsService.swift index 2dbf6861b..14169a1e9 100644 --- a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetCollectionsService.swift +++ b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetCollectionsService.swift @@ -9,5 +9,5 @@ import Foundation protocol FeedlyGetCollectionsService: AnyObject { - func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ()) + func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetEntriesService.swift b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetEntriesService.swift index 7bc00b607..4ec05e334 100644 --- a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetEntriesService.swift +++ b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetEntriesService.swift @@ -9,5 +9,5 @@ import Foundation protocol FeedlyGetEntriesService: AnyObject { - func getEntries(for ids: Set, completion: @escaping (Result<[FeedlyEntry], Error>) -> ()) + func getEntries(for ids: Set, completion: @escaping (Result<[FeedlyEntry], Error>) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamContentsService.swift b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamContentsService.swift index 6770a2593..6b27d752a 100644 --- a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamContentsService.swift +++ b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamContentsService.swift @@ -9,5 +9,5 @@ import Foundation protocol FeedlyGetStreamContentsService: AnyObject { - func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) + func getStreamContents(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamIdsService.swift b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamIdsService.swift index 3d5863e0d..5f6595d7f 100644 --- a/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamIdsService.swift +++ b/Modules/Account/Sources/Account/Feedly/Services/FeedlyGetStreamIdsService.swift @@ -9,5 +9,5 @@ import Foundation protocol FeedlyGetStreamIdsService: AnyObject { - func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> ()) + func getStreamIds(for resource: FeedlyResourceId, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result) -> Void) } diff --git a/Modules/Account/Sources/Account/Feedly/Services/FeedlyMarkArticlesService.swift b/Modules/Account/Sources/Account/Feedly/Services/FeedlyMarkArticlesService.swift index 60a9263c2..0b7be8792 100644 --- a/Modules/Account/Sources/Account/Feedly/Services/FeedlyMarkArticlesService.swift +++ b/Modules/Account/Sources/Account/Feedly/Services/FeedlyMarkArticlesService.swift @@ -13,7 +13,7 @@ enum FeedlyMarkAction: String { case unread case saved case unsaved - + /// These values are paired with the "action" key in POST requests to the markers API. /// See for example: https://developer.feedly.com/v3/markers/#mark-one-or-multiple-articles-as-read var actionValue: String { @@ -31,5 +31,5 @@ enum FeedlyMarkAction: String { } protocol FeedlyMarkArticlesService: AnyObject { - func mark(_ articleIds: Set, as action: FeedlyMarkAction, completion: @escaping (Result) -> ()) + func mark(_ articleIds: Set, as action: FeedlyMarkAction, completion: @escaping (Result) -> Void) }