Use an operation queue to renew the Reddit access token so that we don't do it multiple times in a refresh cycle

This commit is contained in:
Maurice Parker 2020-08-12 19:10:41 -05:00
parent d6ed015514
commit ad48042e42
2 changed files with 100 additions and 31 deletions

View File

@ -10,6 +10,7 @@ import Foundation
import os.log import os.log
import OAuthSwift import OAuthSwift
import Secrets import Secrets
import RSCore
import RSParser import RSParser
import RSWeb import RSWeb
@ -34,7 +35,7 @@ public enum RedditFeedType: Int {
case subreddit = 3 case subreddit = 3
} }
public final class RedditFeedProvider: FeedProvider { public final class RedditFeedProvider: FeedProvider, RedditFeedProviderTokenRefreshOperationDelegate {
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider") var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider")
@ -48,14 +49,16 @@ public final class RedditFeedProvider: FeedProvider {
"all": NSLocalizedString("All", comment: "All") "all": NSLocalizedString("All", comment: "All")
] ]
private let operationQueue = MainThreadOperationQueue()
private var parsingQueue = DispatchQueue(label: "RedditFeedProvider parse queue") private var parsingQueue = DispatchQueue(label: "RedditFeedProvider parse queue")
public var username: String? public var username: String?
private var oauthToken: String var oauthTokenLastRefresh: Date?
private var oauthRefreshToken: String var oauthToken: String
var oauthRefreshToken: String
private var oauthSwift: OAuth2Swift? var oauthSwift: OAuth2Swift?
private var client: OAuthSwiftClient? { private var client: OAuthSwiftClient? {
return oauthSwift?.client return oauthSwift?.client
} }
@ -259,6 +262,13 @@ public final class RedditFeedProvider: FeedProvider {
return components.url 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 // MARK: OAuth1SwiftProvider
@ -412,41 +422,22 @@ private extension RedditFeedProvider {
func handleFailure(error: OAuthSwiftError, completion: @escaping (Error?) -> Void) { func handleFailure(error: OAuthSwiftError, completion: @escaping (Error?) -> Void) {
if case .tokenExpired = error { 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 op.completionBlock = { operation in
guard let strongSelf = self, let username = strongSelf.username else { let refreshOperation = operation as! RedditFeedProviderTokenRefreshOperation
if let error = refreshOperation.error {
completion(error)
} else {
completion(nil) 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 { } else {
completion(error) 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)
}
} }

View File

@ -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)
}
}
}
}