Merge branch 'main' into mac-add-account-refresh
This commit is contained in:
commit
0c9336a1ff
|
@ -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, Error>) -> 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, Error>) -> 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, Error>) -> 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) }
|
||||
|
|
|
@ -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<CreateReaderAPISubscriptionResult, Error>) -> Void) {
|
||||
|
@ -576,297 +567,188 @@ final class ReaderAPICaller: NSObject {
|
|||
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
request.httpMethod = "POST"
|
||||
|
||||
let chunkedArticleIds = articleIDs.chunked(into: 200)
|
||||
let group = DispatchGroup()
|
||||
var groupEntries = [ReaderAPIEntry]()
|
||||
var groupError: Error? = nil
|
||||
|
||||
for articleIDChunk in chunkedArticleIds {
|
||||
let itemFetchParameters = articleIDChunk.map({ articleID -> String in
|
||||
return "i=tag:google.com,2005:reader/item/\(articleID)"
|
||||
}).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))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(webFeedID: String, completion: @escaping (Result<([ReaderAPIEntry]?, String?), Error>) -> Void) {
|
||||
|
||||
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date()) ?? Date()
|
||||
|
||||
guard let baseURL = APIBaseURL else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
}
|
||||
|
||||
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")
|
||||
])
|
||||
|
||||
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
|
||||
// Get ids from above into hex representation of value
|
||||
let idsToFetch = articleIDs.map({ articleID -> String in
|
||||
if self.variant == .theOldReader {
|
||||
return reference.itemId
|
||||
return "i=tag:google.com,2005:reader/item/\(articleID)"
|
||||
} else {
|
||||
// Convert the IDs to the (stupid) Google Hex Format
|
||||
let idValue = Int(reference.itemId)!
|
||||
return String(idValue, radix: 16, uppercase: false)
|
||||
let idValue = Int(articleID)!
|
||||
let idHexString = String(idValue, radix: 16, uppercase: false)
|
||||
return "i=tag:google.com,2005:reader/item/\(idHexString)"
|
||||
}
|
||||
}
|
||||
}).joined(separator:"&")
|
||||
|
||||
self.retrieveEntries(articleIDs: itemIds) { (results) in
|
||||
switch results {
|
||||
case .success(let entries):
|
||||
completion(.success((entries,nil)))
|
||||
let postData = "T=\(token)&output=json&\(idsToFetch)".data(using: String.Encoding.utf8)
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
completion(.success((entryWrapper.entries)))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveEntries(completion: @escaping (Result<([ReaderAPIEntry]?, String?, Int?), Error>) -> Void) {
|
||||
|
||||
}
|
||||
|
||||
func retrieveItemIDs(type: ItemIDType, webFeedID: String? = nil, completion: @escaping ((Result<[String], 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()
|
||||
}
|
||||
}()
|
||||
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 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)
|
||||
])
|
||||
.appendingQueryItems(queryItems)
|
||||
|
||||
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, Error>) -> Void) {
|
||||
func createUnreadEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .read, add: false, completion: completion)
|
||||
}
|
||||
|
||||
func deleteUnreadEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .read, add: true, completion: completion)
|
||||
}
|
||||
|
||||
func createStarredEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .starred, add: true, completion: completion)
|
||||
}
|
||||
|
||||
func deleteStarredEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> 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, Error>) -> Void) {
|
||||
guard let baseURL = APIBaseURL else {
|
||||
completion(.failure(CredentialsError.incompleteCredentials))
|
||||
return
|
||||
|
@ -912,85 +794,5 @@ final class ReaderAPICaller: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
func createUnreadEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .read, add: false, completion: completion)
|
||||
}
|
||||
|
||||
func deleteUnreadEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .read, add: true, completion: completion)
|
||||
|
||||
}
|
||||
|
||||
func createStarredEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
updateStateToEntries(entries: entries, state: .starred, add: true, completion: completion)
|
||||
|
||||
}
|
||||
|
||||
func deleteStarredEntries(entries: [String], completion: @escaping (Result<Void, Error>) -> 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")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ struct ReaderAPIReferenceWrapper: Codable {
|
|||
}
|
||||
|
||||
struct ReaderAPIReference: Codable {
|
||||
let itemId: String
|
||||
let itemId: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case itemId = "id"
|
||||
|
|
|
@ -72,18 +72,23 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
|||
accountsFeedbinWindowController.account = account
|
||||
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFeedbinWindowController
|
||||
case .freshRSS:
|
||||
let accountsFreshRSSWindowController = AccountsReaderAPIWindowController()
|
||||
accountsFreshRSSWindowController.accountType = account.type
|
||||
accountsFreshRSSWindowController.account = account
|
||||
accountsFreshRSSWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFreshRSSWindowController
|
||||
case .inoreader, .bazQux, .theOldReader, .freshRSS:
|
||||
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
|
||||
accountsReaderAPIWindowController.accountType = account.type
|
||||
accountsReaderAPIWindowController.account = account
|
||||
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsReaderAPIWindowController
|
||||
break
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
accountsFeedWranglerWindowController.account = account
|
||||
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFeedWranglerWindowController
|
||||
case .newsBlur:
|
||||
let accountsNewsBlurWindowController = AccountsNewsBlurWindowController()
|
||||
accountsNewsBlurWindowController.account = account
|
||||
accountsNewsBlurWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsNewsBlurWindowController
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ class AccountsFeedWranglerWindowController: NSWindowController {
|
|||
return
|
||||
}
|
||||
|
||||
guard !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: usernameTextField.stringValue) else {
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: usernameTextField.stringValue) else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a FeedWrangler account with that username created.", comment: "Duplicate Error")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -56,7 +56,7 @@ class AccountsNewsBlurWindowController: NSWindowController {
|
|||
return
|
||||
}
|
||||
|
||||
guard !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: usernameTextField.stringValue) else {
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: usernameTextField.stringValue) else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ class AccountsReaderAPIWindowController: NSWindowController {
|
|||
return
|
||||
}
|
||||
|
||||
guard !AccountManager.shared.duplicateServiceAccount(type: accountType, username: usernameTextField.stringValue) else {
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: accountType, username: usernameTextField.stringValue) else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("There is already an account of this type with that username created.", comment: "Duplicate Error")
|
||||
return
|
||||
}
|
||||
|
|
|
@ -80,7 +80,7 @@ class FeedWranglerAccountViewController: UITableViewController {
|
|||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: trimmedEmail) else {
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: trimmedEmail) else {
|
||||
showError(NSLocalizedString("There is already a FeedWrangler account with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
|
|
@ -80,22 +80,28 @@ class NewsBlurAccountViewController: UITableViewController {
|
|||
return
|
||||
}
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: trimmedUsername) else {
|
||||
showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
||||
let password = passwordTextField.text ?? ""
|
||||
|
||||
startAnimatingActivityIndicator()
|
||||
disableNavigation()
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
let credentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
|
||||
Account.validateCredentials(type: .newsBlur, credentials: credentials) { result in
|
||||
let basicCredentials = Credentials(type: .newsBlurBasic, username: trimmedUsername, secret: password)
|
||||
Account.validateCredentials(type: .newsBlur, credentials: basicCredentials) { result in
|
||||
|
||||
self.stopAnimatingActivityIndicator()
|
||||
self.enableNavigation()
|
||||
|
||||
switch result {
|
||||
case .success(let credentials):
|
||||
if let credentials = credentials {
|
||||
case .success(let sessionCredentials):
|
||||
if let sessionCredentials = sessionCredentials {
|
||||
var newAccount = false
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .newsBlur)
|
||||
|
@ -106,8 +112,10 @@ class NewsBlurAccountViewController: UITableViewController {
|
|||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .newsBlurBasic)
|
||||
try self.account?.removeCredentials(type: .newsBlurSessionId)
|
||||
} catch {}
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(basicCredentials)
|
||||
try self.account?.storeCredentials(sessionCredentials)
|
||||
|
||||
if newAccount {
|
||||
self.account?.refreshAll() { result in
|
||||
|
|
|
@ -21,7 +21,6 @@ class ReaderAPIAccountViewController: UITableViewController {
|
|||
@IBOutlet weak var showHideButton: UIButton!
|
||||
@IBOutlet weak var actionButton: UIButton!
|
||||
|
||||
|
||||
weak var account: Account?
|
||||
var accountType: AccountType?
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
@ -109,7 +108,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
|||
}
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
guard validateDataEntry() else {
|
||||
guard validateDataEntry(), let type = accountType else {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -117,15 +116,18 @@ class ReaderAPIAccountViewController: UITableViewController {
|
|||
let password = passwordTextField.text!
|
||||
let url = apiURL()!
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else {
|
||||
showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
||||
startAnimatingActivityIndicator()
|
||||
disableNavigation()
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password)
|
||||
guard let type = accountType else {
|
||||
return
|
||||
}
|
||||
Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in
|
||||
|
||||
self.stopAnimatingActivityIndicator()
|
||||
|
|
|
@ -58,6 +58,25 @@ class AccountInspectorViewController: UITableViewController {
|
|||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .feedWrangler:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedWranglerAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! FeedWranglerAccountViewController
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .newsBlur:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! NewsBlurAccountViewController
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .inoreader, .bazQux, .theOldReader, .freshRSS:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "ReaderAPIAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! ReaderAPIAccountViewController
|
||||
addViewController.accountType = account.type
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue