From ad48042e42f555625fd7f55cdefc282a696e0248 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Wed, 12 Aug 2020 19:10:41 -0500 Subject: [PATCH] Use an operation queue to renew the Reddit access token so that we don't do it multiple times in a refresh cycle --- .../Reddit/RedditFeedProvider.swift | 53 ++++++------- ...ditFeedProviderTokenRefreshOperation.swift | 78 +++++++++++++++++++ 2 files changed, 100 insertions(+), 31 deletions(-) create mode 100644 Account/Sources/Account/FeedProvider/Reddit/RedditFeedProviderTokenRefreshOperation.swift diff --git a/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProvider.swift b/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProvider.swift index e244df51c..91f87218e 100644 --- a/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProvider.swift +++ b/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProvider.swift @@ -10,6 +10,7 @@ import Foundation import os.log import OAuthSwift import Secrets +import RSCore import RSParser import RSWeb @@ -34,7 +35,7 @@ public enum RedditFeedType: Int { case subreddit = 3 } -public final class RedditFeedProvider: FeedProvider { +public final class RedditFeedProvider: FeedProvider, RedditFeedProviderTokenRefreshOperationDelegate { var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider") @@ -48,14 +49,16 @@ public final class RedditFeedProvider: FeedProvider { "all": NSLocalizedString("All", comment: "All") ] + private let operationQueue = MainThreadOperationQueue() private var parsingQueue = DispatchQueue(label: "RedditFeedProvider parse queue") public var username: String? - private var oauthToken: String - private var oauthRefreshToken: String + var oauthTokenLastRefresh: Date? + var oauthToken: String + var oauthRefreshToken: String - private var oauthSwift: OAuth2Swift? + var oauthSwift: OAuth2Swift? private var client: OAuthSwiftClient? { return oauthSwift?.client } @@ -259,6 +262,13 @@ public final class RedditFeedProvider: FeedProvider { return components.url } + static func storeCredentials(username: String, oauthToken: String, oauthRefreshToken: String) throws { + let tokenCredentials = Credentials(type: .oauthAccessToken, username: username, secret: oauthToken) + try CredentialsManager.storeCredentials(tokenCredentials, server: Self.server) + let tokenSecretCredentials = Credentials(type: .oauthRefreshToken, username: username, secret: oauthRefreshToken) + try CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server) + } + } // MARK: OAuth1SwiftProvider @@ -412,41 +422,22 @@ private extension RedditFeedProvider { func handleFailure(error: OAuthSwiftError, completion: @escaping (Error?) -> Void) { if case .tokenExpired = error { - os_log(.debug, log: self.log, "Access token expired, attempting to renew...") + let op = RedditFeedProviderTokenRefreshOperation(delegate: self) - oauthSwift?.renewAccessToken(withRefreshToken: oauthRefreshToken) { [weak self] result in - guard let strongSelf = self, let username = strongSelf.username else { + op.completionBlock = { operation in + let refreshOperation = operation as! RedditFeedProviderTokenRefreshOperation + if let error = refreshOperation.error { + completion(error) + } else { completion(nil) - return - } - - switch result { - case .success(let tokenSuccess): - strongSelf.oauthToken = tokenSuccess.credential.oauthToken - strongSelf.oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken - do { - try Self.storeCredentials(username: username, oauthToken: strongSelf.oauthToken, oauthRefreshToken: strongSelf.oauthRefreshToken) - os_log(.debug, log: strongSelf.log, "Access token renewed.") - } catch { - completion(error) - return - } - completion(nil) - case .failure(let oathError): - completion(oathError) } } + operationQueue.add(op) + } else { completion(error) } } - static func storeCredentials(username: String, oauthToken: String, oauthRefreshToken: String) throws { - let tokenCredentials = Credentials(type: .oauthAccessToken, username: username, secret: oauthToken) - try CredentialsManager.storeCredentials(tokenCredentials, server: Self.server) - let tokenSecretCredentials = Credentials(type: .oauthRefreshToken, username: username, secret: oauthRefreshToken) - try CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server) - } - } diff --git a/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProviderTokenRefreshOperation.swift b/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProviderTokenRefreshOperation.swift new file mode 100644 index 000000000..1b2b6bc73 --- /dev/null +++ b/Account/Sources/Account/FeedProvider/Reddit/RedditFeedProviderTokenRefreshOperation.swift @@ -0,0 +1,78 @@ +// +// RedditFeedProviderTokenRefreshOperation.swift +// +// +// Created by Maurice Parker on 8/12/20. +// + +import Foundation +import os.log +import RSCore +import OAuthSwift +import Secrets + +protocol RedditFeedProviderTokenRefreshOperationDelegate: class { + var username: String? { get } + var oauthTokenLastRefresh: Date? { get set } + var oauthToken: String { get set } + var oauthRefreshToken: String { get set } + var oauthSwift: OAuth2Swift? { get } +} + +class RedditFeedProviderTokenRefreshOperation: MainThreadOperation { + + var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider") + + public var isCanceled = false + public var id: Int? + public weak var operationDelegate: MainThreadOperationDelegate? + public var name: String? = "WebViewProviderReplenishQueueOperation" + public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? + + private weak var delegate: RedditFeedProviderTokenRefreshOperationDelegate? + + var error: Error? + + init(delegate: RedditFeedProviderTokenRefreshOperationDelegate) { + self.delegate = delegate + } + + func run() { + guard let delegate = delegate, let username = delegate.username else { + self.operationDelegate?.operationDidComplete(self) + return + } + + // If another operation has recently refreshed the token, we don't need to do it again + if let lastRefresh = delegate.oauthTokenLastRefresh, Date().timeIntervalSince(lastRefresh) < 120 { + self.operationDelegate?.operationDidComplete(self) + return + } + + os_log(.debug, log: self.log, "Access token expired, attempting to renew...") + + delegate.oauthSwift?.renewAccessToken(withRefreshToken: delegate.oauthRefreshToken) { [weak self] result in + guard let self = self else { return } + + switch result { + case .success(let tokenSuccess): + delegate.oauthToken = tokenSuccess.credential.oauthToken + delegate.oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken + do { + try RedditFeedProvider.storeCredentials(username: username, oauthToken: delegate.oauthToken, oauthRefreshToken: delegate.oauthRefreshToken) + delegate.oauthTokenLastRefresh = Date() + os_log(.debug, log: self.log, "Access token renewed.") + } catch { + self.error = error + self.operationDelegate?.operationDidComplete(self) + } + self.operationDelegate?.operationDidComplete(self) + case .failure(let oathError): + self.error = oathError + self.operationDelegate?.operationDidComplete(self) + } + } + + } + +}