Refactoring, fix folder changes not working, add new web feed
This commit is contained in:
parent
ec855364bc
commit
70302a425c
|
@ -10,9 +10,11 @@
|
||||||
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB7399814F6FB3247825C /* NewsBlurStory.swift */; };
|
179DB02FFBC17AC9798F0EBC /* NewsBlurStory.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB7399814F6FB3247825C /* NewsBlurStory.swift */; };
|
||||||
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */; };
|
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */; };
|
||||||
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */; };
|
179DB28CF49F73A945EBF5DB /* NewsBlurLoginResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */; };
|
||||||
|
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */; };
|
||||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
|
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */; };
|
||||||
179DB96B984E67DC101E470D /* NewsBlurAccountDelegate+Private.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB55DC2CAD332D4376416 /* NewsBlurAccountDelegate+Private.swift */; };
|
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */; };
|
||||||
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */; };
|
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */; };
|
||||||
|
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */; };
|
||||||
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */; };
|
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */; };
|
||||||
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */; };
|
179DBF4DE2562D4C532F6008 /* NewsBlurFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */; };
|
||||||
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A33E6238D3D6800314204 /* Secrets.swift */; };
|
3B3A33E7238D3D6800314204 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A33E6238D3D6800314204 /* Secrets.swift */; };
|
||||||
|
@ -232,11 +234,13 @@
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurLoginResponse.swift; sourceTree = "<group>"; };
|
179DB088236E3236010462E8 /* NewsBlurLoginResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurLoginResponse.swift; sourceTree = "<group>"; };
|
||||||
179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeed.swift; sourceTree = "<group>"; };
|
179DB1B909672E0E807B5E8C /* NewsBlurFeed.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeed.swift; sourceTree = "<group>"; };
|
||||||
179DB55DC2CAD332D4376416 /* NewsBlurAccountDelegate+Private.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAccountDelegate+Private.swift"; sourceTree = "<group>"; };
|
179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFeedChange.swift; sourceTree = "<group>"; };
|
||||||
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryStatusChange.swift; sourceTree = "<group>"; };
|
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryStatusChange.swift; sourceTree = "<group>"; };
|
||||||
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurGenericCodingKeys.swift; sourceTree = "<group>"; };
|
179DB66D933E976C29159DEE /* NewsBlurGenericCodingKeys.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurGenericCodingKeys.swift; sourceTree = "<group>"; };
|
||||||
179DB7399814F6FB3247825C /* NewsBlurStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStory.swift; sourceTree = "<group>"; };
|
179DB7399814F6FB3247825C /* NewsBlurStory.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStory.swift; sourceTree = "<group>"; };
|
||||||
|
179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAccountDelegate+Internal.swift"; sourceTree = "<group>"; };
|
||||||
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryHash.swift; sourceTree = "<group>"; };
|
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurStoryHash.swift; sourceTree = "<group>"; };
|
||||||
|
179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NewsBlurAPICaller+Internal.swift"; sourceTree = "<group>"; };
|
||||||
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFolderChange.swift; sourceTree = "<group>"; };
|
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NewsBlurFolderChange.swift; sourceTree = "<group>"; };
|
||||||
3B3A33E6238D3D6800314204 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Secrets.swift; path = ../../Shared/Secrets.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>"; };
|
3B826D9E2385C81C00FC1ADB /* FeedWranglerAuthorizationResult.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAuthorizationResult.swift; sourceTree = "<group>"; };
|
||||||
|
@ -455,6 +459,15 @@
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
|
179DB1571B95BAD0F833AF6D /* Internals */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
179DB78C47256A122A281942 /* NewsBlurAccountDelegate+Internal.swift */,
|
||||||
|
179DBBF346CF712AB2F0E9E6 /* NewsBlurAPICaller+Internal.swift */,
|
||||||
|
);
|
||||||
|
path = Internals;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
179DBD810D353D9CED7C3BED /* Models */ = {
|
179DBD810D353D9CED7C3BED /* Models */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -465,6 +478,7 @@
|
||||||
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */,
|
179DB818180A51098A9816B2 /* NewsBlurStoryHash.swift */,
|
||||||
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */,
|
179DB5B421C5433B45C5F13E /* NewsBlurStoryStatusChange.swift */,
|
||||||
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */,
|
179DBDDC00B68411AA28941F /* NewsBlurFolderChange.swift */,
|
||||||
|
179DB3CBADAFCF5377DA3D02 /* NewsBlurFeedChange.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -563,7 +577,7 @@
|
||||||
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */,
|
769F2A8DF190549E24B5D110 /* NewsBlurAccountDelegate.swift */,
|
||||||
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */,
|
769F275FD5D942502C5B4716 /* NewsBlurAPICaller.swift */,
|
||||||
179DBD810D353D9CED7C3BED /* Models */,
|
179DBD810D353D9CED7C3BED /* Models */,
|
||||||
179DB55DC2CAD332D4376416 /* NewsBlurAccountDelegate+Private.swift */,
|
179DB1571B95BAD0F833AF6D /* Internals */,
|
||||||
);
|
);
|
||||||
path = NewsBlur;
|
path = NewsBlur;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -1161,8 +1175,10 @@
|
||||||
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
|
179DB49A960F8B78C4924458 /* NewsBlurGenericCodingKeys.swift in Sources */,
|
||||||
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */,
|
179DBED55C9B4D6A413486C1 /* NewsBlurStoryHash.swift in Sources */,
|
||||||
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */,
|
179DB0B17A6C51B95ABC1741 /* NewsBlurStoryStatusChange.swift in Sources */,
|
||||||
179DB96B984E67DC101E470D /* NewsBlurAccountDelegate+Private.swift in Sources */,
|
|
||||||
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */,
|
179DBD4ECC1C9712DF51DB8C /* NewsBlurFolderChange.swift in Sources */,
|
||||||
|
179DBCB4B11C88EBE852A015 /* NewsBlurFeedChange.swift in Sources */,
|
||||||
|
179DBE829FDF48E102F73244 /* NewsBlurAccountDelegate+Internal.swift in Sources */,
|
||||||
|
179DB3A93E3205EF29C2AF62 /* NewsBlurAPICaller+Internal.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
|
|
@ -34,7 +34,7 @@ public extension URLRequest {
|
||||||
case .feedWranglerToken:
|
case .feedWranglerToken:
|
||||||
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
|
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
|
||||||
case .newsBlurBasic:
|
case .newsBlurBasic:
|
||||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||||
httpMethod = "POST"
|
httpMethod = "POST"
|
||||||
var postData = URLComponents()
|
var postData = URLComponents()
|
||||||
postData.queryItems = [
|
postData.queryItems = [
|
||||||
|
|
|
@ -0,0 +1,236 @@
|
||||||
|
//
|
||||||
|
// NewsBlurAPICaller+Internal.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Anh Quang Do on 2020-03-21.
|
||||||
|
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import RSWeb
|
||||||
|
|
||||||
|
protocol NewsBlurDataConvertible {
|
||||||
|
var asData: Data? { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
enum NewsBlurError: LocalizedError {
|
||||||
|
case general(message: String)
|
||||||
|
case invalidParameter
|
||||||
|
case unknown
|
||||||
|
|
||||||
|
var errorDescription: String? {
|
||||||
|
switch self {
|
||||||
|
case .general(let message):
|
||||||
|
return message
|
||||||
|
case .invalidParameter:
|
||||||
|
return "There was an invalid parameter passed"
|
||||||
|
case .unknown:
|
||||||
|
return "An unknown error occurred"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interact with endpoints
|
||||||
|
|
||||||
|
extension NewsBlurAPICaller {
|
||||||
|
// GET endpoint, discard response
|
||||||
|
func requestData(
|
||||||
|
endpoint: String,
|
||||||
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
|
) {
|
||||||
|
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||||
|
|
||||||
|
requestData(callURL: callURL, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET endpoint
|
||||||
|
func requestData<R: Decodable>(
|
||||||
|
endpoint: String,
|
||||||
|
resultType: R.Type,
|
||||||
|
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||||
|
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||||
|
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||||
|
) {
|
||||||
|
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||||
|
|
||||||
|
requestData(
|
||||||
|
callURL: callURL,
|
||||||
|
resultType: resultType,
|
||||||
|
dateDecoding: dateDecoding,
|
||||||
|
keyDecoding: keyDecoding,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to endpoint, discard response
|
||||||
|
func sendUpdates(
|
||||||
|
endpoint: String,
|
||||||
|
payload: NewsBlurDataConvertible,
|
||||||
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
|
) {
|
||||||
|
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||||
|
|
||||||
|
sendUpdates(callURL: callURL, payload: payload, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to endpoint
|
||||||
|
func sendUpdates<R: Decodable>(
|
||||||
|
endpoint: String,
|
||||||
|
payload: NewsBlurDataConvertible,
|
||||||
|
resultType: R.Type,
|
||||||
|
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||||
|
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||||
|
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||||
|
) {
|
||||||
|
let callURL = baseURL.appendingPathComponent(endpoint)
|
||||||
|
|
||||||
|
sendUpdates(
|
||||||
|
callURL: callURL,
|
||||||
|
payload: payload,
|
||||||
|
resultType: resultType,
|
||||||
|
dateDecoding: dateDecoding,
|
||||||
|
keyDecoding: keyDecoding,
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Interact with URLs
|
||||||
|
|
||||||
|
extension NewsBlurAPICaller {
|
||||||
|
// GET URL with params, discard response
|
||||||
|
func requestData(
|
||||||
|
callURL: URL?,
|
||||||
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
|
) {
|
||||||
|
guard let callURL = callURL else {
|
||||||
|
completion(.failure(TransportError.noURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = URLRequest(url: callURL, credentials: credentials)
|
||||||
|
|
||||||
|
transport.send(request: request) { result in
|
||||||
|
if self.suspended {
|
||||||
|
completion(.failure(TransportError.suspended))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET URL with params
|
||||||
|
func requestData<R: Decodable>(
|
||||||
|
callURL: URL?,
|
||||||
|
resultType: R.Type,
|
||||||
|
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||||
|
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||||
|
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||||
|
) {
|
||||||
|
guard let callURL = callURL else {
|
||||||
|
completion(.failure(TransportError.noURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let request = URLRequest(url: callURL, credentials: credentials)
|
||||||
|
|
||||||
|
transport.send(
|
||||||
|
request: request,
|
||||||
|
resultType: resultType,
|
||||||
|
dateDecoding: dateDecoding,
|
||||||
|
keyDecoding: keyDecoding
|
||||||
|
) { result in
|
||||||
|
if self.suspended {
|
||||||
|
completion(.failure(TransportError.suspended))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
completion(.success(response))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to URL with params, discard response
|
||||||
|
func sendUpdates(
|
||||||
|
callURL: URL?,
|
||||||
|
payload: NewsBlurDataConvertible,
|
||||||
|
completion: @escaping (Result<Void, Error>) -> Void
|
||||||
|
) {
|
||||||
|
guard let callURL = callURL else {
|
||||||
|
completion(.failure(TransportError.noURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: callURL, credentials: credentials)
|
||||||
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||||
|
request.httpBody = payload.asData
|
||||||
|
|
||||||
|
transport.send(request: request, method: HTTPMethod.post) { result in
|
||||||
|
if self.suspended {
|
||||||
|
completion(.failure(TransportError.suspended))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
completion(.success(()))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST to URL with params
|
||||||
|
func sendUpdates<R: Decodable>(
|
||||||
|
callURL: URL?,
|
||||||
|
payload: NewsBlurDataConvertible,
|
||||||
|
resultType: R.Type,
|
||||||
|
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||||
|
keyDecoding: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys,
|
||||||
|
completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void
|
||||||
|
) {
|
||||||
|
guard let callURL = callURL else {
|
||||||
|
completion(.failure(TransportError.noURL))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
guard let data = payload.asData else {
|
||||||
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var request = URLRequest(url: callURL, credentials: credentials)
|
||||||
|
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||||
|
|
||||||
|
transport.send(
|
||||||
|
request: request,
|
||||||
|
method: HTTPMethod.post,
|
||||||
|
data: data,
|
||||||
|
resultType: resultType,
|
||||||
|
dateDecoding: dateDecoding,
|
||||||
|
keyDecoding: keyDecoding
|
||||||
|
) { result in
|
||||||
|
if self.suspended {
|
||||||
|
completion(.failure(TransportError.suspended))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let response):
|
||||||
|
completion(.success(response))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
//
|
//
|
||||||
// NewsBlurAccountDelegate+Private.swift
|
// NewsBlurAccountDelegate+Internal.swift
|
||||||
// Mostly adapted from FeedbinAccountDelegate.swift
|
// Mostly adapted from FeedbinAccountDelegate.swift
|
||||||
// Account
|
// Account
|
||||||
//
|
//
|
||||||
|
@ -107,7 +107,7 @@ extension NewsBlurAccountDelegate {
|
||||||
webFeed.name = feed.name
|
webFeed.name = feed.name
|
||||||
// If the name has been changed on the server remove the locally edited name
|
// If the name has been changed on the server remove the locally edited name
|
||||||
webFeed.editedName = nil
|
webFeed.editedName = nil
|
||||||
webFeed.homePageURL = feed.homepageURL
|
webFeed.homePageURL = feed.homePageURL
|
||||||
webFeed.subscriptionID = String(feed.feedID)
|
webFeed.subscriptionID = String(feed.feedID)
|
||||||
webFeed.faviconURL = feed.faviconURL
|
webFeed.faviconURL = feed.faviconURL
|
||||||
}
|
}
|
||||||
|
@ -118,7 +118,7 @@ extension NewsBlurAccountDelegate {
|
||||||
|
|
||||||
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
// Actually add feeds all in one go, so we don’t trigger various rebuilding things that Account does.
|
||||||
feedsToAdd.forEach { feed in
|
feedsToAdd.forEach { feed in
|
||||||
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homepageURL)
|
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||||
webFeed.subscriptionID = String(feed.feedID)
|
webFeed.subscriptionID = String(feed.feedID)
|
||||||
account.addWebFeed(webFeed)
|
account.addWebFeed(webFeed)
|
||||||
}
|
}
|
||||||
|
@ -232,10 +232,10 @@ extension NewsBlurAccountDelegate {
|
||||||
caller.retrieveStories(hashes: hashesToFetch) { result in
|
caller.retrieveStories(hashes: hashesToFetch) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let stories):
|
case .success(let stories):
|
||||||
self.processStories(account: account, stories: stories) { error in
|
self.processStories(account: account, stories: stories) { result in
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
if let error = error {
|
if case .failure(let error) = result {
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -371,4 +371,104 @@ extension NewsBlurAccountDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||||
|
guard let feed = feed else {
|
||||||
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let webFeed = account.createWebFeed(with: feed.name, url: feed.feedURL, webFeedID: String(feed.feedID), homePageURL: feed.homePageURL)
|
||||||
|
webFeed.subscriptionID = String(feed.feedID)
|
||||||
|
webFeed.faviconURL = feed.faviconURL
|
||||||
|
|
||||||
|
account.addWebFeed(webFeed, to: container) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
if let name = name {
|
||||||
|
account.renameWebFeed(webFeed, to: name) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.initialFeedDownload(account: account, feed: webFeed, completion: completion)
|
||||||
|
}
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func downloadFeed(account: Account, feed: WebFeed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
|
caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let stories):
|
||||||
|
// No more stories
|
||||||
|
guard let stories = stories, stories.count > 0 else {
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let since = Calendar.current.date(byAdding: .month, value: -3, to: Date())
|
||||||
|
self.processStories(account: account, stories: stories, since: since) { result in
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
|
if case .failure(let error) = result {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// No more recent stories
|
||||||
|
if case .success(let hasStories) = result, !hasStories {
|
||||||
|
completion(.success(()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
self.downloadFeed(account: account, feed: feed, page: page + 1, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
||||||
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
|
// Download the initial articles
|
||||||
|
downloadFeed(account: account, feed: feed, page: 1) { result in
|
||||||
|
self.refreshArticleStatus(for: account) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.refreshMissingStories(for: account) { result in
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
completion(.success(feed))
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -10,20 +10,19 @@ import Foundation
|
||||||
import RSCore
|
import RSCore
|
||||||
import RSParser
|
import RSParser
|
||||||
|
|
||||||
typealias NewsBlurFeed = NewsBlurFeedsResponse.Feed
|
|
||||||
typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
|
typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
|
||||||
|
|
||||||
struct NewsBlurFeedsResponse: Decodable {
|
struct NewsBlurFeed: Hashable, Codable {
|
||||||
let feeds: [Feed]
|
let name: String
|
||||||
let folders: [Folder]
|
let feedID: Int
|
||||||
|
let feedURL: String
|
||||||
|
let homePageURL: String?
|
||||||
|
let faviconURL: String?
|
||||||
|
}
|
||||||
|
|
||||||
struct Feed: Hashable, Codable {
|
struct NewsBlurFeedsResponse: Decodable {
|
||||||
let name: String
|
let feeds: [NewsBlurFeed]
|
||||||
let feedID: Int
|
let folders: [Folder]
|
||||||
let feedURL: String
|
|
||||||
let homepageURL: String?
|
|
||||||
let faviconURL: String?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct Folder: Hashable, Codable {
|
struct Folder: Hashable, Codable {
|
||||||
let name: String
|
let name: String
|
||||||
|
@ -31,11 +30,25 @@ struct NewsBlurFeedsResponse: Decodable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct NewsBlurAddURLResponse: Decodable {
|
||||||
|
let feed: NewsBlurFeed?
|
||||||
|
}
|
||||||
|
|
||||||
struct NewsBlurFolderRelationship: Codable {
|
struct NewsBlurFolderRelationship: Codable {
|
||||||
let folderName: String
|
let folderName: String
|
||||||
let feedID: Int
|
let feedID: Int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension NewsBlurFeed {
|
||||||
|
private enum CodingKeys: String, CodingKey {
|
||||||
|
case name = "feed_title"
|
||||||
|
case feedID = "id"
|
||||||
|
case feedURL = "feed_address"
|
||||||
|
case homePageURL = "feed_link"
|
||||||
|
case faviconURL = "favicon_url"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension NewsBlurFeedsResponse {
|
extension NewsBlurFeedsResponse {
|
||||||
private enum CodingKeys: String, CodingKey {
|
private enum CodingKeys: String, CodingKey {
|
||||||
case feeds = "feeds"
|
case feeds = "feeds"
|
||||||
|
@ -47,10 +60,10 @@ extension NewsBlurFeedsResponse {
|
||||||
let container = try decoder.container(keyedBy: CodingKeys.self)
|
let container = try decoder.container(keyedBy: CodingKeys.self)
|
||||||
|
|
||||||
// Parse feeds
|
// Parse feeds
|
||||||
var feeds: [Feed] = []
|
var feeds: [NewsBlurFeed] = []
|
||||||
let feedContainer = try container.nestedContainer(keyedBy: NewsBlurGenericCodingKeys.self, forKey: .feeds)
|
let feedContainer = try container.nestedContainer(keyedBy: NewsBlurGenericCodingKeys.self, forKey: .feeds)
|
||||||
try feedContainer.allKeys.forEach { key in
|
try feedContainer.allKeys.forEach { key in
|
||||||
let subscription = try feedContainer.decode(Feed.self, forKey: key)
|
let subscription = try feedContainer.decode(NewsBlurFeed.self, forKey: key)
|
||||||
feeds.append(subscription)
|
feeds.append(subscription)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -71,16 +84,6 @@ extension NewsBlurFeedsResponse {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension NewsBlurFeedsResponse.Feed {
|
|
||||||
private enum CodingKeys: String, CodingKey {
|
|
||||||
case name = "feed_title"
|
|
||||||
case feedID = "id"
|
|
||||||
case feedURL = "feed_address"
|
|
||||||
case homepageURL = "feed_link"
|
|
||||||
case faviconURL = "favicon_url"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NewsBlurFeedsResponse.Folder {
|
extension NewsBlurFeedsResponse.Folder {
|
||||||
var asRelationships: [NewsBlurFolderRelationship] {
|
var asRelationships: [NewsBlurFolderRelationship] {
|
||||||
return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) }
|
return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) }
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
//
|
||||||
|
// NewsBlurFeedChange.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Anh Quang Do on 2020-03-14.
|
||||||
|
// Copyright (c) 2020 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
enum NewsBlurFeedChange {
|
||||||
|
case add(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
extension NewsBlurFeedChange: NewsBlurDataConvertible {
|
||||||
|
var asData: Data? {
|
||||||
|
var postData = URLComponents()
|
||||||
|
postData.queryItems = {
|
||||||
|
switch self {
|
||||||
|
case .add(let url):
|
||||||
|
return [
|
||||||
|
URLQueryItem(name: "url", value: url),
|
||||||
|
URLQueryItem(name: "folder", value: ""), // root folder
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return postData.percentEncodedQuery?.data(using: .utf8)
|
||||||
|
}
|
||||||
|
}
|
|
@ -20,7 +20,10 @@ extension NewsBlurFolderChange: NewsBlurDataConvertible {
|
||||||
postData.queryItems = {
|
postData.queryItems = {
|
||||||
switch self {
|
switch self {
|
||||||
case .add(let name):
|
case .add(let name):
|
||||||
return [URLQueryItem(name: "folder", value: name)]
|
return [
|
||||||
|
URLQueryItem(name: "folder", value: name),
|
||||||
|
URLQueryItem(name: "parent_folder", value: ""), // root folder
|
||||||
|
]
|
||||||
case .rename(let from, let to):
|
case .rename(let from, let to):
|
||||||
return [
|
return [
|
||||||
URLQueryItem(name: "folder_to_rename", value: from),
|
URLQueryItem(name: "folder_to_rename", value: from),
|
||||||
|
|
|
@ -9,33 +9,12 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
import RSWeb
|
import RSWeb
|
||||||
|
|
||||||
protocol NewsBlurDataConvertible {
|
|
||||||
var asData: Data? { get }
|
|
||||||
}
|
|
||||||
|
|
||||||
enum NewsBlurError: LocalizedError {
|
|
||||||
case general(message: String)
|
|
||||||
case invalidParameter
|
|
||||||
case unknown
|
|
||||||
|
|
||||||
var errorDescription: String? {
|
|
||||||
switch self {
|
|
||||||
case .general(let message):
|
|
||||||
return message
|
|
||||||
case .invalidParameter:
|
|
||||||
return "There was an invalid parameter passed"
|
|
||||||
case .unknown:
|
|
||||||
return "An unknown error occurred"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
final class NewsBlurAPICaller: NSObject {
|
final class NewsBlurAPICaller: NSObject {
|
||||||
static let SessionIdCookie = "newsblur_sessionid"
|
static let SessionIdCookie = "newsblur_sessionid"
|
||||||
|
|
||||||
private let baseURL = URL(string: "https://www.newsblur.com/")!
|
let baseURL = URL(string: "https://www.newsblur.com/")!
|
||||||
private var transport: Transport!
|
var transport: Transport!
|
||||||
private var suspended = false
|
var suspended = false
|
||||||
|
|
||||||
var credentials: Credentials?
|
var credentials: Credentials?
|
||||||
weak var accountMetadata: AccountMetadata?
|
weak var accountMetadata: AccountMetadata?
|
||||||
|
@ -56,15 +35,7 @@ final class NewsBlurAPICaller: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||||
let url = baseURL.appendingPathComponent("api/login")
|
requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in
|
||||||
let request = URLRequest(url: url, credentials: credentials)
|
|
||||||
|
|
||||||
transport.send(request: request, resultType: NewsBlurLoginResponse.self) { result in
|
|
||||||
if self.suspended {
|
|
||||||
completion(.failure(TransportError.suspended))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let response, let payload):
|
case .success(let response, let payload):
|
||||||
guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else {
|
guard let url = response.url, let headerFields = response.allHeaderFields as? [String: String], payload?.code != -1 else {
|
||||||
|
@ -97,22 +68,7 @@ final class NewsBlurAPICaller: NSObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
|
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
let url = baseURL.appendingPathComponent("api/logout")
|
requestData(endpoint: "api/logout", completion: completion)
|
||||||
let request = URLRequest(url: url, credentials: credentials)
|
|
||||||
|
|
||||||
transport.send(request: request) { result in
|
|
||||||
if self.suspended {
|
|
||||||
completion(.failure(TransportError.suspended))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
completion(.success(()))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
|
func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
|
||||||
|
@ -123,18 +79,7 @@ final class NewsBlurAPICaller: NSObject {
|
||||||
URLQueryItem(name: "update_counts", value: "false"),
|
URLQueryItem(name: "update_counts", value: "false"),
|
||||||
])
|
])
|
||||||
|
|
||||||
guard let callURL = url else {
|
requestData(callURL: url, resultType: NewsBlurFeedsResponse.self) { result in
|
||||||
completion(.failure(TransportError.noURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = URLRequest(url: callURL, credentials: credentials)
|
|
||||||
transport.send(request: request, resultType: NewsBlurFeedsResponse.self) { result in
|
|
||||||
if self.suspended {
|
|
||||||
completion(.failure(TransportError.suspended))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success((_, let payload)):
|
case .success((_, let payload)):
|
||||||
completion(.success((payload?.feeds, payload?.folders)))
|
completion(.success((payload?.feeds, payload?.folders)))
|
||||||
|
@ -144,92 +89,18 @@ final class NewsBlurAPICaller: NSObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
func retrieveStoryHashes(endpoint: String, completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||||
retrieveStoryHashes(endpoint: "reader/unread_story_hashes", completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
|
||||||
retrieveStoryHashes(endpoint: "reader/starred_story_hashes", completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<[NewsBlurStory]?, Error>) -> Void) {
|
|
||||||
let url = baseURL
|
|
||||||
.appendingPathComponent("reader/river_stories")
|
|
||||||
.appendingQueryItem(.init(name: "include_hidden", value: "true"))?
|
|
||||||
.appendingQueryItems(hashes.map {
|
|
||||||
URLQueryItem(name: "h", value: $0.hash)
|
|
||||||
})
|
|
||||||
|
|
||||||
guard let callURL = url else {
|
|
||||||
completion(.failure(TransportError.noURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
let request = URLRequest(url: callURL, credentials: credentials)
|
|
||||||
transport.send(request: request, resultType: NewsBlurStoriesResponse.self) { result in
|
|
||||||
if self.suspended {
|
|
||||||
completion(.failure(TransportError.suspended))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success((_, let payload)):
|
|
||||||
completion(.success(payload?.stories))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/mark_story_hash_as_unread", payload: NewsBlurStoryStatusChange(hashes: hashes), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/mark_story_hashes_as_read", payload: NewsBlurStoryStatusChange(hashes: hashes), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/mark_story_hash_as_starred", payload: NewsBlurStoryStatusChange(hashes: hashes), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/mark_story_hash_as_unstarred", payload: NewsBlurStoryStatusChange(hashes: hashes), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func addFolder(named name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/add_folder", payload: NewsBlurFolderChange.add(name), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func renameFolder(with folder: String, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/rename_folder", payload: NewsBlurFolderChange.rename(folder, name), completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
sendUpdates(endpoint: "reader/delete_folder", payload: NewsBlurFolderChange.delete(name, feedIDs), completion: completion)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension NewsBlurAPICaller {
|
|
||||||
private func retrieveStoryHashes(endpoint: String, completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
|
||||||
let url = baseURL
|
let url = baseURL
|
||||||
.appendingPathComponent(endpoint)
|
.appendingPathComponent(endpoint)
|
||||||
.appendingQueryItems([
|
.appendingQueryItems([
|
||||||
URLQueryItem(name: "include_timestamps", value: "true"),
|
URLQueryItem(name: "include_timestamps", value: "true"),
|
||||||
])
|
])
|
||||||
|
|
||||||
guard let callURL = url else {
|
requestData(
|
||||||
completion(.failure(TransportError.noURL))
|
callURL: url,
|
||||||
return
|
resultType: NewsBlurStoryHashesResponse.self,
|
||||||
}
|
dateDecoding: .secondsSince1970
|
||||||
|
) { result in
|
||||||
let request = URLRequest(url: callURL, credentials: credentials)
|
|
||||||
transport.send(request: request, resultType: NewsBlurStoryHashesResponse.self, dateDecoding: .secondsSince1970) { result in
|
|
||||||
if self.suspended {
|
|
||||||
completion(.failure(TransportError.suspended))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success((_, let payload)):
|
case .success((_, let payload)):
|
||||||
let hashes = payload?.unread ?? payload?.starred
|
let hashes = payload?.unread ?? payload?.starred
|
||||||
|
@ -240,20 +111,124 @@ extension NewsBlurAPICaller {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func sendUpdates(endpoint: String, payload: NewsBlurDataConvertible, completion: @escaping (Result<Void, Error>) -> Void) {
|
func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||||
let callURL = baseURL.appendingPathComponent(endpoint)
|
retrieveStoryHashes(
|
||||||
|
endpoint: "reader/unread_story_hashes",
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
var request = URLRequest(url: callURL, credentials: credentials)
|
func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
|
||||||
request.httpBody = payload.asData
|
retrieveStoryHashes(
|
||||||
transport.send(request: request, method: HTTPMethod.post) { result in
|
endpoint: "reader/starred_story_hashes",
|
||||||
if self.suspended {
|
completion: completion
|
||||||
completion(.failure(TransportError.suspended))
|
)
|
||||||
return
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<[NewsBlurStory]?, Error>) -> Void) {
|
||||||
|
let url = baseURL
|
||||||
|
.appendingPathComponent("reader/feed/\(feedID)")
|
||||||
|
.appendingQueryItems([
|
||||||
|
URLQueryItem(name: "page", value: String(page)),
|
||||||
|
URLQueryItem(name: "order", value: "newest"),
|
||||||
|
URLQueryItem(name: "read_filter", value: "all"),
|
||||||
|
URLQueryItem(name: "include_hidden", value: "true"),
|
||||||
|
URLQueryItem(name: "include_story_content", value: "true"),
|
||||||
|
])
|
||||||
|
|
||||||
|
requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success((_, let payload)):
|
||||||
completion(.success(()))
|
completion(.success(payload?.stories))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<[NewsBlurStory]?, Error>) -> Void) {
|
||||||
|
let url = baseURL
|
||||||
|
.appendingPathComponent("reader/river_stories")
|
||||||
|
.appendingQueryItem(.init(name: "include_hidden", value: "true"))?
|
||||||
|
.appendingQueryItems(hashes.map {
|
||||||
|
URLQueryItem(name: "h", value: $0.hash)
|
||||||
|
})
|
||||||
|
|
||||||
|
requestData(callURL: url, resultType: NewsBlurStoriesResponse.self) { result in
|
||||||
|
switch result {
|
||||||
|
case .success((_, let payload)):
|
||||||
|
completion(.success(payload?.stories))
|
||||||
|
case .failure(let error):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/mark_story_hash_as_unread",
|
||||||
|
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/mark_story_hashes_as_read",
|
||||||
|
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/mark_story_hash_as_starred",
|
||||||
|
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/mark_story_hash_as_unstarred",
|
||||||
|
payload: NewsBlurStoryStatusChange(hashes: hashes),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFolder(named name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/add_folder",
|
||||||
|
payload: NewsBlurFolderChange.add(name),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func renameFolder(with folder: String, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/rename_folder",
|
||||||
|
payload: NewsBlurFolderChange.rename(folder, name),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/delete_folder",
|
||||||
|
payload: NewsBlurFolderChange.delete(name, feedIDs),
|
||||||
|
completion: completion
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addURL(_ url: String, completion: @escaping (Result<NewsBlurFeed?, Error>) -> Void) {
|
||||||
|
sendUpdates(
|
||||||
|
endpoint: "reader/add_url",
|
||||||
|
payload: NewsBlurFeedChange.add(url),
|
||||||
|
resultType: NewsBlurAddURLResponse.self
|
||||||
|
) { result in
|
||||||
|
switch result {
|
||||||
|
case .success(_, let payload):
|
||||||
|
completion(.success(payload?.feed))
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
completion(.failure(error))
|
completion(.failure(error))
|
||||||
}
|
}
|
||||||
|
|
|
@ -125,10 +125,18 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
database.selectForProcessing { result in
|
database.selectForProcessing { result in
|
||||||
|
|
||||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||||
let createUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == false }
|
let createUnreadStatuses = syncStatuses.filter {
|
||||||
let deleteUnreadStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.read && $0.flag == true }
|
$0.key == ArticleStatus.Key.read && $0.flag == false
|
||||||
let createStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == true }
|
}
|
||||||
let deleteStarredStatuses = syncStatuses.filter { $0.key == ArticleStatus.Key.starred && $0.flag == false }
|
let deleteUnreadStatuses = syncStatuses.filter {
|
||||||
|
$0.key == ArticleStatus.Key.read && $0.flag == true
|
||||||
|
}
|
||||||
|
let createStarredStatuses = syncStatuses.filter {
|
||||||
|
$0.key == ArticleStatus.Key.starred && $0.flag == true
|
||||||
|
}
|
||||||
|
let deleteStarredStatuses = syncStatuses.filter {
|
||||||
|
$0.key == ArticleStatus.Key.starred && $0.flag == false
|
||||||
|
}
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
var errorOccurred = false
|
var errorOccurred = false
|
||||||
|
@ -246,15 +254,18 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshMissingStories(for account: Account, completion: @escaping (Result<Void, Error>)-> Void) {
|
func refreshMissingStories(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
os_log(.debug, log: log, "Refreshing missing stories...")
|
os_log(.debug, log: log, "Refreshing missing stories...")
|
||||||
|
|
||||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { result in
|
||||||
|
|
||||||
func process(_ fetchedHashes: Set<String>) {
|
func process(_ fetchedHashes: Set<String>) {
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
var errorOccurred = false
|
var errorOccurred = false
|
||||||
|
|
||||||
let storyHashes = Array(fetchedHashes).map { NewsBlurStoryHash(hash: $0, timestamp: Date()) }
|
let storyHashes = Array(fetchedHashes).map {
|
||||||
|
NewsBlurStoryHash(hash: $0, timestamp: Date())
|
||||||
|
}
|
||||||
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
let chunkedStoryHashes = storyHashes.chunked(into: 100)
|
||||||
|
|
||||||
for chunk in chunkedStoryHashes {
|
for chunk in chunkedStoryHashes {
|
||||||
|
@ -263,9 +274,9 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let stories):
|
case .success(let stories):
|
||||||
self.processStories(account: account, stories: stories) { error in
|
self.processStories(account: account, stories: stories) { result in
|
||||||
group.leave()
|
group.leave()
|
||||||
if error != nil {
|
if case .failure = result {
|
||||||
errorOccurred = true
|
errorOccurred = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -298,10 +309,26 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processStories(account: Account, stories: [NewsBlurStory]?, completion: @escaping DatabaseCompletionBlock) {
|
func processStories(account: Account, stories: [NewsBlurStory]?, since: Date? = nil, completion: @escaping (Result<Bool, DatabaseError>) -> Void) {
|
||||||
let parsedItems = mapStoriesToParsedItems(stories: stories)
|
let parsedItems = mapStoriesToParsedItems(stories: stories).filter {
|
||||||
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
guard let datePublished = $0.datePublished, let since = since else {
|
||||||
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true, completion: completion)
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return datePublished >= since
|
||||||
|
}
|
||||||
|
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL }).mapValues {
|
||||||
|
Set($0)
|
||||||
|
}
|
||||||
|
|
||||||
|
account.update(webFeedIDsAndItems: webFeedIDsAndItems, defaultRead: true) { error in
|
||||||
|
if let error = error {
|
||||||
|
completion(.failure(error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
completion(.success(!webFeedIDsAndItems.isEmpty))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> ()) {
|
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
|
@ -383,13 +410,43 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> ()) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> ()) {
|
||||||
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
|
caller.addURL(url) { result in
|
||||||
|
self.refreshProgress.completeTask()
|
||||||
|
|
||||||
|
switch result {
|
||||||
|
case .success(let feed):
|
||||||
|
self.createFeed(account: account, feed: feed, name: name, container: container, completion: completion)
|
||||||
|
case .failure(let error):
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||||
|
completion(.failure(wrappedError))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
|
guard let folder = container as? Folder else {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if let account = container as? Account {
|
||||||
|
account.addFeedIfNotInAnyFolder(feed)
|
||||||
|
}
|
||||||
|
completion(.success(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let folderName = folder.name ?? ""
|
||||||
|
saveFolderRelationship(for: feed, withFolderName: folderName, id: folderName)
|
||||||
|
folder.addWebFeed(feed)
|
||||||
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue