diff --git a/Frameworks/Account/Account.xcodeproj/project.pbxproj b/Frameworks/Account/Account.xcodeproj/project.pbxproj index 8a04e97df..808d28db7 100644 --- a/Frameworks/Account/Account.xcodeproj/project.pbxproj +++ b/Frameworks/Account/Account.xcodeproj/project.pbxproj @@ -44,6 +44,7 @@ 513323082281070D00C30F19 /* AccountFeedbinSyncTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */; }; 5133230A2281082F00C30F19 /* subscriptions_initial.json in Resources */ = {isa = PBXBuildFile; fileRef = 513323092281082F00C30F19 /* subscriptions_initial.json */; }; 5133230C2281088A00C30F19 /* subscriptions_add.json in Resources */ = {isa = PBXBuildFile; fileRef = 5133230B2281088A00C30F19 /* subscriptions_add.json */; }; + 5133BB47245FD8140001E3D0 /* RedditListing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5133BB46245FD8140001E3D0 /* RedditListing.swift */; }; 5139A6382459822D004D960C /* CloudKitArticleStatusUpdate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */; }; 5144EA49227B497600D19003 /* FeedbinAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */; }; 5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */; }; @@ -300,6 +301,7 @@ 513323072281070C00C30F19 /* AccountFeedbinSyncTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountFeedbinSyncTest.swift; sourceTree = ""; }; 513323092281082F00C30F19 /* subscriptions_initial.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_initial.json; sourceTree = ""; }; 5133230B2281088A00C30F19 /* subscriptions_add.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = subscriptions_add.json; sourceTree = ""; }; + 5133BB46245FD8140001E3D0 /* RedditListing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RedditListing.swift; sourceTree = ""; }; 5139A6372459822D004D960C /* CloudKitArticleStatusUpdate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKitArticleStatusUpdate.swift; sourceTree = ""; }; 5144EA48227B497600D19003 /* FeedbinAPICaller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAPICaller.swift; sourceTree = ""; }; 5144EA4D227B829A00D19003 /* FeedbinAccountDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountDelegate.swift; sourceTree = ""; }; @@ -633,6 +635,7 @@ isa = PBXGroup; children = ( 5193CD53245E3F7A0092735E /* RedditFeedProvider.swift */, + 5133BB46245FD8140001E3D0 /* RedditListing.swift */, 5193CD80245F295E0092735E /* RedditUser.swift */, ); path = Reddit; @@ -1204,6 +1207,7 @@ 51F6C593245DBA8E001E41CA /* CloudKitRemoteNotificationOperation.swift in Sources */, 846E77541F6F00E300A165E2 /* AccountManager.swift in Sources */, 51E490362288C37100C791F0 /* FeedbinDate.swift in Sources */, + 5133BB47245FD8140001E3D0 /* RedditListing.swift in Sources */, 9EEAE06E235D002D00E3FEE4 /* FeedlyGetCollectionsService.swift in Sources */, 5165D72922835F7A00D9D53D /* FeedSpecifier.swift in Sources */, 9E85C8ED2367020700D0F1F7 /* FeedlyGetEntriesService.swift in Sources */, diff --git a/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift index 77225d220..da8a83929 100644 --- a/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift +++ b/Frameworks/Account/FeedProvider/Reddit/RedditFeedProvider.swift @@ -7,6 +7,7 @@ // import Foundation +import os.log import OAuthSwift import Secrets import RSParser @@ -18,12 +19,14 @@ public enum RedditFeedProviderError: LocalizedError { public var localizedDescription: String { switch self { case .unknown: - return NSLocalizedString("An Reddit Twitter Feed Provider error has occurred.", comment: "Unknown error") + return NSLocalizedString("A Reddit Feed Provider error has occurred.", comment: "Unknown error") } } } -public struct RedditFeedProvider: FeedProvider { +public final class RedditFeedProvider: FeedProvider { + + var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "RedditFeedProvider") private static let server = "www.reddit.com" private static let apiBase = "https://oauth.reddit.com" @@ -42,7 +45,7 @@ public struct RedditFeedProvider: FeedProvider { return oauthSwift?.client } - public init?(username: String) { + public convenience init?(username: String) { guard let tokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthAccessToken, server: Self.server, username: username), let refreshTokenCredentials = try? CredentialsManager.retrieveCredentials(type: .oauthRefreshToken, server: Self.server, username: username) else { return nil @@ -77,25 +80,20 @@ public struct RedditFeedProvider: FeedProvider { } public func iconURL(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { - completion(.failure(TwitterFeedProviderError.screenNameNotFound)) + completion(.failure(RedditFeedProviderError.unknown)) } public func assignName(_ urlComponents: URLComponents, completion: @escaping (Result) -> Void) { let path = urlComponents.path - - switch path { - case "", "/": + + if path == "" || path == "/" { 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)) + return } + + // TODO: call to get the Subreddit name + completion(.success(path)) } public func refresh(_ webFeed: WebFeed, completion: @escaping (Result, Error>) -> Void) { @@ -103,26 +101,25 @@ public struct RedditFeedProvider: FeedProvider { // completion(.failure(TwitterFeedProviderError.unknown)) // return // } + let api = "/r/sphynx/hot.json" + retrieveListing(api: api, parameters: [:]) { result in + completion(.success(Set())) + } - 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) + let 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 + try storeCredentials(username: username, oauthToken: oauthToken, oauthRefreshToken: oauthRefreshToken) completion(.success(redditFeedProvider)) } catch { completion(.failure(error)) @@ -157,7 +154,7 @@ extension RedditFeedProvider: OAuth2SwiftProvider { public static var oauth2Vars: (state: String, scope: String, params: [String : String]) { let state = generateState(withLength: 20) - let scope = "identity mysubreddits" + let scope = "identity mysubreddits read" let params = [ "client_id" : Secrets.redditConsumerKey, "response_type" : "code", @@ -192,5 +189,125 @@ private extension RedditFeedProvider { } } } + + func retrieveListing(api: String, parameters: [String: Any], completion: @escaping (Result) -> Void) { + guard let client = client else { + completion(.failure(RedditFeedProviderError.unknown)) + return + } + let url = "\(Self.apiBase)\(api)" + + client.get(url, parameters: parameters, headers: Self.userAgentHeaders) { result in + switch result { + case .success(let response): + + let jsonString = String(data: response.data, encoding: .utf8) + let url = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first!.appendingPathComponent("reddit.json") + print("******** writing to: \(url.path)") + try? jsonString?.write(toFile: url.path, atomically: true, encoding: .utf8) + +// let decoder = JSONDecoder() +// let dateFormatter = DateFormatter() +// dateFormatter.dateFormat = Self.dateFormat +// decoder.dateDecodingStrategy = .formatted(dateFormatter) + +// do { +// let listing = try decoder.decode(RedditListing.self, from: response.data) +// completion(.success(listing)) +// } catch { +// completion(.failure(error)) +// } + + let listing = RedditListing(name: "") + completion(.success(listing)) + + case .failure(let oathError): + self.handleFailure(error: oathError) { error in + if let error = error { + completion(.failure(error)) + } else { + self.retrieveListing(api: api, parameters: parameters, completion: completion) + } + } + } + } + } + +// func makeParsedItems(_ webFeedURL: String, _ statuses: [TwitterStatus]) -> Set { +// var parsedItems = Set() +// +// for status in statuses { +// guard let idStr = status.idStr, let statusURL = status.url else { continue } +// +// let parsedItem = ParsedItem(syncServiceID: nil, +// uniqueID: idStr, +// feedURL: webFeedURL, +// url: statusURL, +// externalURL: nil, +// title: nil, +// language: nil, +// contentHTML: status.renderAsHTML(), +// contentText: status.renderAsText(), +// summary: nil, +// imageURL: nil, +// bannerImageURL: nil, +// datePublished: status.createdAt, +// dateModified: nil, +// authors: makeParsedAuthors(status.user), +// tags: nil, +// attachments: nil) +// parsedItems.insert(parsedItem) +// } +// +// return parsedItems +// } +// +// func makeUserURL(_ screenName: String) -> String { +// return "https://twitter.com/\(screenName)" +// } +// +// func makeParsedAuthors(_ user: TwitterUser?) -> Set? { +// guard let user = user else { return nil } +// return Set([ParsedAuthor(name: user.name, url: user.url, avatarURL: user.avatarURL, emailAddress: nil)]) +// } + + func handleFailure(error: OAuthSwiftError, completion: @escaping (Error?) -> Void) { + if case .tokenExpired = error { + os_log(.debug, log: self.log, "Access token expired, attempting to renew...") + + oauthSwift?.renewAccessToken(withRefreshToken: oauthRefreshToken) { [weak self] result in + guard let self = self, let username = self.username else { + completion(nil) + return + } + + switch result { + case .success(let tokenSuccess): + self.oauthToken = tokenSuccess.credential.oauthToken + self.oauthRefreshToken = tokenSuccess.credential.oauthRefreshToken + do { + try Self.storeCredentials(username: username, oauthToken: self.oauthToken, oauthRefreshToken: self.oauthRefreshToken) + os_log(.debug, log: self.log, "Access token renewed.") + } catch { + completion(error) + return + } + completion(nil) + case .failure(let oathError): + completion(oathError) + } + } + + } 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/Frameworks/Account/FeedProvider/Reddit/RedditListing.swift b/Frameworks/Account/FeedProvider/Reddit/RedditListing.swift new file mode 100644 index 000000000..e7861d754 --- /dev/null +++ b/Frameworks/Account/FeedProvider/Reddit/RedditListing.swift @@ -0,0 +1,19 @@ +// +// RedditListing.swift +// Account +// +// Created by Maurice Parker on 5/3/20. +// Copyright © 2020 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct RedditListing: Codable { + + let name: String? + + enum CodingKeys: String, CodingKey { + case name = "name" + } + +}