From 0499952b45bc51c928abb59db929ef0ec9a089a2 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 28 Oct 2020 19:03:41 -0500 Subject: [PATCH] Rewrote the article syncing code to be more efficient and handle large datasets --- .../ReaderAPI/ReaderAPIAccountDelegate.swift | 128 +----- .../Account/ReaderAPI/ReaderAPICaller.swift | 433 +++++------------- .../ReaderAPI/ReaderAPIUnreadEntry.swift | 2 +- 3 files changed, 151 insertions(+), 412 deletions(-) diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 5b675736e..139837b4a 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -95,18 +95,25 @@ final class ReaderAPIAccountDelegate: AccountDelegate { refreshAccount(account) { result in switch result { case .success(): - self.sendArticleStatus(for: account) { _ in self.refreshProgress.completeTask() - self.refreshArticleStatus(for: account) { _ in + self.caller.retrieveItemIDs(type: .allForAccount) { result in self.refreshProgress.completeTask() - self.refreshArticles(account) { - self.refreshMissingArticles(account) { - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(())) + switch result { + case .success(let articleIDs): + account.markAsRead(Set(articleIDs)) { _ in + self.refreshArticleStatus(for: account) { _ in + self.refreshProgress.completeTask() + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(())) + } + } } } + case .failure(let error): + completion(.failure(error)) } } } @@ -173,10 +180,10 @@ final class ReaderAPIAccountDelegate: AccountDelegate { func refreshArticleStatus(for account: Account, completion: @escaping ((Result) -> Void)) { os_log(.debug, log: log, "Refreshing article statuses...") + let group = DispatchGroup() - group.enter() - caller.retrieveUnreadEntries() { result in + caller.retrieveItemIDs(type: .unread) { result in switch result { case .success(let articleIDs): self.syncArticleReadState(account: account, articleIDs: articleIDs) @@ -189,7 +196,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } group.enter() - caller.retrieveStarredEntries() { result in + caller.retrieveItemIDs(type: .starred) { result in switch result { case .success(let articleIDs): self.syncArticleStarredState(account: account, articleIDs: articleIDs) @@ -403,7 +410,6 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result) -> Void) { - if let folder = container as? Folder, let feedName = feed.externalID { refreshProgress.addToNumberOfTasksAndRemaining(1) caller.createTagging(subscriptionID: feedName, tagName: folder.name ?? "") { result in @@ -431,7 +437,6 @@ final class ReaderAPIAccountDelegate: AccountDelegate { completion(.success(())) } } - } func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result) -> Void) { @@ -560,7 +565,6 @@ private extension ReaderAPIAccountDelegate { self.syncFolders(account, tags) } self.refreshProgress.completeTask() - self.forceExpireFolderFeedRelationship(account, tags) self.refreshFeeds(account, completion: completion) case .failure(let error): completion(.failure(error)) @@ -568,30 +572,6 @@ private extension ReaderAPIAccountDelegate { } } - func forceExpireFolderFeedRelationship(_ account: Account, _ tags: [ReaderAPITag]?) { - guard let tags = tags else { return } - - let folderNames: [String] = { - if let folders = account.folders { - return folders.map { $0.name ?? "" } - } else { - return [String]() - } - }() - - let readerFolderNames = tags.compactMap { $0.folderName } - - // The sync service has a tag that we don't have a folder for. We might not get a new - // taggings response for it if it is a folder rename. Force expire the subscription - // so that we will for sure get the new tagging information by pulling all subscriptions. - readerFolderNames.forEach { tagName in - if !folderNames.contains(tagName) { - accountMetadata?.conditionalGetInfo[ReaderAPICaller.ConditionalGetKeys.subscriptions] = nil - } - } - - } - func syncFolders(_ account: Account, _ tags: [ReaderAPITag]?) { guard let tags = tags else { return } assert(Thread.isMainThread) @@ -879,30 +859,21 @@ private extension ReaderAPIAccountDelegate { refreshProgress.addToNumberOfTasksAndRemaining(5) // Download the initial articles - self.caller.retrieveEntries(webFeedID: feed.webFeedID) { result in + self.caller.retrieveItemIDs(type: .allForFeed, webFeedID: feed.webFeedID) { result in self.refreshProgress.completeTask() - switch result { - case .success(let (entries, page)): - self.processEntries(account: account, entries: entries) { - + case .success(let articleIDs): + account.markAsRead(Set(articleIDs)) { _ in self.refreshProgress.completeTask() self.refreshArticleStatus(for: account) { _ in - self.refreshProgress.completeTask() - self.refreshArticles(account, page: page) { - - self.refreshProgress.completeTask() - self.refreshMissingArticles(account) { - - self.refreshProgress.clear() - DispatchQueue.main.async { - completion(.success(feed)) - } - + self.refreshMissingArticles(account) { + self.refreshProgress.clear() + DispatchQueue.main.async { + completion(.success(feed)) } - } + } } } @@ -914,29 +885,6 @@ private extension ReaderAPIAccountDelegate { } - func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) { - os_log(.debug, log: log, "Refreshing articles...") - - caller.retrieveEntries() { result in - switch result { - case .success(let (entries, page, lastPageNumber)): - if let last = lastPageNumber { - self.refreshProgress.addToNumberOfTasksAndRemaining(last - 1) - } - self.processEntries(account: account, entries: entries) { - self.refreshProgress.completeTask() - self.refreshArticles(account, page: page) { - os_log(.debug, log: self.log, "Done refreshing articles.") - completion() - } - } - case .failure(let error): - os_log(.error, log: self.log, "Refresh articles failed: %@.", error.localizedDescription) - completion() - } - } - } - func refreshMissingArticles(_ account: Account, completion: @escaping VoidCompletionBlock) { account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in @@ -981,32 +929,6 @@ private extension ReaderAPIAccountDelegate { } } - func refreshArticles(_ account: Account, page: String?, completion: @escaping (() -> Void)) { - - guard let page = page else { - completion() - return - } - - caller.retrieveEntries(page: page) { result in - - switch result { - case .success(let (entries, nextPage)): - - self.processEntries(account: account, entries: entries) { - self.refreshProgress.completeTask() - self.refreshArticles(account, page: nextPage, completion: completion) - } - - case .failure(let error): - os_log(.error, log: self.log, "Refresh articles for additional pages failed: %@.", error.localizedDescription) - completion() - } - - } - - } - func processEntries(account: Account, entries: [ReaderAPIEntry]?, completion: @escaping VoidCompletionBlock) { let parsedItems = mapEntriesToParsedItems(account: account, entries: entries) let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift index a3e4fe06d..714403ebd 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPICaller.swift @@ -18,23 +18,23 @@ enum CreateReaderAPISubscriptionResult { final class ReaderAPICaller: NSObject { - struct ConditionalGetKeys { - static let subscriptions = "subscriptions" - static let tags = "tags" - static let unreadEntries = "unreadEntries" - static let starredEntries = "starredEntries" + enum ItemIDType { + case unread + case starred + case allForAccount + case allForFeed } - enum ReaderState: String { + private enum ReaderState: String { case read = "user/-/state/com.google/read" case starred = "user/-/state/com.google/starred" } - enum ReaderStreams: String { + private enum ReaderStreams: String { case readingList = "user/-/state/com.google/reading-list" } - enum ReaderAPIEndpoints: String { + private enum ReaderAPIEndpoints: String { case login = "/accounts/ClientLogin" case token = "/reader/api/0/token" case disableTag = "/reader/api/0/disable-tag" @@ -184,20 +184,16 @@ final class ReaderAPICaller: NSObject { return } - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.tags] - var request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + var request = URLRequest(url: callURL, credentials: credentials) addVariantHeaders(&request) transport.send(request: request, resultType: ReaderAPITagContainer.self) { result in - switch result { - case .success(let (response, wrapper)): - self.storeConditionalGet(key: ConditionalGetKeys.tags, headers: response.allHeaderFields) + case .success(let (_, wrapper)): completion(.success(wrapper?.tags)) case .failure(let error): completion(.failure(error)) } - } } @@ -292,22 +288,17 @@ final class ReaderAPICaller: NSObject { return } - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.subscriptions] - var request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + var request = URLRequest(url: callURL, credentials: credentials) addVariantHeaders(&request) transport.send(request: request, resultType: ReaderAPISubscriptionContainer.self) { result in - switch result { - case .success(let (response, container)): - self.storeConditionalGet(key: ConditionalGetKeys.subscriptions, headers: response.allHeaderFields) + case .success(let (_, container)): completion(.success(container?.subscriptions)) case .failure(let error): completion(.failure(error)) } - } - } func createSubscription(url: String, name: String?, folder: Folder?, completion: @escaping (Result) -> Void) { @@ -577,8 +568,14 @@ final class ReaderAPICaller: NSObject { request.httpMethod = "POST" // Get ids from above into hex representation of value - let idsToFetch = articleIDs.map({ (reference) -> String in - return "i=tag:google.com,2005:reader/item/\(reference)" + let idsToFetch = articleIDs.map({ articleID -> String in + if self.variant == .theOldReader { + return "i=tag:google.com,2005:reader/item/\(articleID)" + } else { + let idValue = Int(articleID)! + let idHexString = String(idValue, radix: 16, uppercase: false) + return "i=tag:google.com,2005:reader/item/\(idHexString)" + } }).joined(separator:"&") let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8) @@ -604,254 +601,154 @@ final class ReaderAPICaller: NSObject { } } - - func retrieveEntries(webFeedID: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) { - - let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() - + + func retrieveItemIDs(type: ItemIDType, webFeedID: String? = nil, completion: @escaping ((Result<[String], Error>) -> Void)) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return } + var queryItems = [ + URLQueryItem(name: "n", value: "1000"), + URLQueryItem(name: "output", value: "json") + ] + + switch type { + case .allForAccount: + let since: Date = { + if let lastArticleFetch = self.accountMetadata?.lastArticleFetchStartTime { + return lastArticleFetch + } else { + return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() + } + }() + + let sinceTimeInterval = since.timeIntervalSince1970 + queryItems.append(URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval)))) + queryItems.append(URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue)) + case .allForFeed: + guard let webFeedID = webFeedID else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } + let sinceTimeInterval = (Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()).timeIntervalSince1970 + queryItems.append(URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval)))) + queryItems.append(URLQueryItem(name: "s", value: webFeedID)) + case .unread: + queryItems.append(URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue)) + queryItems.append(URLQueryItem(name: "xt", value: ReaderState.read.rawValue)) + case .starred: + queryItems.append(URLQueryItem(name: "s", value: ReaderState.starred.rawValue)) + } + let url = baseURL .appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue) - .appendingQueryItems([ - URLQueryItem(name: "s", value: webFeedID), - URLQueryItem(name: "ot", value: String(Int(since.timeIntervalSince1970))), - URLQueryItem(name: "output", value: "json") - ]) + .appendingQueryItems(queryItems) guard let callURL = url else { completion(.failure(TransportError.noURL)) return } - var request = URLRequest(url: callURL, credentials: credentials, conditionalGet: nil) - addVariantHeaders(&request) - - transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in - - switch result { - case .success(let (_, unreadEntries)): - - guard let itemRefs = unreadEntries?.itemRefs else { - completion(.success(([], nil))) - return - } - - let itemIds = itemRefs.map { (reference) -> String in - if self.variant == .theOldReader { - return reference.itemId - } else { - // Convert the IDs to the (stupid) Google Hex Format - let idValue = Int(reference.itemId)! - return String(idValue, radix: 16, uppercase: false) - } - } - - self.retrieveEntries(articleIDs: itemIds) { (results) in - switch results { - case .success(let entries): - completion(.success((entries,nil))) - case .failure(let error): - completion(.failure(error)) - } - } - - case .failure(let error): - completion(.failure(error)) - } - - } - - } - - func retrieveEntries(completion: @escaping (Result<([ReaderAPIEntry]?, String?, Int?), Error>) -> Void) { - - guard let baseURL = APIBaseURL else { - completion(.failure(CredentialsError.incompleteCredentials)) - return - } - - let since: Date = { - if let lastArticleFetch = self.accountMetadata?.lastArticleFetchStartTime { - return lastArticleFetch - } else { - return Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date() - } - }() - - let sinceTimeInterval = since.timeIntervalSince1970 - let url = baseURL - .appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue) - .appendingQueryItems([ - URLQueryItem(name: "ot", value: String(Int(sinceTimeInterval))), - URLQueryItem(name: "n", value: "1000"), - URLQueryItem(name: "output", value: "json"), - URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue) - ]) - - guard let callURL = url else { - completion(.failure(TransportError.noURL)) - return - } - - var request = URLRequest(url: callURL, credentials: credentials) + var request: URLRequest = URLRequest(url: callURL, credentials: credentials) addVariantHeaders(&request) self.transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in - switch result { case .success(let (response, entries)): - guard let entriesItemRefs = entries?.itemRefs, entriesItemRefs.count > 0 else { - completion(.success((nil, nil, nil))) + completion(.success([String]())) return } - - // This needs to be moved when we fix paging for item ids let dateInfo = HTTPDateInfo(urlResponse: response) + let itemIDs = entriesItemRefs.compactMap { $0.itemId } + self.retrieveItemIDs(type: type, url: callURL, dateInfo: dateInfo, itemIDs: itemIDs, continuation: entries?.continuation, completion: completion) + case .failure(let error): + completion(.failure(error)) + } + } + } + + func retrieveItemIDs(type: ItemIDType, url: URL, dateInfo: HTTPDateInfo?, itemIDs: [String], continuation: String?, completion: @escaping ((Result<[String], Error>) -> Void)) { + guard let continuation = continuation else { + if type == .allForAccount { self.accountMetadata?.lastArticleFetchStartTime = dateInfo?.date self.accountMetadata?.lastArticleFetchEndTime = Date() - - self.requestAuthorizationToken(endpoint: baseURL) { (result) in - switch result { - case .success(let token): - var request = URLRequest(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.contents.rawValue), credentials: self.credentials) - self.addVariantHeaders(&request) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - - let chunkedItemRefs = entriesItemRefs.chunked(into: 200) - let group = DispatchGroup() - var groupEntries = [ReaderAPIEntry]() - var groupError: Error? = nil - - for itemRefsChunk in chunkedItemRefs { - let itemFetchParameters = itemRefsChunk.map({ itemRef -> String in - if self.variant == .theOldReader { - return "i=tag:google.com,2005:reader/item/\(itemRef.itemId)" - } else { - let idValue = Int(itemRef.itemId)! - let idHexString = String(idValue, radix: 16, uppercase: false) - return "i=tag:google.com,2005:reader/item/\(idHexString)" - } - }).joined(separator:"&") - - let postData = "T=\(token)&output=json&\(itemFetchParameters)".data(using: String.Encoding.utf8) - - group.enter() - self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIEntryWrapper.self, completion: { (result) in - switch result { - case .success(let (_, entryWrapper)): - guard let entryWrapper = entryWrapper else { - completion(.failure(ReaderAPIAccountDelegateError.invalidResponse)) - return - } - groupEntries.append(contentsOf: entryWrapper.entries) - group.leave() - case .failure(let error): - groupError = error - group.leave() - } - }) - } - - group.notify(queue: DispatchQueue.main) { - if let error = groupError { - completion(.failure(error)) - } else { - completion(.success((groupEntries, nil, nil))) - } - } - case .failure(let error): - completion(.failure(error)) - } - } - - case .failure(let error): - self.accountMetadata?.lastArticleFetchStartTime = nil - completion(.failure(error)) } - - } - } - - func retrieveEntries(page: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) { - - guard let url = URL(string: page)?.appendingQueryItem(URLQueryItem(name: "mode", value: "extended")) else { - completion(.success((nil, nil))) - return - } - var request = URLRequest(url: url, credentials: credentials) - addVariantHeaders(&request) - - transport.send(request: request, resultType: [ReaderAPIEntry].self) { result in - - switch result { - case .success(let (response, entries)): - - let pagingInfo = HTTPLinkPagingInfo(urlResponse: response) - completion(.success((entries, pagingInfo.nextPage))) - - case .failure(let error): - self.accountMetadata?.lastArticleFetchStartTime = nil - completion(.failure(error)) - } - - } - - } - - func retrieveUnreadEntries(completion: @escaping (Result<[String]?, Error>) -> Void) { - - guard let baseURL = APIBaseURL else { - completion(.failure(CredentialsError.incompleteCredentials)) + completion(.success(itemIDs)) return } - let url = baseURL - .appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue) - .appendingQueryItems([ - URLQueryItem(name: "s", value: ReaderStreams.readingList.rawValue), - URLQueryItem(name: "n", value: "1000"), - URLQueryItem(name: "xt", value: ReaderState.read.rawValue), - URLQueryItem(name: "output", value: "json") - ]) + guard var urlComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else { + completion(.failure(ReaderAPIAccountDelegateError.invalidParameter)) + return + } - guard let callURL = url else { + var queryItems = urlComponents.queryItems!.filter({ $0.name != "c" }) + queryItems.append(URLQueryItem(name: "c", value: continuation)) + urlComponents.queryItems = queryItems + + guard let callURL = urlComponents.url else { completion(.failure(TransportError.noURL)) return } - - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.unreadEntries] - var request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) + + var request: URLRequest = URLRequest(url: callURL, credentials: credentials) addVariantHeaders(&request) - transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in - + self.transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in switch result { - case .success(let (response, unreadEntries)): - - guard let itemRefs = unreadEntries?.itemRefs else { - completion(.success([])) + case .success(let (_, entries)): + guard let entriesItemRefs = entries?.itemRefs, entriesItemRefs.count > 0 else { + self.retrieveItemIDs(type: type, url: callURL, dateInfo: dateInfo, itemIDs: itemIDs, continuation: entries?.continuation, completion: completion) return } - - let itemIds = itemRefs.map { $0.itemId } - - self.storeConditionalGet(key: ConditionalGetKeys.unreadEntries, headers: response.allHeaderFields) - completion(.success(itemIds)) + var totalItemIDs = itemIDs + totalItemIDs.append(contentsOf: entriesItemRefs.compactMap { $0.itemId }) + self.retrieveItemIDs(type: type, url: callURL, dateInfo: dateInfo, itemIDs: totalItemIDs, continuation: entries?.continuation, completion: completion) case .failure(let error): completion(.failure(error)) } - } - } - func updateStateToEntries(entries: [String], state: ReaderState, add: Bool, completion: @escaping (Result) -> Void) { + func createUnreadEntries(entries: [String], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .read, add: false, completion: completion) + } + + func deleteUnreadEntries(entries: [String], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .read, add: true, completion: completion) + } + + func createStarredEntries(entries: [String], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .starred, add: true, completion: completion) + } + + func deleteStarredEntries(entries: [String], completion: @escaping (Result) -> Void) { + updateStateToEntries(entries: entries, state: .starred, add: false, completion: completion) + } + +} + +// MARK: Private + +private extension ReaderAPICaller { + + func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { + if var conditionalGet = accountMetadata?.conditionalGetInfo { + conditionalGet[key] = HTTPConditionalGetInfo(headers: headers) + accountMetadata?.conditionalGetInfo = conditionalGet + } + } + + func addVariantHeaders(_ request: inout URLRequest) { + if variant == .inoreader { + request.addValue(SecretsManager.provider.inoreaderAppId, forHTTPHeaderField: "AppId") + request.addValue(SecretsManager.provider.inoreaderAppKey, forHTTPHeaderField: "AppKey") + } + } + + private func updateStateToEntries(entries: [String], state: ReaderState, add: Bool, completion: @escaping (Result) -> Void) { guard let baseURL = APIBaseURL else { completion(.failure(CredentialsError.incompleteCredentials)) return @@ -897,85 +794,5 @@ final class ReaderAPICaller: NSObject { } } - func createUnreadEntries(entries: [String], completion: @escaping (Result) -> Void) { - updateStateToEntries(entries: entries, state: .read, add: false, completion: completion) - } - - func deleteUnreadEntries(entries: [String], completion: @escaping (Result) -> Void) { - updateStateToEntries(entries: entries, state: .read, add: true, completion: completion) - } - - func createStarredEntries(entries: [String], completion: @escaping (Result) -> Void) { - updateStateToEntries(entries: entries, state: .starred, add: true, completion: completion) - - } - - func deleteStarredEntries(entries: [String], completion: @escaping (Result) -> Void) { - updateStateToEntries(entries: entries, state: .starred, add: false, completion: completion) - } - - func retrieveStarredEntries(completion: @escaping (Result<[String]?, Error>) -> Void) { - guard let baseURL = APIBaseURL else { - completion(.failure(CredentialsError.incompleteCredentials)) - return - } - - let url = baseURL - .appendingPathComponent(ReaderAPIEndpoints.itemIds.rawValue) - .appendingQueryItems([ - URLQueryItem(name: "s", value: ReaderState.starred.rawValue), - URLQueryItem(name: "n", value: "1000"), - URLQueryItem(name: "output", value: "json") - ]) - - guard let callURL = url else { - completion(.failure(TransportError.noURL)) - return - } - - let conditionalGet = accountMetadata?.conditionalGetInfo[ConditionalGetKeys.starredEntries] - var request = URLRequest(url: callURL, credentials: credentials, conditionalGet: conditionalGet) - addVariantHeaders(&request) - - transport.send(request: request, resultType: ReaderAPIReferenceWrapper.self) { result in - - switch result { - case .success(let (response, unreadEntries)): - guard let itemRefs = unreadEntries?.itemRefs else { - completion(.success([])) - return - } - - let itemIds = itemRefs.map { $0.itemId } - self.storeConditionalGet(key: ConditionalGetKeys.starredEntries, headers: response.allHeaderFields) - completion(.success(itemIds)) - case .failure(let error): - completion(.failure(error)) - } - - } - - } - -} - -// MARK: Private - -private extension ReaderAPICaller { - - func storeConditionalGet(key: String, headers: [AnyHashable : Any]) { - if var conditionalGet = accountMetadata?.conditionalGetInfo { - conditionalGet[key] = HTTPConditionalGetInfo(headers: headers) - accountMetadata?.conditionalGetInfo = conditionalGet - } - } - - func addVariantHeaders(_ request: inout URLRequest) { - if variant == .inoreader { - request.addValue(SecretsManager.provider.inoreaderAppId, forHTTPHeaderField: "AppId") - request.addValue(SecretsManager.provider.inoreaderAppKey, forHTTPHeaderField: "AppKey") - } - } - } diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift index 147310f34..4fe3fde89 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIUnreadEntry.swift @@ -19,7 +19,7 @@ struct ReaderAPIReferenceWrapper: Codable { } struct ReaderAPIReference: Codable { - let itemId: String + let itemId: String? enum CodingKeys: String, CodingKey { case itemId = "id"