Make authorize endpoint async

This commit is contained in:
Maurice Parker 2020-05-03 13:23:36 -05:00
parent 578d22f3c2
commit ccd600b880
7 changed files with 175 additions and 54 deletions

View File

@ -62,6 +62,7 @@
516896352448EBEA00185AC5 /* FeedProviderManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516896342448EBEA00185AC5 /* FeedProviderManager.swift */; };
5170743C232AEDB500A461A3 /* OPMLFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5170743B232AEDB500A461A3 /* OPMLFile.swift */; };
5193CD54245E3F7A0092735E /* RedditFeedProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */; };
5193CD81245F295E0092735E /* RedditUser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD80245F295E0092735E /* RedditUser.swift */; };
519E84A62433D49000D238B0 /* OPMLNormalizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */; };
519E84A82434C5EF00D238B0 /* CloudKitArticlesZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */; };
519E84AC2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */; };
@ -317,6 +318,7 @@
516896342448EBEA00185AC5 /* FeedProviderManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedProviderManager.swift; sourceTree = "<group>"; };
5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = "<group>"; };
5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditFeedProvider.swift; sourceTree = "<group>"; };
5193CD80245F295E0092735E /* RedditUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditUser.swift; sourceTree = "<group>"; };
519E84A52433D49000D238B0 /* OPMLNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLNormalizer.swift; sourceTree = "<group>"; };
519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZone.swift; sourceTree = "<group>"; };
519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZoneDelegate.swift; sourceTree = "<group>"; };
@ -631,6 +633,7 @@
isa = PBXGroup;
children = (
5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */,
5193CD80245F295E0092735E /* RedditUser.swift */,
);
path = Reddit;
sourceTree = "<group>";
@ -1216,6 +1219,7 @@
9E1D15512334282100F4944C /* FeedlyMirrorCollectionsAsFoldersOperation.swift in Sources */,
9E1773D7234575AB0056A5A8 /* FeedlyTag.swift in Sources */,
3B826DAB2385C81C00FC1ADB /* FeedWranglerConfig.swift in Sources */,
5193CD81245F295E0092735E /* RedditUser.swift in Sources */,
515E4EB62324FF8C0057B0E7 /* URLRequest+RSWeb.swift in Sources */,
51B36315244BCCA4000DEF2A /* TwitterSearchResult.swift in Sources */,
9EB1D576238E6A3900A753D7 /* FeedlyAddNewFeedOperation.swift in Sources */,

View File

@ -31,51 +31,32 @@ public struct RedditFeedProvider: FeedProvider {
private static let userPaths = ["/home", "/notifications"]
private static let reservedPaths = ["/search", "/explore", "/messages", "/i", "/compose"]
public var username: String
public var username: String?
private var oauthToken: String
private var oauthTokenSecret: String
private var oauthRefreshToken: String
private var client: OAuthSwiftClient
public init?(tokenSuccess: OAuthSwift.TokenSuccess) {
guard let username = tokenSuccess.parameters["screen_name"] as? String else {
return nil
}
self.username = username
self.oauthToken = tokenSuccess.credential.oauthToken
self.oauthTokenSecret = tokenSuccess.credential.oauthTokenSecret
let tokenCredentials = Credentials(type: .oauthAccessToken, username: username, secret: oauthToken)
try? CredentialsManager.storeCredentials(tokenCredentials, server: Self.server)
let tokenSecretCredentials = Credentials(type: .oauthAccessTokenSecret, username: username, secret: oauthTokenSecret)
try? CredentialsManager.storeCredentials(tokenSecretCredentials, server: Self.server)
client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey,
consumerSecret: Secrets.twitterConsumerSecret,
oauthToken: oauthToken,
oauthTokenSecret: oauthTokenSecret,
version: .oauth1)
private var oauthSwift: OAuth2Swift?
private var client: OAuthSwiftClient? {
return oauthSwift?.client
}
public init?(username: String) {
self.username = username
guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: username),
let tokenSecretCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessTokenSecret, server: Self.server, username: username) else {
let refreshTokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthRefreshToken, server: Self.server, username: username) else {
return nil
}
self.oauthToken = tokenCredentials.secret
self.oauthTokenSecret = tokenSecretCredentials.secret
self.init(oauthToken: tokenCredentials.secret, oauthRefreshToken: refreshTokenCredentials.secret)
self.username = username
}
client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey,
consumerSecret: Secrets.twitterConsumerSecret,
oauthToken: oauthToken,
oauthTokenSecret: oauthTokenSecret,
version: .oauth1)
init(oauthToken: String, oauthRefreshToken: String) {
self.oauthToken = oauthToken
self.oauthRefreshToken = oauthRefreshToken
oauthSwift = Self.oauth2Swift
oauthSwift!.client.credential.oauthToken = oauthToken
oauthSwift!.client.credential.oauthRefreshToken = oauthRefreshToken
}
public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility {
@ -125,6 +106,34 @@ public struct RedditFeedProvider: FeedProvider {
completion(.success(Set<ParsedItem>()))
}
public static func create(tokenSuccess: OAuthSwift.TokenSuccess, completion: @escaping (Result<RedditFeedProvider, Error>) -> Void) {
let oauthToken = tokenSuccess.credential.oauthToken
let oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken
var redditFeedProvider = RedditFeedProvider(oauthToken: oauthToken, oauthRefreshToken: oauthRefreshToken)
redditFeedProvider.retrieveUserName() { result in
switch result {
case .success(let username):
do {
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)
redditFeedProvider.username = username
completion(.success(redditFeedProvider))
} catch {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}
// MARK: OAuth1SwiftProvider
@ -132,7 +141,37 @@ public struct RedditFeedProvider: FeedProvider {
extension RedditFeedProvider: OAuth2SwiftProvider {
public static var oauth2Swift: OAuth2Swift {
return OAuth2Swift(consumerKey: "", consumerSecret: "", authorizeUrl: "", accessTokenUrl: "", responseType: "")
let oauth2 = OAuth2Swift(consumerKey: Secrets.redditConsumerKey,
consumerSecret: "",
authorizeUrl: "https://www.reddit.com/api/v1/authorize.compact?",
accessTokenUrl: "https://www.reddit.com/api/v1/access_token",
responseType: "token")
oauth2.accessTokenBasicAuthentification = true
return oauth2
}
}
private extension RedditFeedProvider {
func retrieveUserName(completion: @escaping (Result<String, Error>) -> Void) {
guard let client = client else {
completion(.failure(RedditFeedProviderError.unknown))
return
}
client.request(Self.apiBase + "/api/v1/me", method: .GET) { result in
switch result {
case .success(let response):
if let redditUser = try? JSONDecoder().decode(RedditUser.self, from: response.data), let username = redditUser.name {
completion(.success(username))
} else {
completion(.failure(RedditFeedProviderError.unknown))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}

View File

@ -0,0 +1,40 @@
//
// RedditUser.swift
// Account
//
// Created by Maurice Parker on 5/3/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct RedditUser: Codable {
let name: String?
enum CodingKeys: String, CodingKey {
case name = "name"
}
// var url: String {
// return "https://twitter.com/\(screenName ?? "")"
// }
//
// func renderAsHTML() -> String? {
// var html = String()
// html += "<div><a href=\"\(url)\">"
// if let avatarURL = avatarURL {
// html += "<img class=\"twitterAvatar\" src=\"\(avatarURL)\">"
// }
// html += "<span class=\"twitterUsername\">"
// if let name = name {
// html += " \(name)"
// }
// if let screenName = screenName {
// html += " @\(screenName)"
// }
// html += "</span></a></div>"
// return html
// }
}

View File

@ -2,7 +2,7 @@
%{
import os
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET']
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET', 'REDDIT_CONSUMER_KEY']
def chunks(seq, size):
return (seq[i:(i + size)] for i in range(0, len(seq), size))

View File

@ -59,8 +59,12 @@ class ExtensionPointEnableWindowController: NSWindowController {
if let oauth1 = extensionPointType as? OAuth1SwiftProvider.Type {
enableOauth1(oauth1)
} else {
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType) { result in
if case .failure(let error) = result {
self.presentError(error)
}
self.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
}
}
}
@ -120,8 +124,12 @@ private extension ExtensionPointEnableWindowController {
switch result {
case .success(let tokenSuccess):
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess)
self.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
ExtensionPointManager.shared.activateExtensionPoint(extensionPointType, tokenSuccess: tokenSuccess) { result in
if case .failure(let error) = result {
self.presentError(error)
}
self.hostWindow!.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
}
case .failure(let oauthSwiftError):
NSApplication.shared.presentError(oauthSwiftError)
}

View File

@ -15,6 +15,18 @@ public extension Notification.Name {
static let ActiveExtensionPointsDidChange = Notification.Name(rawValue: "ActiveExtensionPointsDidChange")
}
public enum ExtensionPointManagerError: LocalizedError {
case unableToCreate
public var localizedDescription: String {
switch self {
case .unableToCreate:
return NSLocalizedString("Unable to create extension.", comment: "Unable to create extension")
}
}
}
final class ExtensionPointManager: FeedProviderManagerDelegate {
static let shared = ExtensionPointManager()
@ -74,10 +86,16 @@ final class ExtensionPointManager: FeedProviderManagerDelegate {
loadExtensionPoints()
}
func activateExtensionPoint(_ extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess? = nil) {
if let extensionPoint = self.extensionPoint(for: extensionPointType, tokenSuccess: tokenSuccess) {
activeExtensionPoints[extensionPoint.extensionPointID] = extensionPoint
saveExtensionPointIDs()
func activateExtensionPoint(_ extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
self.extensionPoint(for: extensionPointType, tokenSuccess: tokenSuccess) { result in
switch result {
case .success(let extensionPoint):
self.activeExtensionPoints[extensionPoint.extensionPointID] = extensionPoint
self.saveExtensionPointIDs()
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
@ -105,30 +123,36 @@ private extension ExtensionPointManager {
NotificationCenter.default.post(name: .ActiveExtensionPointsDidChange, object: nil, userInfo: nil)
}
func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?) -> ExtensionPoint? {
func extensionPoint(for extensionPointType: ExtensionPoint.Type, tokenSuccess: OAuthSwift.TokenSuccess?, completion: @escaping (Result<ExtensionPoint, Error>) -> Void) {
switch extensionPointType {
#if os(macOS)
case is SendToMarsEditCommand.Type:
return SendToMarsEditCommand()
completion(.success(SendToMarsEditCommand()))
case is SendToMicroBlogCommand.Type:
return SendToMicroBlogCommand()
completion(.success(SendToMicroBlogCommand()))
#endif
case is TwitterFeedProvider.Type:
if let tokenSuccess = tokenSuccess {
return TwitterFeedProvider(tokenSuccess: tokenSuccess)
if let tokenSuccess = tokenSuccess, let twitter = TwitterFeedProvider(tokenSuccess: tokenSuccess) {
completion(.success(twitter))
} else {
return nil
completion(.failure(ExtensionPointManagerError.unableToCreate))
}
case is RedditFeedProvider.Type:
if let tokenSuccess = tokenSuccess {
return RedditFeedProvider(tokenSuccess: tokenSuccess)
RedditFeedProvider.create(tokenSuccess: tokenSuccess) { result in
switch result {
case .success(let reddit):
completion(.success(reddit))
case .failure(let error):
completion(.failure(error))
}
}
} else {
return nil
completion(.failure(ExtensionPointManagerError.unableToCreate))
}
default:
assertionFailure("Unrecognized Extension Point Type.")
}
return nil
}
func extensionPoint(for extensionPointID: ExtensionPointIdentifer) -> ExtensionPoint? {

View File

@ -20,10 +20,16 @@ extension RedditFeedProvider: ExtensionPoint {
}()
var extensionPointID: ExtensionPointIdentifer {
guard let username = username else {
fatalError()
}
return ExtensionPointIdentifer.reddit(username)
}
var title: String {
guard let username = username else {
fatalError()
}
return "/u/\(username)"
}