Create NewsBlur local package.

This commit is contained in:
Brent Simmons
2023-08-27 17:36:57 -07:00
parent 4b2fcae96e
commit 92623222fd
20 changed files with 152 additions and 69 deletions

View File

@@ -14,6 +14,7 @@ dependencies.append(contentsOf: [
.package(path: "../ArticlesDatabase"),
.package(path: "../Secrets"),
.package(path: "../SyncDatabase"),
.package(path: "../SyncClients/NewsBlur"),
])
#else
dependencies.append(contentsOf: [
@@ -47,6 +48,7 @@ let package = Package(
"ArticlesDatabase",
"Secrets",
"SyncDatabase",
"NewsBlur"
],
linkerSettings: [
.unsafeFlags(["-Xlinker", "-no_application_extension"])

View File

@@ -13,6 +13,7 @@ import RSDatabase
import RSParser
import RSWeb
import SyncDatabase
import NewsBlur
extension NewsBlurAccountDelegate {

View File

@@ -13,6 +13,7 @@ import RSParser
import RSWeb
import SyncDatabase
import Secrets
import NewsBlur
final class NewsBlurAccountDelegate: AccountDelegate, Logging {
@@ -34,7 +35,9 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
init(dataFolder: String, transport: Transport?) {
if let transport = transport {
caller = NewsBlurAPICaller(transport: transport)
caller = NewsBlurAPICaller(transport: transport) { url, credentials in
URLRequest(url: url, credentials: credentials)
}
} else {
let sessionConfiguration = URLSessionConfiguration.default
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
@@ -50,7 +53,9 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
}
let session = URLSession(configuration: sessionConfiguration)
caller = NewsBlurAPICaller(transport: session)
caller = NewsBlurAPICaller(transport: session) { url, credentials in
URLRequest(url: url, credentials: credentials)
}
}
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
@@ -644,7 +649,9 @@ final class NewsBlurAccountDelegate: AccountDelegate, Logging {
}
class func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> ()) {
let caller = NewsBlurAPICaller(transport: transport)
let caller = NewsBlurAPICaller(transport: transport) { url, credentials in
URLRequest(url: url, credentials: credentials)
}
caller.credentials = credentials
caller.validateCredentials() { result in
DispatchQueue.main.async {

View File

@@ -9,6 +9,7 @@
import Foundation
import RSWeb
import Secrets
import NewsBlur
public extension URLRequest {

View File

@@ -1379,6 +1379,7 @@
848363072262A3DD00DA1D35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
8483630A2262A3F000DA1D35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/RenameSheet.xib; sourceTree = SOURCE_ROOT; };
8483630D2262A3FE00DA1D35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Mac/Base.lproj/MainWindow.storyboard; sourceTree = SOURCE_ROOT; };
8486EC3E2A9BE083007EF90D /* NewsBlur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = NewsBlur; path = SyncClients/NewsBlur; sourceTree = "<group>"; };
848B937121C8C5540038DC0D /* CrashReporter.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CrashReporter.swift; sourceTree = "<group>"; };
848D578D21543519005FFAD5 /* PasteboardFeed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardFeed.swift; sourceTree = "<group>"; };
848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconDownloader.swift; sourceTree = "<group>"; };
@@ -2501,6 +2502,7 @@
849C64611ED37A5D003D8FC0 /* Products */,
51C452B22265141B00C03939 /* Frameworks */,
51CD32C624D2DEF9009ABAEF /* Account */,
8486EC3E2A9BE083007EF90D /* NewsBlur */,
51CD32C424D2CF1D009ABAEF /* Articles */,
51CD32C324D2CD57009ABAEF /* ArticlesDatabase */,
51CD32C724D2E06C009ABAEF /* Secrets */,

9
SyncClients/NewsBlur/.gitignore vendored Normal file
View File

@@ -0,0 +1,9 @@
.DS_Store
/.build
/Packages
/*.xcodeproj
xcuserdata/
DerivedData/
.swiftpm/config/registries.json
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
.netrc

View File

@@ -0,0 +1,36 @@
// swift-tools-version: 5.8
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "NewsBlur",
platforms: [.macOS(.v13), .iOS(.v16)],
products: [
// Products define the executables and libraries a package produces, and make them visible to other packages.
.library(
name: "NewsBlur",
targets: ["NewsBlur"]),
],
dependencies: [
.package(path: "../Secrets"),
.package(url: "https://github.com/Ranchero-Software/RSWeb.git", .upToNextMajor(from: "1.0.0")),
.package(url: "https://github.com/Ranchero-Software/RSCore.git", .upToNextMajor(from: "2.0.1")),
.package(url: "https://github.com/Ranchero-Software/RSParser.git", .upToNextMajor(from: "2.0.2"))
],
targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.target(
name: "NewsBlur",
dependencies: [
"Secrets",
"RSWeb",
"RSParser",
"RSCore"
]),
.testTarget(
name: "NewsBlurTests",
dependencies: ["NewsBlur"]),
]
)

View File

@@ -0,0 +1,3 @@
# NewsBlur
A description of this package.

View File

@@ -10,23 +10,23 @@ import Foundation
import RSCore
import RSParser
typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
public typealias NewsBlurFolder = NewsBlurFeedsResponse.Folder
struct NewsBlurFeed: Hashable, Codable {
let name: String
let feedID: Int
let feedURL: String
let homePageURL: String?
let faviconURL: String?
public struct NewsBlurFeed: Hashable, Codable {
public let name: String
public let feedID: Int
public let feedURL: String
public let homePageURL: String?
public let faviconURL: String?
}
struct NewsBlurFeedsResponse: Decodable {
public struct NewsBlurFeedsResponse: Decodable {
let feeds: [NewsBlurFeed]
let folders: [Folder]
struct Folder: Hashable, Codable {
let name: String
let feedIDs: [Int]
public struct Folder: Hashable, Codable {
public let name: String
public let feedIDs: [Int]
}
}
@@ -34,9 +34,9 @@ struct NewsBlurAddURLResponse: Decodable {
let feed: NewsBlurFeed?
}
struct NewsBlurFolderRelationship {
let folderName: String
let feedID: Int
public struct NewsBlurFolderRelationship {
public let folderName: String
public let feedID: Int
}
extension NewsBlurFeed {
@@ -56,7 +56,7 @@ extension NewsBlurFeedsResponse {
// TODO: flat_folders_with_inactive
}
init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Tricky part: Some feeds listed in `feeds` don't exist in `folders` for some reason
@@ -89,7 +89,7 @@ extension NewsBlurFeedsResponse {
}
extension NewsBlurFeedsResponse.Folder {
var asRelationships: [NewsBlurFolderRelationship] {
public var asRelationships: [NewsBlurFolderRelationship] {
return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) }
}
}

View File

@@ -10,23 +10,23 @@ import Foundation
import RSCore
import RSParser
typealias NewsBlurStory = NewsBlurStoriesResponse.Story
public typealias NewsBlurStory = NewsBlurStoriesResponse.Story
struct NewsBlurStoriesResponse: Decodable {
public struct NewsBlurStoriesResponse: Decodable {
let stories: [Story]
struct Story: Decodable {
let storyID: String
let feedID: Int
let title: String?
let url: String?
let authorName: String?
let contentHTML: String?
var imageURL: String? {
public struct Story: Decodable {
public let storyID: String
public let feedID: Int
public let title: String?
public let url: String?
public let authorName: String?
public let contentHTML: String?
public var imageURL: String? {
return imageURLs?.first?.value
}
var tags: [String]?
var datePublished: Date? {
public var tags: [String]?
public var datePublished: Date? {
let interval = (publishedTimestamp as NSString).doubleValue
return Date(timeIntervalSince1970: interval)
}

View File

@@ -10,17 +10,22 @@ import Foundation
import RSCore
import RSParser
typealias NewsBlurStoryHash = NewsBlurStoryHashesResponse.StoryHash
public typealias NewsBlurStoryHash = NewsBlurStoryHashesResponse.StoryHash
struct NewsBlurStoryHashesResponse: Decodable {
public struct NewsBlurStoryHashesResponse: Decodable {
typealias StoryHashDictionary = [String: [StoryHash]]
var unread: [StoryHash]?
var starred: [StoryHash]?
struct StoryHash: Hashable, Codable {
var hash: String
var timestamp: Date
public struct StoryHash: Hashable, Codable {
public var hash: String
public var timestamp: Date
public init(hash: String, timestamp: Date) {
self.hash = hash
self.timestamp = timestamp
}
}
}
@@ -30,7 +35,7 @@ extension NewsBlurStoryHashesResponse {
case starred = "starred_story_hashes"
}
init(from decoder: Decoder) throws {
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// Parse unread

View File

@@ -0,0 +1,6 @@
public struct NewsBlur {
public private(set) var text = "Hello, World!"
public init() {
}
}

View File

@@ -13,12 +13,12 @@ protocol NewsBlurDataConvertible {
var asData: Data? { get }
}
enum NewsBlurError: LocalizedError {
public enum NewsBlurError: LocalizedError {
case general(message: String)
case invalidParameter
case unknown
var errorDescription: String? {
public var errorDescription: String? {
switch self {
case .general(let message):
return message
@@ -108,7 +108,7 @@ extension NewsBlurAPICaller {
return
}
let request = URLRequest(url: callURL, credentials: credentials)
let request = createURLRequest(callURL, credentials)
transport.send(request: request) { result in
if self.suspended {
@@ -138,7 +138,7 @@ extension NewsBlurAPICaller {
return
}
let request = URLRequest(url: callURL, credentials: credentials)
let request = createURLRequest(callURL, credentials)
transport.send(
request: request,
@@ -171,7 +171,7 @@ extension NewsBlurAPICaller {
return
}
var request = URLRequest(url: callURL, credentials: credentials)
var request = createURLRequest(callURL, credentials)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
request.httpBody = payload.asData
@@ -209,7 +209,7 @@ extension NewsBlurAPICaller {
return
}
var request = URLRequest(url: callURL, credentials: credentials)
var request = createURLRequest(callURL, credentials)
request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
transport.send(

View File

@@ -10,32 +10,32 @@ import Foundation
import RSWeb
import Secrets
final class NewsBlurAPICaller: NSObject {
static let SessionIdCookie = "newsblur_sessionid"
public final class NewsBlurAPICaller {
public static let SessionIdCookie = "newsblur_sessionid"
let baseURL = URL(string: "https://www.newsblur.com/")!
var transport: Transport!
let createURLRequest: ((URL, Credentials?) -> URLRequest)
var suspended = false
var credentials: Credentials?
weak var accountMetadata: AccountMetadata?
public var credentials: Credentials?
init(transport: Transport!) {
super.init()
public init(transport: Transport!, createURLRequest: @escaping (URL, Credentials?) -> URLRequest) {
self.transport = transport
self.createURLRequest = createURLRequest
}
/// Cancels all pending requests rejects any that come in later
func suspend() {
public func suspend() {
transport.cancelAll()
suspended = true
}
func resume() {
public func resume() {
suspended = false
}
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
public func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in
switch result {
case .success((let response, let payload)):
@@ -68,11 +68,11 @@ final class NewsBlurAPICaller: NSObject {
}
}
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
public func logout(completion: @escaping (Result<Void, Error>) -> Void) {
requestData(endpoint: "api/logout", completion: completion)
}
func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
public func retrieveFeeds(completion: @escaping (Result<([NewsBlurFeed]?, [NewsBlurFolder]?), Error>) -> Void) {
let url = baseURL
.appendingPathComponent("reader/feeds")
.appendingQueryItems([
@@ -112,21 +112,21 @@ final class NewsBlurAPICaller: NSObject {
}
}
func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
public func retrieveUnreadStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
retrieveStoryHashes(
endpoint: "reader/unread_story_hashes",
completion: completion
)
}
func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
public func retrieveStarredStoryHashes(completion: @escaping (Result<[NewsBlurStoryHash]?, Error>) -> Void) {
retrieveStoryHashes(
endpoint: "reader/starred_story_hashes",
completion: completion
)
}
func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
public func retrieveStories(feedID: String, page: Int, completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
let url = baseURL
.appendingPathComponent("reader/feed/\(feedID)")
.appendingQueryItems([
@@ -147,7 +147,7 @@ final class NewsBlurAPICaller: NSObject {
}
}
func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
public func retrieveStories(hashes: [NewsBlurStoryHash], completion: @escaping (Result<([NewsBlurStory]?, Date?), Error>) -> Void) {
let url = baseURL
.appendingPathComponent("reader/river_stories")
.appendingQueryItem(.init(name: "include_hidden", value: "false"))?
@@ -165,7 +165,7 @@ final class NewsBlurAPICaller: NSObject {
}
}
func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
public func markAsUnread(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/mark_story_hash_as_unread",
payload: NewsBlurStoryStatusChange(hashes: hashes),
@@ -173,7 +173,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
public func markAsRead(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/mark_story_hashes_as_read",
payload: NewsBlurStoryStatusChange(hashes: hashes),
@@ -181,7 +181,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
public func star(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/mark_story_hash_as_starred",
payload: NewsBlurStoryStatusChange(hashes: hashes),
@@ -189,7 +189,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
public func unstar(hashes: [String], completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/mark_story_hash_as_unstarred",
payload: NewsBlurStoryStatusChange(hashes: hashes),
@@ -197,7 +197,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func addFolder(named name: String, completion: @escaping (Result<Void, Error>) -> Void) {
public func addFolder(named name: String, completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/add_folder",
payload: NewsBlurFolderChange.add(name),
@@ -205,7 +205,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func renameFolder(with folder: String, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
public func renameFolder(with folder: String, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/rename_folder",
payload: NewsBlurFolderChange.rename(folder, name),
@@ -213,7 +213,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result<Void, Error>) -> Void) {
public func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/delete_folder",
payload: NewsBlurFolderChange.delete(name, feedIDs),
@@ -221,7 +221,7 @@ final class NewsBlurAPICaller: NSObject {
)
}
func addURL(_ url: String, folder: String?, completion: @escaping (Result<NewsBlurFeed?, Error>) -> Void) {
public func addURL(_ url: String, folder: String?, completion: @escaping (Result<NewsBlurFeed?, Error>) -> Void) {
sendUpdates(
endpoint: "reader/add_url",
payload: NewsBlurFeedChange.add(url, folder),
@@ -236,7 +236,7 @@ final class NewsBlurAPICaller: NSObject {
}
}
func renameFeed(feedID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
public func renameFeed(feedID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/rename_feed",
payload: NewsBlurFeedChange.rename(feedID, newName)
@@ -250,7 +250,7 @@ final class NewsBlurAPICaller: NSObject {
}
}
func deleteFeed(feedID: String, folder: String? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
public func deleteFeed(feedID: String, folder: String? = nil, completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/delete_feed",
payload: NewsBlurFeedChange.delete(feedID, folder)
@@ -264,7 +264,7 @@ final class NewsBlurAPICaller: NSObject {
}
}
func moveFeed(feedID: String, from: String?, to: String?, completion: @escaping (Result<Void, Error>) -> Void) {
public func moveFeed(feedID: String, from: String?, to: String?, completion: @escaping (Result<Void, Error>) -> Void) {
sendUpdates(
endpoint: "reader/move_feed_to_folder",
payload: NewsBlurFeedChange.move(feedID, from, to)

View File

@@ -0,0 +1,11 @@
import XCTest
@testable import NewsBlur
final class NewsBlurTests: XCTestCase {
func testExample() throws {
// This is an example of a functional test case.
// Use XCTAssert and related functions to verify your tests produce the correct
// results.
XCTAssertEqual(NewsBlur().text, "Hello, World!")
}
}