Add login/logout support

This commit is contained in:
Anh Do
2020-03-09 20:19:24 -04:00
parent 8f5f856e49
commit 034aabbfff
9 changed files with 298 additions and 14 deletions

View File

@@ -243,6 +243,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
case .feedWrangler:
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
case .newsBlur:
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
default:
return nil
}
@@ -325,6 +327,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion)
case .feedWrangler:
FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
case .newsBlur:
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
default:
break
}

View File

@@ -7,6 +7,7 @@
objects = {
/* Begin PBXBuildFile section */
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */; };
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A33E6238D3D6800314204 /* Secrets.swift */; };
3B826DA72385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */; };
3B826DA82385C81C00FC1ADB /* FeedWranglerFeedItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */; };
@@ -63,6 +64,8 @@
552032FD229D5D5A009559E0 /* ReaderAPITagging.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */; };
552032FE229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */; };
55203300229D5D5A009559E0 /* ReaderAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */; };
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */; };
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */; };
841973FE1F6DD1BC006346C4 /* RSCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973EF1F6DD19E006346C4 /* RSCore.framework */; };
841973FF1F6DD1C5006346C4 /* RSParser.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 841973FA1F6DD1AC006346C4 /* RSParser.framework */; };
841974011F6DD1EC006346C4 /* Folder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 841974001F6DD1EC006346C4 /* Folder.swift */; };
@@ -220,6 +223,7 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurLoginResponse.swift; sourceTree = "<group>"; };
3B3A33E6238D3D6800314204 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Secrets.swift; path = ../../Shared/Secrets.swift; sourceTree = "<group>"; };
3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAuthorizationResult.swift; sourceTree = "<group>"; };
3B826D9F2385C81C00FC1ADB /* FeedWranglerFeedItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerFeedItem.swift; sourceTree = "<group>"; };
@@ -278,6 +282,8 @@
552032F2229D5D5A009559E0 /* ReaderAPITagging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPITagging.swift; sourceTree = "<group>"; };
552032F3229D5D5A009559E0 /* ReaderAPIAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountDelegate.swift; sourceTree = "<group>"; };
552032F5229D5D5A009559E0 /* ReaderAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ReaderAPICaller.swift; sourceTree = "<group>"; };
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAPICaller.swift; sourceTree = "<group>"; };
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurAccountDelegate.swift; sourceTree = "<group>"; };
841973E81F6DD19E006346C4 /* RSCore.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSCore.xcodeproj; path = ../RSCore/RSCore.xcodeproj; sourceTree = "<group>"; };
841973F41F6DD1AC006346C4 /* RSParser.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RSParser.xcodeproj; path = ../RSParser/RSParser.xcodeproj; sourceTree = "<group>"; };
841974001F6DD1EC006346C4 /* Folder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Folder.swift; sourceTree = "<group>"; };
@@ -435,6 +441,14 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
179DBD810D353D9CED7C3BED /* Models */ = {
isa = PBXGroup;
children = (
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */,
);
path = Models;
sourceTree = "<group>";
};
3B826D9D2385C81C00FC1ADB /* FeedWrangler */ = {
isa = PBXGroup;
children = (
@@ -523,6 +537,16 @@
path = ReaderAPI;
sourceTree = "<group>";
};
769F2630AF8DC873D4A73567 /* NewsBlur */ = {
isa = PBXGroup;
children = (
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */,
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */,
179DBD810D353D9CED7C3BED /* Models */,
);
path = NewsBlur;
sourceTree = "<group>";
};
841973E91F6DD19E006346C4 /* Products */ = {
isa = PBXGroup;
children = (
@@ -622,6 +646,7 @@
8469F80F1F6DC3C10084783E /* Frameworks */,
D511EEB4202422BB00712EC3 /* xcconfig */,
848934FA1F62484F00CEBD24 /* Info.plist */,
769F2630AF8DC873D4A73567 /* NewsBlur */,
);
sourceTree = "<group>";
usesTabs = 1;
@@ -1107,6 +1132,9 @@
9EF2602C23C91FFE006D160C /* FeedlyGetUpdatedArticleIdsOperation.swift in Sources */,
3B826DAA2385C81C00FC1ADB /* FeedWranglerSubscription.swift in Sources */,
3B826DAC2385C81C00FC1ADB /* FeedWranglerAccountDelegate.swift in Sources */,
769F295938E5A30D03DFF88F /* NewsBlurAccountDelegate.swift in Sources */,
769F2BA02EF5F329CDE45F5A /* NewsBlurAPICaller.swift in Sources */,
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};

View File

@@ -17,6 +17,7 @@ public enum CredentialsType: String {
case basic = "password"
case feedWranglerBasic = "feedWranglerBasic"
case feedWranglerToken = "feedWranglerToken"
case newsBlur = "newsBlur"
case readerBasic = "readerBasic"
case readerAPIKey = "readerAPIKey"
case oauthAccessToken = "oauthAccessToken"

View File

@@ -33,7 +33,12 @@ public extension URLRequest {
])
case .feedWranglerToken:
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
case .readerBasic:
case .newsBlur:
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
httpMethod = "POST"
let postData = "username=\(credentials.username)&password=\(credentials.secret)"
httpBody = postData.data(using: String.Encoding.utf8)
case .readerBasic:
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
httpMethod = "POST"
var postData = URLComponents()

View File

@@ -0,0 +1,26 @@
//
// NewsBlurLoginResponse.swift
// Account
//
// Created by Anh Quang Do on 2020-03-09.
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
struct NewsBlurLoginResponse: Decodable {
var code: Int
var errors: LoginError?
struct LoginError: Decodable {
var username: [String]?
var others: [String]?
}
}
extension NewsBlurLoginResponse.LoginError {
private enum CodingKeys: String, CodingKey {
case username = "username"
case others = "__all__"
}
}

View File

@@ -0,0 +1,72 @@
//
// NewsBlurAPICaller.swift
// Account
//
// Created by Anh-Quang Do on 3/9/20.
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSWeb
enum NewsBlurError: LocalizedError {
case general(message: String)
var errorDescription: String? {
switch self {
case .general(let message):
return message
}
}
}
final class NewsBlurAPICaller: NSObject {
private let baseURL = URL(string: "https://www.newsblur.com/")!
private var transport: Transport!
var credentials: Credentials?
weak var accountMetadata: AccountMetadata?
init(transport: Transport!) {
super.init()
self.transport = transport
}
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
let url = baseURL.appendingPathComponent("api/login")
let request = URLRequest(url: url, credentials: credentials)
transport.send(request: request, resultType: NewsBlurLoginResponse.self) { result in
switch result {
case .success(_, let payload):
guard payload?.code != -1 else {
let error = payload?.errors?.username ?? payload?.errors?.others
if let message = error?.first {
completion(.failure(NewsBlurError.general(message: message)))
} else {
completion(.failure(NewsBlurError.general(message: "Failed to log in")))
}
return
}
completion(.success(self.credentials))
case .failure(let error):
completion(.failure(error))
}
}
}
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
let url = baseURL.appendingPathComponent("api/logout")
let request = URLRequest(url: url, credentials: credentials)
transport.send(request: request) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
completion(.failure(error))
}
}
}
}

View File

@@ -0,0 +1,146 @@
//
// NewsBlurAccountDelegate.swift
// Account
//
// Created by Anh-Quang Do on 3/9/20.
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
//
import Articles
import RSCore
import RSDatabase
import RSParser
import RSWeb
import SyncDatabase
import os.log
final class NewsBlurAccountDelegate: AccountDelegate {
var behaviors: AccountBehaviors = []
var isOPMLImportInProgress: Bool = false
var server: String? = "newsblur.com"
var credentials: Credentials? {
didSet {
caller.credentials = credentials
}
}
var accountMetadata: AccountMetadata? = nil
var refreshProgress = DownloadProgress(numberOfTasks: 0)
private let caller: NewsBlurAPICaller
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "NewsBlur")
private let database: SyncDatabase
init(dataFolder: String, transport: Transport?) {
if let transport = transport {
caller = NewsBlurAPICaller(transport: transport)
} else {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
sessionConfiguration.timeoutIntervalForRequest = 60.0
sessionConfiguration.httpShouldSetCookies = false
sessionConfiguration.httpCookieAcceptPolicy = .never
sessionConfiguration.httpMaximumConnectionsPerHost = 1
sessionConfiguration.httpCookieStorage = nil
sessionConfiguration.urlCache = nil
if let userAgentHeaders = UserAgent.headers() {
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
}
let session = URLSession(configuration: sessionConfiguration)
caller = NewsBlurAPICaller(transport: session)
}
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func sendArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func refreshArticleStatus(for account: Account, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> ()) {
}
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> ()) {
}
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> ()) {
completion(.success(()))
}
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
fatalError("markArticles(for:articles:statusKey:flag:) has not been implemented")
}
func accountDidInitialize(_ account: Account) {
credentials = try? account.retrieveCredentials(type: .newsBlur)
}
func accountWillBeDeleted(_ account: Account) {
caller.logout() { _ in }
}
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> ()) {
let caller = NewsBlurAPICaller(transport: transport)
caller.credentials = credentials
caller.validateCredentials() { result in
DispatchQueue.main.async {
completion(result)
}
}
}
func suspendNetwork() {
}
func suspendDatabase() {
database.suspend()
}
func resume() {
database.resume()
}
}