diff --git a/Account/Sources/Account/URLRequest+Account.swift b/Account/Sources/Account/URLRequest+Account.swift index 0be36b699..b791058c1 100755 --- a/Account/Sources/Account/URLRequest+Account.swift +++ b/Account/Sources/Account/URLRequest+Account.swift @@ -9,6 +9,7 @@ import Foundation import Web import Secrets +import NewsBlur public extension URLRequest { diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeed.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeed.swift index d0ff29d55..645ceada0 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeed.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeed.swift @@ -9,36 +9,42 @@ import Foundation import Parser -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, Sendable { + + public let name: String + public let feedID: Int + public let feedURL: String + public let homePageURL: String? + public let faviconURL: String? } -struct NewsBlurFeedsResponse: Decodable { - let feeds: [NewsBlurFeed] - let folders: [Folder] +public struct NewsBlurFeedsResponse: Decodable, Sendable { - struct Folder: Hashable, Codable { - let name: String - let feedIDs: [Int] + public let feeds: [NewsBlurFeed] + public let folders: [Folder] + + public struct Folder: Hashable, Codable, Sendable { + + public let name: String + public let feedIDs: [Int] } } -struct NewsBlurAddURLResponse: Decodable { - let feed: NewsBlurFeed? +public struct NewsBlurAddURLResponse: Decodable, Sendable { + + public let feed: NewsBlurFeed? } -struct NewsBlurFolderRelationship { - let folderName: String - let feedID: Int +public struct NewsBlurFolderRelationship: Sendable { + + public let folderName: String + public let feedID: Int } extension NewsBlurFeed { + private enum CodingKeys: String, CodingKey { case name = "feed_title" case feedID = "id" @@ -49,13 +55,14 @@ extension NewsBlurFeed { } extension NewsBlurFeedsResponse { + private enum CodingKeys: String, CodingKey { case feeds = "feeds" case folders = "flat_folders" // 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 @@ -88,7 +95,8 @@ extension NewsBlurFeedsResponse { } extension NewsBlurFeedsResponse.Folder { - var asRelationships: [NewsBlurFolderRelationship] { + + public var asRelationships: [NewsBlurFolderRelationship] { return feedIDs.map { NewsBlurFolderRelationship(folderName: name, feedID: $0) } } } diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeedChange.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeedChange.swift index 62ae641d5..be3ad5ccc 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeedChange.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFeedChange.swift @@ -8,7 +8,8 @@ import Foundation -enum NewsBlurFeedChange { +public enum NewsBlurFeedChange: Sendable { + case add(String, String?) case rename(String, String) case delete(String, String?) @@ -16,7 +17,8 @@ enum NewsBlurFeedChange { } extension NewsBlurFeedChange: NewsBlurDataConvertible { - var asData: Data? { + + public var asData: Data? { var postData = URLComponents() postData.queryItems = { switch self { diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFolderChange.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFolderChange.swift index 88fe53b34..9cbd4c39c 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurFolderChange.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurFolderChange.swift @@ -8,14 +8,16 @@ import Foundation -enum NewsBlurFolderChange { +public enum NewsBlurFolderChange: Sendable { + case add(String) case rename(String, String) case delete(String, [String]) } extension NewsBlurFolderChange: NewsBlurDataConvertible { - var asData: Data? { + + public var asData: Data? { var postData = URLComponents() postData.queryItems = { switch self { diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurGenericCodingKeys.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurGenericCodingKeys.swift index 99ecaa89f..7409cbdea 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurGenericCodingKeys.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurGenericCodingKeys.swift @@ -8,18 +8,19 @@ import Foundation -struct NewsBlurGenericCodingKeys: CodingKey { - var stringValue: String +public struct NewsBlurGenericCodingKeys: CodingKey { - init?(stringValue: String) { + public var stringValue: String + + public init?(stringValue: String) { self.stringValue = stringValue } - var intValue: Int? { + public var intValue: Int? { return nil } - init?(intValue: Int) { + public init?(intValue: Int) { return nil } } diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurLoginResponse.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurLoginResponse.swift index 9529ea8e0..7c35376f5 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurLoginResponse.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurLoginResponse.swift @@ -8,17 +8,20 @@ import Foundation -struct NewsBlurLoginResponse: Decodable { - var code: Int - var errors: LoginError? +public struct NewsBlurLoginResponse: Decodable, Sendable { - struct LoginError: Decodable { - var username: [String]? - var others: [String]? + public var code: Int + public var errors: LoginError? + + public struct LoginError: Decodable, Sendable { + + public var username: [String]? + public var others: [String]? } } extension NewsBlurLoginResponse.LoginError { + private enum CodingKeys: String, CodingKey { case username = "username" case others = "__all__" diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStory.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStory.swift index 0c6e8a100..51387f6fe 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStory.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStory.swift @@ -9,23 +9,25 @@ import Foundation import Parser -typealias NewsBlurStory = NewsBlurStoriesResponse.Story +public typealias NewsBlurStory = NewsBlurStoriesResponse.Story -struct NewsBlurStoriesResponse: Decodable { - let stories: [Story] +public struct NewsBlurStoriesResponse: Decodable, Sendable { - 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 let stories: [Story] + + public struct Story: Decodable, Sendable { + + 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) } @@ -36,12 +38,14 @@ struct NewsBlurStoriesResponse: Decodable { } extension NewsBlurStoriesResponse { + private enum CodingKeys: String, CodingKey { case stories = "stories" } } extension NewsBlurStoriesResponse.Story { + private enum CodingKeys: String, CodingKey { case storyID = "story_hash" case feedID = "story_feed_id" diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryHash.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryHash.swift index 960594683..2a32ea40e 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryHash.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryHash.swift @@ -9,27 +9,36 @@ import Foundation import Parser -typealias NewsBlurStoryHash = NewsBlurStoryHashesResponse.StoryHash +public typealias NewsBlurStoryHash = NewsBlurStoryHashesResponse.StoryHash -struct NewsBlurStoryHashesResponse: Decodable { - typealias StoryHashDictionary = [String: [StoryHash]] +public struct NewsBlurStoryHashesResponse: Decodable, Sendable { - var unread: [StoryHash]? - var starred: [StoryHash]? + public typealias StoryHashDictionary = [String: [StoryHash]] - struct StoryHash: Hashable, Codable { - var hash: String - var timestamp: Date + public var unread: [StoryHash]? + public var starred: [StoryHash]? + + public struct StoryHash: Hashable, Codable, Sendable { + + public var hash: String + public var timestamp: Date + + public init(hash: String, timestamp: Date) { + + self.hash = hash + self.timestamp = timestamp + } } } extension NewsBlurStoryHashesResponse { + private enum CodingKeys: String, CodingKey { case unread = "unread_feed_story_hashes" 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 diff --git a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryStatusChange.swift b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryStatusChange.swift index c07fd9220..2ed349a01 100644 --- a/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryStatusChange.swift +++ b/NewsBlur/Sources/NewsBlur/Models/NewsBlurStoryStatusChange.swift @@ -8,12 +8,14 @@ import Foundation -struct NewsBlurStoryStatusChange { - let hashes: [String] +public struct NewsBlurStoryStatusChange: Sendable { + + public let hashes: [String] } extension NewsBlurStoryStatusChange: NewsBlurDataConvertible { - var asData: Data? { + + public var asData: Data? { var postData = URLComponents() postData.queryItems = hashes.map { URLQueryItem(name: "story_hash", value: $0) } diff --git a/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller+Internal.swift b/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller+Internal.swift index ef4f6b050..f54ba8369 100644 --- a/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller+Internal.swift +++ b/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller+Internal.swift @@ -9,16 +9,18 @@ import Foundation import Web -protocol NewsBlurDataConvertible { +public protocol NewsBlurDataConvertible { + var asData: Data? { get } } -enum NewsBlurError: LocalizedError { +public enum NewsBlurError: LocalizedError, Sendable { + case general(message: String) case invalidParameter case unknown - var errorDescription: String? { + public var errorDescription: String? { switch self { case .general(let message): return message diff --git a/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller.swift b/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller.swift index e7e268e53..c2bd96c23 100644 --- a/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller.swift +++ b/NewsBlur/Sources/NewsBlur/NewsBlurAPICaller.swift @@ -11,30 +11,31 @@ import Web import Secrets @MainActor public final class NewsBlurAPICaller: NSObject { - static let SessionIdCookie = "newsblur_sessionid" + + public static let SessionIdCookie = "newsblur_sessionid" let baseURL = URL(string: "https://www.newsblur.com/")! var transport: Transport! var suspended = false - var credentials: Credentials? + public var credentials: Credentials? - init(transport: Transport!) { + public init(transport: Transport!) { super.init() self.transport = transport } /// 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) -> Void) { + public func validateCredentials(completion: @escaping (Result) -> Void) { requestData(endpoint: "api/login", resultType: NewsBlurLoginResponse.self) { result in switch result { case .success((let response, let payload)): @@ -67,11 +68,11 @@ import Secrets } } - func logout(completion: @escaping (Result) -> Void) { + public func logout(completion: @escaping (Result) -> 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([ @@ -111,21 +112,21 @@ import Secrets } } - 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([ @@ -146,7 +147,7 @@ import Secrets } } - 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"))? @@ -164,7 +165,7 @@ import Secrets } } - func markAsUnread(hashes: [String], completion: @escaping (Result) -> Void) { + public func markAsUnread(hashes: [String], completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/mark_story_hash_as_unread", payload: NewsBlurStoryStatusChange(hashes: hashes), @@ -172,7 +173,7 @@ import Secrets ) } - func markAsRead(hashes: [String], completion: @escaping (Result) -> Void) { + public func markAsRead(hashes: [String], completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/mark_story_hashes_as_read", payload: NewsBlurStoryStatusChange(hashes: hashes), @@ -180,7 +181,7 @@ import Secrets ) } - func star(hashes: [String], completion: @escaping (Result) -> Void) { + public func star(hashes: [String], completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/mark_story_hash_as_starred", payload: NewsBlurStoryStatusChange(hashes: hashes), @@ -188,7 +189,7 @@ import Secrets ) } - func unstar(hashes: [String], completion: @escaping (Result) -> Void) { + public func unstar(hashes: [String], completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/mark_story_hash_as_unstarred", payload: NewsBlurStoryStatusChange(hashes: hashes), @@ -196,7 +197,7 @@ import Secrets ) } - func addFolder(named name: String, completion: @escaping (Result) -> Void) { + public func addFolder(named name: String, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/add_folder", payload: NewsBlurFolderChange.add(name), @@ -204,7 +205,7 @@ import Secrets ) } - func renameFolder(with folder: String, to name: String, completion: @escaping (Result) -> Void) { + public func renameFolder(with folder: String, to name: String, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/rename_folder", payload: NewsBlurFolderChange.rename(folder, name), @@ -212,7 +213,7 @@ import Secrets ) } - func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result) -> Void) { + public func removeFolder(named name: String, feedIDs: [String], completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/delete_folder", payload: NewsBlurFolderChange.delete(name, feedIDs), @@ -220,7 +221,7 @@ import Secrets ) } - func addURL(_ url: String, folder: String?, completion: @escaping (Result) -> Void) { + public func addURL(_ url: String, folder: String?, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/add_url", payload: NewsBlurFeedChange.add(url, folder), @@ -235,7 +236,7 @@ import Secrets } } - func renameFeed(feedID: String, newName: String, completion: @escaping (Result) -> Void) { + public func renameFeed(feedID: String, newName: String, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/rename_feed", payload: NewsBlurFeedChange.rename(feedID, newName) @@ -249,7 +250,7 @@ import Secrets } } - func deleteFeed(feedID: String, folder: String? = nil, completion: @escaping (Result) -> Void) { + public func deleteFeed(feedID: String, folder: String? = nil, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/delete_feed", payload: NewsBlurFeedChange.delete(feedID, folder) @@ -263,7 +264,7 @@ import Secrets } } - func moveFeed(feedID: String, from: String?, to: String?, completion: @escaping (Result) -> Void) { + public func moveFeed(feedID: String, from: String?, to: String?, completion: @escaping (Result) -> Void) { sendUpdates( endpoint: "reader/move_feed_to_folder", payload: NewsBlurFeedChange.move(feedID, from, to)