From e681fd640276ba0eb426ebaaadf829e7434be1de Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Sat, 2 May 2020 19:38:57 -0500 Subject: [PATCH] Stub out Reddit Feed Provider / Extension Point --- .../Account/Account.xcodeproj/project.pbxproj | 12 ++ .../Reddit/RedditFeedProvider.swift | 138 ++++++++++++++++++ Frameworks/Secrets/OAuth2SwiftProvider.swift | 17 +++ .../Secrets/Secrets.xcodeproj/project.pbxproj | 4 + Mac/AppAssets.swift | 4 + .../Contents.json | 15 ++ .../reddit-logo.pdf | Bin 0 -> 4583 bytes NetNewsWire.xcodeproj/project.pbxproj | 8 + .../ExtensionPointIdentifer.swift | 16 +- .../ExtensionPointManager.swift | 20 ++- .../RedditFeedProvider-Extensions.swift | 30 ++++ iOS/AppAssets.swift | 4 + .../Contents.json | 15 ++ .../reddit-logo.pdf | Bin 0 -> 4583 bytes 14 files changed, 278 insertions(+), 5 deletions(-) create mode 100644 Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift create mode 100644 Frameworks/Secrets/OAuth2SwiftProvider.swift create mode 100644 Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/Contents.json create mode 100644 Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/reddit-logo.pdf create mode 100644 Shared/ExtensionPoints/RedditFeedProvider-Extensions.swift create mode 100644 iOS/Resources/Assets.xcassets/extensionPointReddit.imageset/Contents.json create mode 100644 iOS/Resources/Assets.xcassets/extensionPointReddit.imageset/reddit-logo.pdf diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 3c1de4b7c..b51ae8086 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -61,6 +61,7 @@ 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */; }; 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 */; }; 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 */; }; @@ -315,6 +316,7 @@ 5165D73022837F3400D9D53D /* InitialFeedDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = InitialFeedDownloader.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -590,6 +592,7 @@ 5132AAC12448BAD90077840A /* FeedProvider.swift */, 516896342448EBEA00185AC5 /* FeedProviderManager.swift */, 5132AAC22448BAD90077840A /* Twitter */, + 5193CD52245E3F520092735E /* Reddit */, ); path = FeedProvider; sourceTree = ""; @@ -624,6 +627,14 @@ path = FeedFinder; sourceTree = ""; }; + 5193CD52245E3F520092735E /* Reddit */ = { + isa = PBXGroup; + children = ( + 5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */, + ); + path = Reddit; + sourceTree = ""; + }; 51D58756227F62E300900287 /* JSON */ = { isa = PBXGroup; children = ( @@ -1180,6 +1191,7 @@ 841974251F6DDCE4006346C4 /* AccountDelegate.swift in Sources */, 510BD113232C3E9D002692E4 /* WebFeedMetadataFile.swift in Sources */, 51B36309244B62A5000DEF2A /* TwitterURL.swift in Sources */, + 5193CD54245E3F7A0092735E /* RedditFeedProvider.swift in Sources */, 5103A9D92422546800410853 /* CloudKitAccountDelegate.swift in Sources */, 5165D73122837F3400D9D53D /* InitialFeedDownloader.swift in Sources */, 9E784EBE237E890600099B1B /* FeedlyLogoutOperation.swift in Sources */, diff --git a/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift new file mode 100644 index 000000000..5fae04115 --- /dev/null +++ b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift @@ -0,0 +1,138 @@ +// +// RedditFeedProvider.swift +// Account +// +// Created by Maurice Parker on 5/2/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import OAuthSwift +import Secrets +import RSParser + +public enum RedditFeedProviderError: LocalizedError { + case unknown + + public var localizedDescription: String { + switch self { + case .unknown: + return NSLocalizedString("An Reddit Twitter Feed Provider error has occurred.", comment: "Unknown error") + } + } +} + +public struct RedditFeedProvider: FeedProvider { + + private static let server = "api.twitter.com" + private static let apiBase = "https://api.twitter.com/1.1/" + private static let dateFormat = "EEE MMM dd HH:mm:ss Z yyyy" + + private static let userPaths = ["/home", "/notifications"] + private static let reservedPaths = ["/search", "/explore", "/messages", "/i", "/compose"] + + public var username: String + + private var oauthToken: String + private var oauthTokenSecret: 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) + } + + 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 { + return nil + } + + self.oauthToken = tokenCredentials.secret + self.oauthTokenSecret = tokenSecretCredentials.secret + + client = OAuthSwiftClient(consumerKey: Secrets.twitterConsumerKey, + consumerSecret: Secrets.twitterConsumerSecret, + oauthToken: oauthToken, + oauthTokenSecret: oauthTokenSecret, + version: .oauth1) + } + + public func ability(_ urlComponents: URLComponents) -> FeedProviderAbility { + guard urlComponents.host?.hasSuffix("reddit.com") ?? false else { + return .none + } + + if let username = urlComponents.user { + if username == username { + return .owner + } else { + return .none + } + } + + return .available + } + + public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { + completion(.failure(TwitterFeedProviderError.screenNameNotFound)) + } + + public func assignName(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { + let path = urlComponents.path + + switch path { + case "", "/": + let name = NSLocalizedString("Reddit Timeline", comment: "Reddit Timeline") + completion(.success(name)) + case "/r", "/u": + let path = String(path.suffix(from: path.index(path.startIndex, offsetBy: 2))) + completion(.success(path)) + case "/user": + let path = String(path.suffix(from: path.index(path.startIndex, offsetBy: 5))) + completion(.success(path)) + default: + completion(.failure(TwitterFeedProviderError.unknown)) + } + } + + public func refresh(_ webFeed: WebFeed, completion: @escaping (Result, Error>) -> Void) { +// guard let urlComponents = URLComponents(string: webFeed.url) else { +// completion(.failure(TwitterFeedProviderError.unknown)) +// return +// } + + completion(.success(Set())) + } + +} + +// MARK: OAuth1SwiftProvider + +extension RedditFeedProvider: OAuth2SwiftProvider { + + public static var oauth2Swift: OAuth2Swift { + return OAuth2Swift(consumerKey: "", consumerSecret: "", authorizeUrl: "", accessTokenUrl: "", responseType: "") + } + +} diff --git a/Frameworks/Secrets/OAuth2SwiftProvider.swift b/Frameworks/Secrets/OAuth2SwiftProvider.swift new file mode 100644 index 000000000..5509dfeea --- /dev/null +++ b/Frameworks/Secrets/OAuth2SwiftProvider.swift @@ -0,0 +1,17 @@ +// +// OAuth2SwiftProvider.swift +// Secrets +// +// Created by Maurice Parker on 5/2/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +import OAuthSwift + +public protocol OAuth2SwiftProvider { + + static var oauth2Swift: OAuth2Swift { get } + +} diff --git a/Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj b/Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj index 489bf569f..5cc15d12b 100644 --- a/Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj +++ b/Frameworks/Secrets/Secrets.xcodeproj/project.pbxproj @@ -15,6 +15,7 @@ 514BB43B243FFBFF0023B621 /* CredentialsManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BB439243FFBFF0023B621 /* CredentialsManager.swift */; }; 514BB43C243FFBFF0023B621 /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514BB43A243FFBFF0023B621 /* Credentials.swift */; }; 5152BEF2244633FA00138380 /* OAuth1SwiftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */; }; + 5193CD56245E40B70092735E /* OAuth2SwiftProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD55245E40B70092735E /* OAuth2SwiftProvider.swift */; }; 51C99ABD2447DD730027D5F6 /* OAuthSwift.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C99ABC2447DD730027D5F6 /* OAuthSwift.framework */; }; /* End PBXBuildFile section */ @@ -29,6 +30,7 @@ 514BB439243FFBFF0023B621 /* CredentialsManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CredentialsManager.swift; sourceTree = ""; }; 514BB43A243FFBFF0023B621 /* Credentials.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Credentials.swift; sourceTree = ""; }; 5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth1SwiftProvider.swift; sourceTree = ""; }; + 5193CD55245E40B70092735E /* OAuth2SwiftProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OAuth2SwiftProvider.swift; sourceTree = ""; }; 51C99ABC2447DD730027D5F6 /* OAuthSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = OAuthSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -50,6 +52,7 @@ 514BB43A243FFBFF0023B621 /* Credentials.swift */, 514BB439243FFBFF0023B621 /* CredentialsManager.swift */, 5152BEF1244633FA00138380 /* OAuth1SwiftProvider.swift */, + 5193CD55245E40B70092735E /* OAuth2SwiftProvider.swift */, 514BB41E243FFA640023B621 /* Info.plist */, 514BB41B243FFA640023B621 /* Products */, 514446EC2440030900EE752D /* Secrets.swift */, @@ -192,6 +195,7 @@ 514BB43C243FFBFF0023B621 /* Credentials.swift in Sources */, 514446ED2440030900EE752D /* Secrets.swift in Sources */, 5152BEF2244633FA00138380 /* OAuth1SwiftProvider.swift in Sources */, + 5193CD56245E40B70092735E /* OAuth2SwiftProvider.swift in Sources */, 514BB43B243FFBFF0023B621 /* CredentialsManager.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index b7dca8f1f..5e4699fa1 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -85,6 +85,10 @@ struct AppAssets { return RSImage(named: "extensionPointMicroblog")! }() + static var extensionPointReddit: RSImage = { + return RSImage(named: "extensionPointReddit")! + }() + static var extensionPointTwitter: RSImage = { return RSImage(named: "extensionPointTwitter")! }() diff --git a/Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/Contents.json b/Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/Contents.json new file mode 100644 index 000000000..08e567354 --- /dev/null +++ b/Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/Contents.json @@ -0,0 +1,15 @@ +{ + "images" : [ + { + "filename" : "reddit-logo.pdf", + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + }, + "properties" : { + "template-rendering-intent" : "template" + } +} diff --git a/Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/reddit-logo.pdf b/Mac/Resources/Assets.xcassets/extensionPointReddit.imageset/reddit-logo.pdf new file mode 100644 index 0000000000000000000000000000000000000000..b4b6d741976ccf2d6454147ef4b82a762dc5810d GIT binary patch literal 4583 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