Create NewsBlur module.

This commit is contained in:
Brent Simmons 2024-04-07 13:58:14 -07:00
parent 0c5bcbbeb9
commit 8c2db159d2
21 changed files with 185 additions and 111 deletions

View File

@ -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"
],

View File

@ -14,6 +14,7 @@ import Web
import SyncDatabase
import os.log
import Core
import NewsBlur
extension NewsBlurAccountDelegate {

View File

@ -13,6 +13,7 @@ import Web
import SyncDatabase
import os.log
import Secrets
import NewsBlur
final class NewsBlurAccountDelegate: AccountDelegate {

View File

@ -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)
}
}

View File

@ -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 */,

8
NewsBlur/.gitignore vendored Normal file
View File

@ -0,0 +1,8 @@
.DS_Store
/.build
/Packages
xcuserdata/
DerivedData/
.swiftpm/configuration/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

38
NewsBlur/Package.swift Normal file
View File

@ -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"]),
]
)

View File

@ -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))
}
}

View File

@ -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()

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}
}