Create NewsBlur module.
This commit is contained in:
parent
0c5bcbbeb9
commit
8c2db159d2
|
@ -22,6 +22,7 @@ let package = Package(
|
|||
.package(path: "../CloudKitExtras"),
|
||||
.package(path: "../ReaderAPI"),
|
||||
.package(path: "../CloudKitSync"),
|
||||
.package(path: "../NewsBlur"),
|
||||
.package(path: "../CommonErrors")
|
||||
],
|
||||
targets: [
|
||||
|
@ -38,6 +39,7 @@ let package = Package(
|
|||
"Core",
|
||||
"CloudKitExtras",
|
||||
"ReaderAPI",
|
||||
"NewsBlur",
|
||||
"CloudKitSync",
|
||||
"CommonErrors"
|
||||
],
|
||||
|
|
|
@ -14,6 +14,7 @@ import Web
|
|||
import SyncDatabase
|
||||
import os.log
|
||||
import Core
|
||||
import NewsBlur
|
||||
|
||||
extension NewsBlurAccountDelegate {
|
||||
|
|
@ -13,6 +13,7 @@ import Web
|
|||
import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
import NewsBlur
|
||||
|
||||
final class NewsBlurAccountDelegate: AccountDelegate {
|
||||
|
||||
|
|
|
@ -11,15 +11,15 @@ import Web
|
|||
import Secrets
|
||||
|
||||
public extension URLRequest {
|
||||
|
||||
|
||||
init(url: URL, credentials: Credentials?, conditionalGet: HTTPConditionalGetInfo? = nil) {
|
||||
|
||||
|
||||
self.init(url: url)
|
||||
|
||||
|
||||
guard let credentials = credentials else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
switch credentials.type {
|
||||
case .basic:
|
||||
let data = "\(credentials.username):\(credentials.secret)".data(using: .utf8)
|
||||
|
@ -39,8 +39,8 @@ public extension URLRequest {
|
|||
setValue("\(NewsBlurAPICaller.SessionIdCookie)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
|
||||
httpShouldHandleCookies = true
|
||||
case .readerBasic:
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
httpMethod = "POST"
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
|
||||
httpMethod = "POST"
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = [
|
||||
URLQueryItem(name: "Email", value: credentials.username),
|
||||
|
@ -48,36 +48,23 @@ public extension URLRequest {
|
|||
]
|
||||
httpBody = postData.enhancedPercentEncodedQuery?.data(using: .utf8)
|
||||
case .readerAPIKey:
|
||||
let auth = "GoogleLogin auth=\(credentials.secret)"
|
||||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
let auth = "GoogleLogin auth=\(credentials.secret)"
|
||||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
case .oauthAccessToken:
|
||||
let auth = "OAuth \(credentials.secret)"
|
||||
setValue(auth, forHTTPHeaderField: "Authorization")
|
||||
let auth = "OAuth \(credentials.secret)"
|
||||
setValue(auth, forHTTPHeaderField: "Authorization")
|
||||
case .oauthAccessTokenSecret:
|
||||
assertionFailure("Token secrets are used by OAuth1. Did you mean to use `OAuthSwift` instead of a URLRequest?")
|
||||
break
|
||||
case .oauthRefreshToken:
|
||||
// While both access and refresh tokens are credentials, it seems the `Credentials` cases
|
||||
// enumerates how the identity of the user can be proved rather than
|
||||
// credentials-in-general, such as in this refresh token case,
|
||||
// the authority to prove an identity.
|
||||
assertionFailure("Refresh tokens are used to replace expired access tokens. Did you mean to use `accessToken` instead?")
|
||||
break
|
||||
}
|
||||
|
||||
guard let conditionalGet = conditionalGet else {
|
||||
return
|
||||
assertionFailure("Token secrets are used by OAuth1. Did you mean to use `OAuthSwift` instead of a URLRequest?")
|
||||
break
|
||||
case .oauthRefreshToken:
|
||||
// While both access and refresh tokens are credentials, it seems the `Credentials` cases
|
||||
// enumerates how the identity of the user can be proved rather than
|
||||
// credentials-in-general, such as in this refresh token case,
|
||||
// the authority to prove an identity.
|
||||
assertionFailure("Refresh tokens are used to replace expired access tokens. Did you mean to use `accessToken` instead?")
|
||||
break
|
||||
}
|
||||
|
||||
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
|
||||
// TODO: drop this check in late 2037.
|
||||
if let lastModified = conditionalGet.lastModified, !lastModified.contains("2038") {
|
||||
setValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
|
||||
}
|
||||
if let etag = conditionalGet.etag {
|
||||
setValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
|
||||
}
|
||||
|
||||
|
||||
conditionalGet?.addRequestHeadersToURLRequest(&self)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1465,6 +1465,7 @@
|
|||
84F9EAE1213660A100CF2DE4 /* testGenericScript.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = testGenericScript.applescript; sourceTree = "<group>"; };
|
||||
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = "<group>"; };
|
||||
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsBlur; sourceTree = "<group>"; };
|
||||
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
|
||||
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
|
||||
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||
|
@ -2357,6 +2358,7 @@
|
|||
849C64611ED37A5D003D8FC0 /* Products */,
|
||||
51C452B22265141B00C03939 /* Frameworks */,
|
||||
51CD32C624D2DEF9009ABAEF /* Account */,
|
||||
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */,
|
||||
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,
|
||||
845F3D2B2BC268FE00AEBB68 /* CloudKitSync */,
|
||||
8410C4A62BC221C900D4F799 /* CommonErrors */,
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
|
@ -0,0 +1,38 @@
|
|||
// swift-tools-version: 5.10
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "NewsBlur",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
// Products define the executables and libraries a package produces, making them visible to other packages.
|
||||
.library(
|
||||
name: "NewsBlur",
|
||||
targets: ["NewsBlur"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../Web"),
|
||||
.package(path: "../Secrets"),
|
||||
.package(path: "../Parser"),
|
||||
],
|
||||
targets: [
|
||||
// Targets are the basic building blocks of a package, defining a module or a test suite.
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.target(
|
||||
name: "NewsBlur",
|
||||
dependencies: [
|
||||
"Web",
|
||||
"Parser",
|
||||
"Secrets"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "NewsBlurTests",
|
||||
dependencies: ["NewsBlur"]),
|
||||
]
|
||||
)
|
|
@ -44,7 +44,7 @@ extension NewsBlurAPICaller {
|
|||
}
|
||||
|
||||
// GET endpoint
|
||||
func requestData<R: Decodable>(
|
||||
func requestData<R: Decodable & Sendable>(
|
||||
endpoint: String,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
|
@ -74,7 +74,7 @@ extension NewsBlurAPICaller {
|
|||
}
|
||||
|
||||
// POST to endpoint
|
||||
func sendUpdates<R: Decodable>(
|
||||
func sendUpdates<R: Decodable & Sendable>(
|
||||
endpoint: String,
|
||||
payload: NewsBlurDataConvertible,
|
||||
resultType: R.Type,
|
||||
|
@ -108,25 +108,21 @@ extension NewsBlurAPICaller {
|
|||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let request = URLRequest(url: callURL, newsBlurCredentials: credentials)
|
||||
|
||||
transport.send(request: request) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
do {
|
||||
try await transport.send(request: request)
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GET URL with params
|
||||
func requestData<R: Decodable>(
|
||||
func requestData<R: Decodable & Sendable>(
|
||||
callURL: URL?,
|
||||
resultType: R.Type,
|
||||
dateDecoding: JSONDecoder.DateDecodingStrategy = .iso8601,
|
||||
|
@ -138,23 +134,20 @@ extension NewsBlurAPICaller {
|
|||
return
|
||||
}
|
||||
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let request = URLRequest(url: callURL, newsBlurCredentials: credentials)
|
||||
|
||||
transport.send(
|
||||
request: request,
|
||||
resultType: resultType,
|
||||
dateDecoding: dateDecoding,
|
||||
keyDecoding: keyDecoding
|
||||
) { result in
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
do {
|
||||
let response = try await transport.send(request: request, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding)
|
||||
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
completion(.success(response))
|
||||
case .failure(let error):
|
||||
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
@ -162,42 +155,42 @@ extension NewsBlurAPICaller {
|
|||
|
||||
// POST to URL with params, discard response
|
||||
func sendUpdates(
|
||||
callURL: URL?,
|
||||
payload: NewsBlurDataConvertible,
|
||||
completion: @escaping (Result<Void, Error>) -> Void
|
||||
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)
|
||||
var request = URLRequest(url: callURL, newsBlurCredentials: 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
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
do {
|
||||
try await transport.send(request: request, method: HTTPMethod.post)
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
} catch {
|
||||
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
|
||||
func sendUpdates<R: Decodable & Sendable>(
|
||||
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))
|
||||
|
@ -209,26 +202,23 @@ extension NewsBlurAPICaller {
|
|||
return
|
||||
}
|
||||
|
||||
var request = URLRequest(url: callURL, credentials: credentials)
|
||||
var request = URLRequest(url: callURL, newsBlurCredentials: 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
|
||||
}
|
||||
Task { @MainActor in
|
||||
|
||||
do {
|
||||
|
||||
let response = try await transport.send(request: request, method: HTTPMethod.post, data: data, resultType: resultType, dateDecoding: dateDecoding, keyDecoding: keyDecoding)
|
||||
|
||||
if self.suspended {
|
||||
completion(.failure(TransportError.suspended))
|
||||
return
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let response):
|
||||
completion(.success(response))
|
||||
case .failure(let error):
|
||||
|
||||
} catch {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ import Foundation
|
|||
import Web
|
||||
import Secrets
|
||||
|
||||
final class NewsBlurAPICaller: NSObject {
|
||||
@MainActor public final class NewsBlurAPICaller: NSObject {
|
||||
static let SessionIdCookie = "newsblur_sessionid"
|
||||
|
||||
let baseURL = URL(string: "https://www.newsblur.com/")!
|
||||
|
@ -18,7 +18,6 @@ final class NewsBlurAPICaller: NSObject {
|
|||
var suspended = false
|
||||
|
||||
var credentials: Credentials?
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
|
||||
init(transport: Transport!) {
|
||||
super.init()
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Brent Simmons on 4/7/24.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Web
|
||||
import Secrets
|
||||
|
||||
public extension URLRequest {
|
||||
|
||||
@MainActor init(url: URL, newsBlurCredentials: Credentials?, conditionalGet: HTTPConditionalGetInfo? = nil) {
|
||||
|
||||
self.init(url: url)
|
||||
|
||||
guard let credentials = newsBlurCredentials else {
|
||||
return
|
||||
}
|
||||
|
||||
let credentialsType = credentials.type
|
||||
precondition(credentialsType == .newsBlurBasic || credentialsType == .newsBlurSessionId)
|
||||
|
||||
if credentialsType == .newsBlurBasic {
|
||||
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
httpMethod = "POST"
|
||||
|
||||
var postData = URLComponents()
|
||||
postData.queryItems = [
|
||||
URLQueryItem(name: "username", value: credentials.username),
|
||||
URLQueryItem(name: "password", value: credentials.secret),
|
||||
]
|
||||
httpBody = postData.enhancedPercentEncodedQuery?.data(using: .utf8)
|
||||
|
||||
} else if credentialsType == .newsBlurSessionId {
|
||||
|
||||
setValue("\(NewsBlurAPICaller.SessionIdCookie)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
|
||||
httpShouldHandleCookies = true
|
||||
}
|
||||
|
||||
conditionalGet?.addRequestHeadersToURLRequest(&self)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
import XCTest
|
||||
@testable import NewsBlur
|
||||
|
||||
final class NewsBlurTests: XCTestCase {
|
||||
func testExample() throws {
|
||||
// XCTest Documentation
|
||||
// https://developer.apple.com/documentation/xctest
|
||||
|
||||
// Defining Test Cases and Test Methods
|
||||
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
|
||||
}
|
||||
}
|
|
@ -39,17 +39,6 @@ extension URLRequest {
|
|||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
}
|
||||
|
||||
guard let conditionalGet = conditionalGet else {
|
||||
return
|
||||
}
|
||||
|
||||
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
|
||||
// TODO: drop this check in late 2037.
|
||||
if let lastModified = conditionalGet.lastModified, !lastModified.contains("2038") {
|
||||
setValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
|
||||
}
|
||||
if let etag = conditionalGet.etag {
|
||||
setValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
|
||||
}
|
||||
conditionalGet?.addRequestHeadersToURLRequest(&self)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,10 +37,10 @@ public struct HTTPConditionalGetInfo: Codable, Equatable {
|
|||
// Bug seen in the wild: lastModified with last possible 32-bit date, which is in 2038. Ignore those.
|
||||
// TODO: drop this check in late 2037.
|
||||
if let lastModified = lastModified, !lastModified.contains("2038") {
|
||||
urlRequest.addValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
|
||||
urlRequest.setValue(lastModified, forHTTPHeaderField: HTTPRequestHeader.ifModifiedSince)
|
||||
}
|
||||
if let etag = etag {
|
||||
urlRequest.addValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
|
||||
urlRequest.setValue(etag, forHTTPHeaderField: HTTPRequestHeader.ifNoneMatch)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue