From ccd600b880c131aaf932099346de150860becca9 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sun, 3 May 2020 13:23:36 -0500 Subject: [PATCH] Make authorize endpoint async --- .../Account/Account.xcodeproj/project.pbxproj | 4 + .../Reddit/RedditFeedProvider.swift | 111 ++++++++++++------ .../FeedProvider/Reddit/RedditUser.swift | 40 +++++++ Frameworks/Secrets/Secrets.swift.gyb | 2 +- ...ExtensionPointEnableWindowController.swift | 16 ++- .../ExtensionPointManager.swift | 50 ++++++-- .../RedditFeedProvider-Extensions.swift | 6 + 7 files changed, 175 insertions(+), 54 deletions(-) create mode 100644 Frameworks/Account/FeedProvider/Reddit/RedditUser.swift diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index b51ae8086..8a04e97df 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -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 = ""; }; 5170743B232AEDB500A461A3 /* OPMLFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLFile.swift; sourceTree = ""; }; 5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditFeedProvider.swift; sourceTree = ""; }; + 5193CD80245F295E0092735E /* RedditUser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditUser.swift; sourceTree = ""; }; 519E84A52433D49000D238B0 /* OPMLNormalizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLNormalizer.swift; sourceTree = ""; }; 519E84A72434C5EF00D238B0 /* CloudKitArticlesZone.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZone.swift; sourceTree = ""; }; 519E84AB2435019100D238B0 /* CloudKitArticlesZoneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CloudKitArticlesZoneDelegate.swift; sourceTree = ""; }; @@ -631,6 +633,7 @@ isa = PBXGroup; children = ( 5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */, + 5193CD80245F295E0092735E /* RedditUser.swift */, ); path = Reddit; sourceTree = ""; @@ -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 */, diff --git a/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift index 5fae04115..0c3da6dd0 100644 --- a/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift +++ b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift @@ -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 - - client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey, - consumerSecret: Secrets.twitterConsumerSecret, - oauthToken: oauthToken, - oauthTokenSecret: oauthTokenSecret, - version: .oauth1) + self.init(oauthToken: tokenCredentials.secret, oauthRefreshToken: refreshTokenCredentials.secret) + self.username = username + } + + 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())) } + public static func create(tokenSuccess: OAuthSwift.TokenSuccess, completion: @escaping (Result) -> 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) -> 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)) + } + } + } + +} diff --git a/Frameworks/Account/FeedProvider/Reddit/RedditUser.swift b/Frameworks/Account/FeedProvider/Reddit/RedditUser.swift new file mode 100644 index 000000000..ade2acdc7 --- /dev/null +++ b/Frameworks/Account/FeedProvider/Reddit/RedditUser.swift @@ -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 += "" +// return html +// } + +} diff --git a/Frameworks/Secrets/Secrets.swift.gyb b/Frameworks/Secrets/Secrets.swift.gyb index 1f92b37c7..3907729aa 100644 --- a/Frameworks/Secrets/Secrets.swift.gyb +++ b/Frameworks/Secrets/Secrets.swift.gyb @@ -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)) diff --git a/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift b/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift index fc400d531..feccecc73 100644 --- a/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift +++ b/Mac/Preferences/ExtensionPoints/ExtensionPointEnableWindowController.swift @@ -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) } diff --git a/Shared/ExtensionPoints/ExtensionPointManager.swift b/Shared/ExtensionPoints/ExtensionPointManager.swift index 4d51b8a4b..c2ab048bd 100644 --- a/Shared/ExtensionPoints/ExtensionPointManager.swift +++ b/Shared/ExtensionPoints/ExtensionPointManager.swift @@ -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) { + 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) -> 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? { diff --git a/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift b/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift index a2f3839b6..d9bd6187c 100644 --- a/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift +++ b/Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift @@ -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)" }