mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Create Feedly module.
This commit is contained in:
@@ -26,6 +26,7 @@ let package = Package(
|
||||
.package(path: "../Feedbin"),
|
||||
.package(path: "../LocalAccount"),
|
||||
.package(path: "../FeedFinder"),
|
||||
.package(path: "../Feedly"),
|
||||
.package(path: "../CommonErrors")
|
||||
],
|
||||
targets: [
|
||||
@@ -47,7 +48,8 @@ let package = Package(
|
||||
"Feedbin",
|
||||
"LocalAccount",
|
||||
"FeedFinder",
|
||||
"CommonErrors"
|
||||
"CommonErrors",
|
||||
"Feedly"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
|
||||
@@ -14,6 +14,7 @@ import os.log
|
||||
import Secrets
|
||||
import Core
|
||||
import CommonErrors
|
||||
import Feedly
|
||||
|
||||
final class FeedlyAccountDelegate: AccountDelegate {
|
||||
|
||||
@@ -761,7 +762,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
@MainActor func accountWillBeDeleted(_ account: Account) {
|
||||
let logout = FeedlyLogoutOperation(account: account, service: caller, log: log)
|
||||
let logout = FeedlyLogoutOperation(service: caller, log: log)
|
||||
// Dispatch on the shared queue because the lifetime of the account delegate is uncertain.
|
||||
MainThreadOperationQueue.shared.add(logout)
|
||||
}
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Web
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
protocol FeedlyAPICallerDelegate: AnyObject {
|
||||
/// Implemented by the `FeedlyAccountDelegate` reauthorize the client with a fresh OAuth token so the client can retry the unauthorized request.
|
||||
@@ -390,7 +391,7 @@ final class FeedlyAPICaller {
|
||||
|
||||
extension FeedlyAPICaller: FeedlyAddFeedToCollectionService {
|
||||
|
||||
func addFeed(with feedId: FeedlyFeedResourceID, title: String? = nil, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
|
||||
@MainActor func addFeed(with feedId: FeedlyFeedResourceID, title: String? = nil, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ()) {
|
||||
guard !isSuspended else {
|
||||
return DispatchQueue.main.async {
|
||||
completion(.failure(TransportError.suspended))
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Feedly
|
||||
|
||||
@MainActor struct FeedlyFeedContainerValidator {
|
||||
var container: Container
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
//
|
||||
// FeedlyEntryIdentifierProviding.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyEntryIdentifierProviding: AnyObject {
|
||||
@MainActor var entryIDs: Set<String> { get }
|
||||
}
|
||||
|
||||
final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
|
||||
private (set) var entryIDs: Set<String>
|
||||
|
||||
init(entryIDs: Set<String> = Set()) {
|
||||
self.entryIDs = entryIDs
|
||||
}
|
||||
|
||||
@MainActor func addEntryIDs(from provider: FeedlyEntryIdentifierProviding) {
|
||||
entryIDs.formUnion(provider.entryIDs)
|
||||
}
|
||||
|
||||
@MainActor func addEntryIDs(in articleIDs: [String]) {
|
||||
entryIDs.formUnion(articleIDs)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import AuthenticationServices
|
||||
import Secrets
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
public protocol OAuthAccountAuthorizationOperationDelegate: AnyObject {
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import Web
|
||||
import Feedly
|
||||
|
||||
/// Models section 6 of the OAuth 2.0 Authorization Framework
|
||||
/// https://tools.ietf.org/html/rfc6749#section-6
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
extension OAuthAuthorizationClient {
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import Web
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
/// Client-specific information for requesting an authorization code grant.
|
||||
/// Accounts are responsible for the scope.
|
||||
|
||||
@@ -11,6 +11,7 @@ import os.log
|
||||
import Web
|
||||
import Secrets
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
@MainActor final class FeedlyAddExistingFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import CommonErrors
|
||||
import Feedly
|
||||
|
||||
protocol FeedlyAddFeedToCollectionService {
|
||||
func addFeed(with feedId: FeedlyFeedResourceID, title: String?, toCollectionWith collectionID: String, completion: @escaping (Result<[FeedlyFeed], Error>) -> ())
|
||||
|
||||
@@ -13,8 +13,9 @@ import Web
|
||||
import Secrets
|
||||
import Core
|
||||
import CommonErrors
|
||||
import Feedly
|
||||
|
||||
class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
final class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlySearchOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
private let operationQueue = MainThreadOperationQueue()
|
||||
private let folder: Folder
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Feedly
|
||||
|
||||
protocol FeedlyCheckpointOperationDelegate: AnyObject {
|
||||
@MainActor func feedlyCheckpointOperationDidReachCheckpoint(_ operation: FeedlyCheckpointOperation)
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
/// Single responsibility is to accurately reflect Collections and their Feeds as Folders and their Feeds.
|
||||
final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
@@ -23,7 +24,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
defer {
|
||||
didFinish()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import Web
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
class FeedlyDownloadArticlesOperation: FeedlyOperation {
|
||||
|
||||
@@ -45,12 +46,11 @@ class FeedlyDownloadArticlesOperation: FeedlyOperation {
|
||||
|
||||
Task { @MainActor in
|
||||
let provider = FeedlyEntryIdentifierProvider(entryIDs: Set(articleIDs))
|
||||
let getEntries = FeedlyGetEntriesOperation(service: getEntriesService, provider: provider, log: log)
|
||||
let getEntries = FeedlyGetEntriesOperation(service: self.getEntriesService, provider: provider, log: self.log)
|
||||
getEntries.delegate = self
|
||||
self.operationQueue.add(getEntries)
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account,
|
||||
parsedItemProvider: getEntries,
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getEntries,
|
||||
log: log)
|
||||
organiseByFeed.delegate = self
|
||||
organiseByFeed.addDependency(getEntries)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import Feedly
|
||||
|
||||
final class FeedlyFetchIDsForMissingArticlesOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import SyncDatabase
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
/// Clone locally the remote starred article state.
|
||||
///
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import Secrets
|
||||
import Database
|
||||
import Feedly
|
||||
|
||||
/// Ensure a status exists for every article id the user might be interested in.
|
||||
///
|
||||
|
||||
@@ -11,6 +11,7 @@ import os.log
|
||||
import Parser
|
||||
import SyncDatabase
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
/// Clone locally the remote unread article state.
|
||||
///
|
||||
@@ -27,12 +28,12 @@ final class FeedlyIngestUnreadArticleIDsOperation: FeedlyOperation {
|
||||
private var remoteEntryIDs = Set<String>()
|
||||
private let log: OSLog
|
||||
|
||||
convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||
public convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||
let resource = FeedlyCategoryResourceID.Global.all(for: userID)
|
||||
self.init(account: account, resource: resource, service: service, database: database, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||
public init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, database: SyncDatabase, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
import os.log
|
||||
import Feedly
|
||||
|
||||
protocol FeedlyFeedsAndFoldersProviding {
|
||||
@MainActor var feedsAndFolders: [([FeedlyFeed], Folder)] { get }
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import os.log
|
||||
import Web
|
||||
import Secrets
|
||||
import Feedly
|
||||
|
||||
final class FeedlyRefreshAccessTokenOperation: FeedlyOperation {
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import SyncDatabase
|
||||
import Web
|
||||
import Secrets
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
/// Compose the operations necessary to get the entire set of articles, feeds and folders with the statuses the user expects between now and a certain date in the past.
|
||||
final class FeedlySyncAllOperation: FeedlyOperation {
|
||||
@@ -83,7 +84,7 @@ final class FeedlySyncAllOperation: FeedlyOperation {
|
||||
|
||||
// Get each page of the article ids which have been update since the last successful fetch start date.
|
||||
// If the date is nil, this operation provides an empty set (everything is new, nothing is updated).
|
||||
let getUpdated = FeedlyGetUpdatedArticleIDsOperation(account: account, userID: feedlyUserID, service: getStreamIDsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
||||
let getUpdated = FeedlyGetUpdatedArticleIDsOperation(userID: feedlyUserID, service: getStreamIDsService, newerThan: lastSuccessfulFetchStartDate, log: log)
|
||||
getUpdated.delegate = self
|
||||
getUpdated.downloadProgress = downloadProgress
|
||||
getUpdated.addDependency(createFeedsOperation)
|
||||
|
||||
@@ -12,6 +12,7 @@ import Parser
|
||||
import Web
|
||||
import Secrets
|
||||
import Core
|
||||
import Feedly
|
||||
|
||||
final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationDelegate, FeedlyGetStreamContentsOperationDelegate, FeedlyCheckpointOperationDelegate {
|
||||
|
||||
@@ -63,15 +64,14 @@ final class FeedlySyncStreamContentsOperation: FeedlyOperation, FeedlyOperationD
|
||||
}
|
||||
|
||||
func pageOperations(for continuation: String?) -> [MainThreadOperation] {
|
||||
let getPage = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
let getPage = FeedlyGetStreamContentsOperation(resource: resource,
|
||||
service: service,
|
||||
continuation: continuation,
|
||||
newerThan: newerThan,
|
||||
log: log)
|
||||
|
||||
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(account: account, parsedItemProvider: getPage, log: log)
|
||||
let organiseByFeed = FeedlyOrganiseParsedItemsByFeedOperation(parsedItemProvider: getPage, log: log)
|
||||
|
||||
let updateAccount = FeedlyUpdateAccountFeedsWithItemsOperation(account: account, organisedItemsProvider: organiseByFeed, log: log)
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import Foundation
|
||||
import Parser
|
||||
import os.log
|
||||
import Database
|
||||
import Feedly
|
||||
|
||||
/// Combine the articles with their feeds for a specific account.
|
||||
final class FeedlyUpdateAccountFeedsWithItemsOperation: FeedlyOperation {
|
||||
|
||||
@@ -11,14 +11,14 @@ import CloudKit
|
||||
|
||||
public extension CKRecord {
|
||||
|
||||
public var externalID: String {
|
||||
var externalID: String {
|
||||
return recordID.externalID
|
||||
}
|
||||
}
|
||||
|
||||
public extension CKRecord.ID {
|
||||
|
||||
public var externalID: String {
|
||||
var externalID: String {
|
||||
return recordName
|
||||
}
|
||||
}
|
||||
|
||||
8
Feedly/.gitignore
vendored
Normal file
8
Feedly/.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
.DS_Store
|
||||
/.build
|
||||
/Packages
|
||||
xcuserdata/
|
||||
DerivedData/
|
||||
.swiftpm/configuration/registries.json
|
||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||
.netrc
|
||||
40
Feedly/Package.swift
Normal file
40
Feedly/Package.swift
Normal file
@@ -0,0 +1,40 @@
|
||||
// swift-tools-version: 5.10
|
||||
|
||||
import PackageDescription
|
||||
|
||||
let package = Package(
|
||||
name: "Feedly",
|
||||
platforms: [.macOS(.v14), .iOS(.v17)],
|
||||
products: [
|
||||
.library(
|
||||
name: "Feedly",
|
||||
targets: ["Feedly"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(path: "../Parser"),
|
||||
.package(path: "../Articles"),
|
||||
.package(path: "../Secrets"),
|
||||
.package(path: "../Core"),
|
||||
.package(path: "../SyncDatabase"),
|
||||
.package(path: "../Web"),
|
||||
],
|
||||
targets: [
|
||||
.target(
|
||||
name: "Feedly",
|
||||
dependencies: [
|
||||
"Parser",
|
||||
"Articles",
|
||||
"Secrets",
|
||||
"Core",
|
||||
"SyncDatabase",
|
||||
"Web"
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableExperimentalFeature("StrictConcurrency")
|
||||
]
|
||||
),
|
||||
.testTarget(
|
||||
name: "FeedlyTests",
|
||||
dependencies: ["Feedly"]),
|
||||
]
|
||||
)
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FeedlyAccountDelegateError: LocalizedError {
|
||||
public enum FeedlyAccountDelegateError: LocalizedError, Sendable {
|
||||
|
||||
case notLoggedIn
|
||||
case unexpectedResourceID(String)
|
||||
case unableToAddFolder(String)
|
||||
@@ -20,7 +21,7 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
||||
case unableToRenameFeed(String, String)
|
||||
case unableToRemoveFeed(String)
|
||||
|
||||
var errorDescription: String? {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .notLoggedIn:
|
||||
return NSLocalizedString("Please add the Feedly account again. If this problem persists, open Keychain Access and delete all feedly.com entries, then try again.", comment: "Feedly – Credentials not found.")
|
||||
@@ -62,7 +63,7 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
var recoverySuggestion: String? {
|
||||
public var recoverySuggestion: String? {
|
||||
switch self {
|
||||
case .notLoggedIn:
|
||||
return nil
|
||||
@@ -8,13 +8,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyResourceProviding {
|
||||
public protocol FeedlyResourceProviding {
|
||||
@MainActor var resource: FeedlyResourceID { get }
|
||||
}
|
||||
|
||||
extension FeedlyFeedResourceID: FeedlyResourceProviding {
|
||||
|
||||
var resource: FeedlyResourceID {
|
||||
public var resource: FeedlyResourceID {
|
||||
return self
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyCategory: Decodable {
|
||||
let label: String
|
||||
let id: String
|
||||
public struct FeedlyCategory: Decodable, Sendable {
|
||||
|
||||
public let label: String
|
||||
public let id: String
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyCollection: Codable {
|
||||
let feeds: [FeedlyFeed]
|
||||
let label: String
|
||||
let id: String
|
||||
public struct FeedlyCollection: Codable, Sendable {
|
||||
|
||||
public let feeds: [FeedlyFeed]
|
||||
public let label: String
|
||||
public let id: String
|
||||
}
|
||||
@@ -8,16 +8,21 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyCollectionParser {
|
||||
let collection: FeedlyCollection
|
||||
public struct FeedlyCollectionParser: Sendable {
|
||||
|
||||
public let collection: FeedlyCollection
|
||||
|
||||
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
|
||||
|
||||
var folderName: String {
|
||||
public var folderName: String {
|
||||
return rightToLeftTextSantizer.sanitize(collection.label) ?? ""
|
||||
}
|
||||
|
||||
var externalID: String {
|
||||
public var externalID: String {
|
||||
return collection.id
|
||||
}
|
||||
|
||||
public init(collection: FeedlyCollection) {
|
||||
self.collection = collection
|
||||
}
|
||||
}
|
||||
@@ -8,58 +8,59 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyEntry: Decodable {
|
||||
public struct FeedlyEntry: Decodable, Sendable {
|
||||
|
||||
/// the unique, immutable ID for this particular article.
|
||||
let id: String
|
||||
|
||||
public let id: String
|
||||
|
||||
/// the article’s title. This string does not contain any HTML markup.
|
||||
let title: String?
|
||||
|
||||
struct Content: Decodable {
|
||||
|
||||
enum Direction: String, Decodable {
|
||||
public let title: String?
|
||||
|
||||
public struct Content: Decodable, Sendable {
|
||||
|
||||
public enum Direction: String, Decodable, Sendable {
|
||||
case leftToRight = "ltr"
|
||||
case rightToLeft = "rtl"
|
||||
}
|
||||
|
||||
let content: String?
|
||||
let direction: Direction?
|
||||
public let content: String?
|
||||
public let direction: Direction?
|
||||
}
|
||||
|
||||
/// This object typically has two values: “content” for the content itself, and “direction” (“ltr” for left-to-right, “rtl” for right-to-left). The content itself contains sanitized HTML markup.
|
||||
let content: Content?
|
||||
|
||||
public let content: Content?
|
||||
|
||||
/// content object the article summary. See the content object above.
|
||||
let summary: Content?
|
||||
|
||||
public let summary: Content?
|
||||
|
||||
/// the author’s name
|
||||
let author: String?
|
||||
|
||||
public let author: String?
|
||||
|
||||
/// the immutable timestamp, in ms, when this article was processed by the feedly Cloud servers.
|
||||
let crawled: Date
|
||||
public let crawled: Date
|
||||
|
||||
/// the timestamp, in ms, when this article was re-processed and updated by the feedly Cloud servers.
|
||||
let recrawled: Date?
|
||||
public let recrawled: Date?
|
||||
|
||||
/// the feed from which this article was crawled. If present, “streamId” will contain the feed id, “title” will contain the feed title, and “htmlUrl” will contain the feed’s website.
|
||||
let origin: FeedlyOrigin?
|
||||
|
||||
public let origin: FeedlyOrigin?
|
||||
|
||||
/// Used to help find the URL to visit an article on a web site.
|
||||
/// See https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
let canonical: [FeedlyLink]?
|
||||
public let canonical: [FeedlyLink]?
|
||||
|
||||
/// a list of alternate links for this article. Each link object contains a media type and a URL. Typically, a single object is present, with a link to the original web page.
|
||||
let alternate: [FeedlyLink]?
|
||||
public let alternate: [FeedlyLink]?
|
||||
|
||||
/// Was this entry read by the user? If an Authorization header is not provided, this will always return false. If an Authorization header is provided, it will reflect if the user has read this entry or not.
|
||||
let unread: Bool
|
||||
public let unread: Bool
|
||||
|
||||
/// a list of tag objects (“id” and “label”) that the user added to this entry. This value is only returned if an Authorization header is provided, and at least one tag has been added. If the entry has been explicitly marked as read (not the feed itself), the “global.read” tag will be present.
|
||||
let tags: [FeedlyTag]?
|
||||
public let tags: [FeedlyTag]?
|
||||
|
||||
/// a list of category objects (“id” and “label”) that the user associated with the feed of this entry. This value is only returned if an Authorization header is provided.
|
||||
let categories: [FeedlyCategory]?
|
||||
public let categories: [FeedlyCategory]?
|
||||
|
||||
/// A list of media links (videos, images, sound etc) provided by the feed. Some entries do not have a summary or content, only a collection of media links.
|
||||
let enclosure: [FeedlyLink]?
|
||||
public let enclosure: [FeedlyLink]?
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
//
|
||||
// FeedlyEntryIdentifierProviding.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Kiel Gillard on 9/1/20.
|
||||
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public protocol FeedlyEntryIdentifierProviding: AnyObject {
|
||||
@MainActor var entryIDs: Set<String> { get }
|
||||
}
|
||||
|
||||
public final class FeedlyEntryIdentifierProvider: FeedlyEntryIdentifierProviding {
|
||||
|
||||
private (set) public var entryIDs: Set<String>
|
||||
|
||||
public init(entryIDs: Set<String> = Set()) {
|
||||
self.entryIDs = entryIDs
|
||||
}
|
||||
|
||||
@MainActor public func addEntryIDs(from provider: FeedlyEntryIdentifierProviding) {
|
||||
entryIDs.formUnion(provider.entryIDs)
|
||||
}
|
||||
|
||||
@MainActor public func addEntryIDs(in articleIDs: [String]) {
|
||||
entryIDs.formUnion(articleIDs)
|
||||
}
|
||||
}
|
||||
@@ -10,17 +10,18 @@ import Foundation
|
||||
import Articles
|
||||
import Parser
|
||||
|
||||
struct FeedlyEntryParser {
|
||||
let entry: FeedlyEntry
|
||||
|
||||
public struct FeedlyEntryParser: Sendable {
|
||||
|
||||
public let entry: FeedlyEntry
|
||||
|
||||
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
|
||||
|
||||
var id: String {
|
||||
public var id: String {
|
||||
return entry.id
|
||||
}
|
||||
|
||||
/// When ingesting articles, the feedURL must match a feed's `feedID` for the article to be reachable between it and its matching feed. It reminds me of a foreign key.
|
||||
var feedUrl: String? {
|
||||
public var feedUrl: String? {
|
||||
guard let id = entry.origin?.streamID else {
|
||||
// At this point, check Feedly's API isn't glitching or the response has not changed structure.
|
||||
assertionFailure("Entries need to be traceable to a feed or this entry will be dropped.")
|
||||
@@ -31,7 +32,7 @@ struct FeedlyEntryParser {
|
||||
|
||||
/// Convoluted external URL logic "documented" here:
|
||||
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
var externalUrl: String? {
|
||||
public var externalUrl: String? {
|
||||
let multidimensionalArrayOfLinks = [entry.canonical, entry.alternate]
|
||||
let withExistingValues = multidimensionalArrayOfLinks.compactMap { $0 }
|
||||
let flattened = withExistingValues.flatMap { $0 }
|
||||
@@ -39,32 +40,32 @@ struct FeedlyEntryParser {
|
||||
return webPageLinks.first?.href
|
||||
}
|
||||
|
||||
var title: String? {
|
||||
public var title: String? {
|
||||
return rightToLeftTextSantizer.sanitize(entry.title)
|
||||
}
|
||||
|
||||
var contentHMTL: String? {
|
||||
public var contentHMTL: String? {
|
||||
return entry.content?.content ?? entry.summary?.content
|
||||
}
|
||||
|
||||
var contentText: String? {
|
||||
public var contentText: String? {
|
||||
// We could strip HTML from contentHTML?
|
||||
return nil
|
||||
}
|
||||
|
||||
var summary: String? {
|
||||
public var summary: String? {
|
||||
return rightToLeftTextSantizer.sanitize(entry.summary?.content)
|
||||
}
|
||||
|
||||
var datePublished: Date {
|
||||
public var datePublished: Date {
|
||||
return entry.crawled
|
||||
}
|
||||
|
||||
var dateModified: Date? {
|
||||
public var dateModified: Date? {
|
||||
return entry.recrawled
|
||||
}
|
||||
|
||||
var authors: Set<ParsedAuthor>? {
|
||||
public var authors: Set<ParsedAuthor>? {
|
||||
guard let name = entry.author else {
|
||||
return nil
|
||||
}
|
||||
@@ -72,14 +73,14 @@ struct FeedlyEntryParser {
|
||||
}
|
||||
|
||||
/// While there is not yet a tagging interface, articles can still be searched for by tags.
|
||||
var tags: Set<String>? {
|
||||
public var tags: Set<String>? {
|
||||
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
return Set(labels)
|
||||
}
|
||||
|
||||
var attachments: Set<ParsedAttachment>? {
|
||||
public var attachments: Set<ParsedAttachment>? {
|
||||
guard let enclosure = entry.enclosure, !enclosure.isEmpty else {
|
||||
return nil
|
||||
}
|
||||
@@ -87,7 +88,7 @@ struct FeedlyEntryParser {
|
||||
return attachments.isEmpty ? nil : Set(attachments)
|
||||
}
|
||||
|
||||
var parsedItemRepresentation: ParsedItem? {
|
||||
public var parsedItemRepresentation: ParsedItem? {
|
||||
guard let feedUrl = feedUrl else {
|
||||
return nil
|
||||
}
|
||||
@@ -8,9 +8,10 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyFeed: Codable {
|
||||
let id: String
|
||||
let title: String?
|
||||
let updated: Date?
|
||||
let website: String?
|
||||
public struct FeedlyFeed: Codable, Sendable {
|
||||
|
||||
public let id: String
|
||||
public let title: String?
|
||||
public let updated: Date?
|
||||
public let website: String?
|
||||
}
|
||||
@@ -8,25 +8,31 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyFeedParser {
|
||||
let feed: FeedlyFeed
|
||||
public struct FeedlyFeedParser: Sendable {
|
||||
|
||||
public let feed: FeedlyFeed
|
||||
|
||||
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
|
||||
|
||||
var title: String? {
|
||||
public var title: String? {
|
||||
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
|
||||
}
|
||||
|
||||
var feedID: String {
|
||||
public var feedID: String {
|
||||
return feed.id
|
||||
}
|
||||
|
||||
var url: String {
|
||||
public var url: String {
|
||||
let resource = FeedlyFeedResourceID(id: feed.id)
|
||||
return resource.url
|
||||
}
|
||||
|
||||
var homePageURL: String? {
|
||||
public var homePageURL: String? {
|
||||
return feed.website
|
||||
}
|
||||
|
||||
public init(feed: FeedlyFeed) {
|
||||
|
||||
self.feed = feed
|
||||
}
|
||||
}
|
||||
@@ -8,12 +8,13 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyFeedsSearchResponse: Decodable {
|
||||
|
||||
struct Feed: Decodable {
|
||||
let title: String
|
||||
let feedId: String
|
||||
public struct FeedlyFeedsSearchResponse: Decodable, Sendable {
|
||||
|
||||
public struct Feed: Decodable, Sendable {
|
||||
|
||||
public let title: String
|
||||
public let feedId: String
|
||||
}
|
||||
|
||||
let results: [Feed]
|
||||
public let results: [Feed]
|
||||
}
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyLink: Decodable {
|
||||
let href: String
|
||||
|
||||
public struct FeedlyLink: Decodable, Sendable {
|
||||
|
||||
public let href: String
|
||||
|
||||
/// The mime type of the resource located by `href`.
|
||||
/// When `nil`, it's probably a web page?
|
||||
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
|
||||
let type: String?
|
||||
public let type: String?
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyOrigin: Decodable {
|
||||
let title: String?
|
||||
let streamID: String?
|
||||
let htmlUrl: String?
|
||||
public struct FeedlyOrigin: Decodable, Sendable {
|
||||
|
||||
public let title: String?
|
||||
public let streamID: String?
|
||||
public let htmlUrl: String?
|
||||
}
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyRTLTextSanitizer {
|
||||
public struct FeedlyRTLTextSanitizer: Sendable {
|
||||
|
||||
private let rightToLeftPrefix = "<div style=\"direction:rtl;text-align:right\">"
|
||||
private let rightToLeftSuffix = "</div>"
|
||||
|
||||
func sanitize(_ sourceText: String?) -> String? {
|
||||
public func sanitize(_ sourceText: String?) -> String? {
|
||||
guard let source = sourceText, !source.isEmpty else {
|
||||
return sourceText
|
||||
}
|
||||
@@ -9,16 +9,17 @@
|
||||
import Foundation
|
||||
|
||||
/// The kinds of Resource IDs is documented here: https://developer.feedly.com/cloud/
|
||||
protocol FeedlyResourceID {
|
||||
public protocol FeedlyResourceID {
|
||||
|
||||
/// The resource ID from Feedly.
|
||||
@MainActor var id: String { get }
|
||||
}
|
||||
|
||||
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
|
||||
struct FeedlyFeedResourceID: FeedlyResourceID {
|
||||
let id: String
|
||||
|
||||
public struct FeedlyFeedResourceID: FeedlyResourceID, Sendable {
|
||||
|
||||
public let id: String
|
||||
|
||||
/// The location of the kind of resource a concrete type represents.
|
||||
/// If the concrete type cannot strip the resource type from the ID, it should just return the ID
|
||||
/// since the ID is a legitimate URL.
|
||||
@@ -26,7 +27,7 @@ struct FeedlyFeedResourceID: FeedlyResourceID {
|
||||
/// It is not documented as such and could potentially change.
|
||||
/// Feedly does not include the source feed URL as a separate field.
|
||||
/// See https://developer.feedly.com/v3/feeds/#get-the-metadata-about-a-specific-feed
|
||||
var url: String {
|
||||
public var url: String {
|
||||
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
|
||||
var mutant = id
|
||||
mutant.removeSubrange(range)
|
||||
@@ -36,34 +37,40 @@ struct FeedlyFeedResourceID: FeedlyResourceID {
|
||||
// It seems values like "something/https://my.blog/posts.xml" is a legit URL.
|
||||
return id
|
||||
}
|
||||
|
||||
public init(id: String) {
|
||||
self.id = id
|
||||
}
|
||||
}
|
||||
|
||||
extension FeedlyFeedResourceID {
|
||||
|
||||
init(url: String) {
|
||||
self.id = "feed/\(url)"
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedlyCategoryResourceID: FeedlyResourceID {
|
||||
let id: String
|
||||
|
||||
enum Global {
|
||||
|
||||
static func uncategorized(for userID: String) -> FeedlyCategoryResourceID {
|
||||
public struct FeedlyCategoryResourceID: FeedlyResourceID, Sendable {
|
||||
|
||||
public let id: String
|
||||
|
||||
public enum Global {
|
||||
|
||||
public static func uncategorized(for userID: String) -> FeedlyCategoryResourceID {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userID)/category/global.uncategorized"
|
||||
return FeedlyCategoryResourceID(id: id)
|
||||
}
|
||||
|
||||
/// All articles from all the feeds the user subscribes to.
|
||||
static func all(for userID: String) -> FeedlyCategoryResourceID {
|
||||
public static func all(for userID: String) -> FeedlyCategoryResourceID {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userID)/category/global.all"
|
||||
return FeedlyCategoryResourceID(id: id)
|
||||
}
|
||||
|
||||
/// All articles from all the feeds the user loves most.
|
||||
static func mustRead(for userID: String) -> FeedlyCategoryResourceID {
|
||||
public static func mustRead(for userID: String) -> FeedlyCategoryResourceID {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userID)/category/global.must"
|
||||
return FeedlyCategoryResourceID(id: id)
|
||||
@@ -71,12 +78,13 @@ struct FeedlyCategoryResourceID: FeedlyResourceID {
|
||||
}
|
||||
}
|
||||
|
||||
struct FeedlyTagResourceID: FeedlyResourceID {
|
||||
let id: String
|
||||
|
||||
enum Global {
|
||||
|
||||
static func saved(for userID: String) -> FeedlyTagResourceID {
|
||||
public struct FeedlyTagResourceID: FeedlyResourceID, Sendable {
|
||||
|
||||
public let id: String
|
||||
|
||||
public enum Global {
|
||||
|
||||
public static func saved(for userID: String) -> FeedlyTagResourceID {
|
||||
// https://developer.feedly.com/cloud/#global-resource-ids
|
||||
let id = "user/\(userID)/tag/global.saved"
|
||||
return FeedlyTagResourceID(id: id)
|
||||
@@ -8,19 +8,20 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyStream: Decodable {
|
||||
let id: String
|
||||
|
||||
public struct FeedlyStream: Decodable, Sendable {
|
||||
|
||||
public let id: String
|
||||
|
||||
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
|
||||
let updated: Date?
|
||||
|
||||
public let updated: Date?
|
||||
|
||||
/// the continuation id to pass to the next stream call, for pagination.
|
||||
/// This id guarantees that no entry will be duplicated in a stream (meaning, there is no need to de-duplicate entries returned by this call).
|
||||
/// If this value is not returned, it means the end of the stream has been reached.
|
||||
let continuation: String?
|
||||
let items: [FeedlyEntry]
|
||||
|
||||
var isStreamEnd: Bool {
|
||||
public let continuation: String?
|
||||
public let items: [FeedlyEntry]
|
||||
|
||||
public var isStreamEnd: Bool {
|
||||
return continuation == nil
|
||||
}
|
||||
}
|
||||
@@ -8,11 +8,12 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyStreamIDs: Decodable {
|
||||
let continuation: String?
|
||||
let ids: [String]
|
||||
|
||||
var isStreamEnd: Bool {
|
||||
public struct FeedlyStreamIDs: Decodable, Sendable {
|
||||
|
||||
public let continuation: String?
|
||||
public let ids: [String]
|
||||
|
||||
public var isStreamEnd: Bool {
|
||||
return continuation == nil
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,8 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedlyTag: Decodable {
|
||||
let id: String
|
||||
let label: String?
|
||||
public struct FeedlyTag: Decodable, Sendable {
|
||||
|
||||
public let id: String
|
||||
public let label: String?
|
||||
}
|
||||
@@ -9,25 +9,25 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyCollectionProviding: AnyObject {
|
||||
|
||||
public protocol FeedlyCollectionProviding: AnyObject {
|
||||
|
||||
@MainActor var collections: [FeedlyCollection] { get }
|
||||
}
|
||||
|
||||
/// Get Collections from Feedly.
|
||||
final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
|
||||
public final class FeedlyGetCollectionsOperation: FeedlyOperation, FeedlyCollectionProviding {
|
||||
|
||||
let service: FeedlyGetCollectionsService
|
||||
let log: OSLog
|
||||
|
||||
private(set) var collections = [FeedlyCollection]()
|
||||
private(set) public var collections = [FeedlyCollection]()
|
||||
|
||||
init(service: FeedlyGetCollectionsService, log: OSLog) {
|
||||
public init(service: FeedlyGetCollectionsService, log: OSLog) {
|
||||
self.service = service
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
os_log(.debug, log: log, "Requesting collections.")
|
||||
|
||||
service.getCollections { result in
|
||||
@@ -11,23 +11,23 @@ import os.log
|
||||
import Parser
|
||||
|
||||
/// Get full entries for the entry identifiers.
|
||||
final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
public final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
|
||||
let service: FeedlyGetEntriesService
|
||||
let provider: FeedlyEntryIdentifierProviding
|
||||
let log: OSLog
|
||||
|
||||
init(service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
|
||||
public init(service: FeedlyGetEntriesService, provider: FeedlyEntryIdentifierProviding, log: OSLog) {
|
||||
self.service = service
|
||||
self.provider = provider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
private (set) var entries = [FeedlyEntry]()
|
||||
|
||||
private (set) public var entries = [FeedlyEntry]()
|
||||
|
||||
private var storedParsedEntries: Set<ParsedItem>?
|
||||
|
||||
var parsedEntries: Set<ParsedItem> {
|
||||
public var parsedEntries: Set<ParsedItem> {
|
||||
if let entries = storedParsedEntries {
|
||||
return entries
|
||||
}
|
||||
@@ -49,11 +49,11 @@ final class FeedlyGetEntriesOperation: FeedlyOperation, FeedlyEntryProviding, Fe
|
||||
return parsed
|
||||
}
|
||||
|
||||
var parsedItemProviderName: String {
|
||||
public var parsedItemProviderName: String {
|
||||
return name ?? String(describing: Self.self)
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
service.getEntries(for: provider.entryIDs) { result in
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
@@ -10,33 +10,33 @@ import Foundation
|
||||
import Parser
|
||||
import os.log
|
||||
|
||||
protocol FeedlyEntryProviding {
|
||||
public protocol FeedlyEntryProviding {
|
||||
@MainActor var entries: [FeedlyEntry] { get }
|
||||
}
|
||||
|
||||
protocol FeedlyParsedItemProviding {
|
||||
public protocol FeedlyParsedItemProviding {
|
||||
@MainActor var parsedItemProviderName: String { get }
|
||||
@MainActor var parsedEntries: Set<ParsedItem> { get }
|
||||
}
|
||||
|
||||
protocol FeedlyGetStreamContentsOperationDelegate: AnyObject {
|
||||
public protocol FeedlyGetStreamContentsOperationDelegate: AnyObject {
|
||||
func feedlyGetStreamContentsOperation(_ operation: FeedlyGetStreamContentsOperation, didGetContentsOf stream: FeedlyStream)
|
||||
}
|
||||
|
||||
/// Get the stream content of a Collection from Feedly.
|
||||
final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
|
||||
public final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProviding, FeedlyParsedItemProviding {
|
||||
|
||||
@MainActor struct ResourceProvider: FeedlyResourceProviding {
|
||||
var resource: FeedlyResourceID
|
||||
}
|
||||
|
||||
let resourceProvider: FeedlyResourceProviding
|
||||
|
||||
var parsedItemProviderName: String {
|
||||
public var parsedItemProviderName: String {
|
||||
return resourceProvider.resource.id
|
||||
}
|
||||
|
||||
var entries: [FeedlyEntry] {
|
||||
public var entries: [FeedlyEntry] {
|
||||
guard let entries = stream?.items else {
|
||||
// assert(isFinished, "This should only be called when the operation finishes without error.")
|
||||
assertionFailure("Has this operation been addeded as a dependency on the caller?")
|
||||
@@ -45,7 +45,7 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
|
||||
return entries
|
||||
}
|
||||
|
||||
var parsedEntries: Set<ParsedItem> {
|
||||
public var parsedEntries: Set<ParsedItem> {
|
||||
if let entries = storedParsedEntries {
|
||||
return entries
|
||||
}
|
||||
@@ -74,17 +74,16 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
|
||||
|
||||
private var storedParsedEntries: Set<ParsedItem>?
|
||||
|
||||
let account: Account
|
||||
let service: FeedlyGetStreamContentsService
|
||||
let unreadOnly: Bool?
|
||||
let newerThan: Date?
|
||||
let continuation: String?
|
||||
let log: OSLog
|
||||
|
||||
weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate?
|
||||
public weak var streamDelegate: FeedlyGetStreamContentsOperationDelegate?
|
||||
|
||||
public init(resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
|
||||
|
||||
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamContentsService, continuation: String? = nil, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
|
||||
self.account = account
|
||||
self.resourceProvider = ResourceProvider(resource: resource)
|
||||
self.service = service
|
||||
self.continuation = continuation
|
||||
@@ -93,11 +92,12 @@ final class FeedlyGetStreamContentsOperation: FeedlyOperation, FeedlyEntryProvid
|
||||
self.log = log
|
||||
}
|
||||
|
||||
convenience init(account: Account, resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
|
||||
self.init(account: account, resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log)
|
||||
convenience init(resourceProvider: FeedlyResourceProviding, service: FeedlyGetStreamContentsService, newerThan: Date?, unreadOnly: Bool? = nil, log: OSLog) {
|
||||
|
||||
self.init(resource: resourceProvider.resource, service: service, newerThan: newerThan, unreadOnly: unreadOnly, log: log)
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
service.getStreamContents(for: resourceProvider.resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
|
||||
switch result {
|
||||
case .success(let stream):
|
||||
@@ -9,14 +9,14 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyGetStreamIDsOperationDelegate: AnyObject {
|
||||
public protocol FeedlyGetStreamIDsOperationDelegate: AnyObject {
|
||||
func feedlyGetStreamIDsOperation(_ operation: FeedlyGetStreamIDsOperation, didGet streamIDs: FeedlyStreamIDs)
|
||||
}
|
||||
|
||||
/// Single responsibility is to get the stream ids from Feedly.
|
||||
final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
public final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
|
||||
var entryIDs: Set<String> {
|
||||
public var entryIDs: Set<String> {
|
||||
guard let ids = streamIDs?.ids else {
|
||||
assertionFailure("Has this operation been addeded as a dependency on the caller?")
|
||||
return []
|
||||
@@ -26,7 +26,6 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
|
||||
|
||||
private(set) var streamIDs: FeedlyStreamIDs?
|
||||
|
||||
let account: Account
|
||||
let service: FeedlyGetStreamIDsService
|
||||
let continuation: String?
|
||||
let resource: FeedlyResourceID
|
||||
@@ -34,8 +33,8 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
|
||||
let newerThan: Date?
|
||||
let log: OSLog
|
||||
|
||||
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, continuation: String? = nil, newerThan: Date? = nil, unreadOnly: Bool?, log: OSLog) {
|
||||
self.account = account
|
||||
init(resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, continuation: String? = nil, newerThan: Date? = nil, unreadOnly: Bool?, log: OSLog) {
|
||||
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.continuation = continuation
|
||||
@@ -46,7 +45,7 @@ final class FeedlyGetStreamIDsOperation: FeedlyOperation, FeedlyEntryIdentifierP
|
||||
|
||||
weak var streamIDsDelegate: FeedlyGetStreamIDsOperationDelegate?
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
service.getStreamIDs(for: resource, continuation: continuation, newerThan: newerThan, unreadOnly: unreadOnly) { result in
|
||||
switch result {
|
||||
case .success(let stream):
|
||||
@@ -14,34 +14,33 @@ import Secrets
|
||||
///
|
||||
/// Typically, it pages through the article ids of the global.all stream.
|
||||
/// When all the article ids are collected, it is the responsibility of another operation to download them when appropriate.
|
||||
class FeedlyGetUpdatedArticleIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
public final class FeedlyGetUpdatedArticleIDsOperation: FeedlyOperation, FeedlyEntryIdentifierProviding {
|
||||
|
||||
private let account: Account
|
||||
private let resource: FeedlyResourceID
|
||||
private let service: FeedlyGetStreamIDsService
|
||||
private let newerThan: Date?
|
||||
private let log: OSLog
|
||||
|
||||
init(account: Account, resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
|
||||
self.account = account
|
||||
public init(resource: FeedlyResourceID, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
|
||||
|
||||
self.resource = resource
|
||||
self.service = service
|
||||
self.newerThan = newerThan
|
||||
self.log = log
|
||||
}
|
||||
|
||||
convenience init(account: Account, userID: String, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
|
||||
public convenience init(userID: String, service: FeedlyGetStreamIDsService, newerThan: Date?, log: OSLog) {
|
||||
let all = FeedlyCategoryResourceID.Global.all(for: userID)
|
||||
self.init(account: account, resource: all, service: service, newerThan: newerThan, log: log)
|
||||
self.init(resource: all, service: service, newerThan: newerThan, log: log)
|
||||
}
|
||||
|
||||
var entryIDs: Set<String> {
|
||||
public var entryIDs: Set<String> {
|
||||
return storedUpdatedArticleIDs
|
||||
}
|
||||
|
||||
private var storedUpdatedArticleIDs = Set<String>()
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
getStreamIDs(nil)
|
||||
}
|
||||
|
||||
@@ -9,24 +9,22 @@
|
||||
import Foundation
|
||||
import os.log
|
||||
|
||||
protocol FeedlyLogoutService {
|
||||
public protocol FeedlyLogoutService {
|
||||
func logout(completion: @escaping (Result<Void, Error>) -> ())
|
||||
}
|
||||
|
||||
final class FeedlyLogoutOperation: FeedlyOperation {
|
||||
public final class FeedlyLogoutOperation: FeedlyOperation {
|
||||
|
||||
let service: FeedlyLogoutService
|
||||
let account: Account
|
||||
let log: OSLog
|
||||
|
||||
init(account: Account, service: FeedlyLogoutService, log: OSLog) {
|
||||
public init(service: FeedlyLogoutService, log: OSLog) {
|
||||
self.service = service
|
||||
self.account = account
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
os_log("Requesting logout of %{public}@ account.", "\(account.type)")
|
||||
public override func run() {
|
||||
os_log("Requesting logout of Feedly account.")
|
||||
service.logout(completion: didCompleteLogout(_:))
|
||||
}
|
||||
|
||||
@@ -34,10 +32,11 @@ final class FeedlyLogoutOperation: FeedlyOperation {
|
||||
assert(Thread.isMainThread)
|
||||
switch result {
|
||||
case .success:
|
||||
os_log("Logged out of %{public}@ account.", "\(account.type)")
|
||||
os_log("Logged out of Feedly account.")
|
||||
do {
|
||||
try account.removeCredentials(type: .oauthAccessToken)
|
||||
try account.removeCredentials(type: .oauthRefreshToken)
|
||||
// TODO: fix removing credentials
|
||||
// try account.removeCredentials(type: .oauthAccessToken)
|
||||
// try account.removeCredentials(type: .oauthRefreshToken)
|
||||
} catch {
|
||||
// oh well, we tried our best.
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Web
|
||||
import Core
|
||||
|
||||
protocol FeedlyOperationDelegate: AnyObject {
|
||||
public protocol FeedlyOperationDelegate: AnyObject {
|
||||
@MainActor func feedlyOperation(_ operation: FeedlyOperation, didFailWith error: Error)
|
||||
}
|
||||
|
||||
@@ -18,10 +18,10 @@ protocol FeedlyOperationDelegate: AnyObject {
|
||||
///
|
||||
/// Normally we don’t do inheritance — but in this case
|
||||
/// it’s the best option.
|
||||
@MainActor class FeedlyOperation: MainThreadOperation {
|
||||
@MainActor open class FeedlyOperation: MainThreadOperation {
|
||||
|
||||
weak var delegate: FeedlyOperationDelegate?
|
||||
var downloadProgress: DownloadProgress? {
|
||||
public weak var delegate: FeedlyOperationDelegate?
|
||||
public var downloadProgress: DownloadProgress? {
|
||||
didSet {
|
||||
oldValue?.completeTask()
|
||||
downloadProgress?.addToNumberOfTasksAndRemaining(1)
|
||||
@@ -29,34 +29,36 @@ protocol FeedlyOperationDelegate: AnyObject {
|
||||
}
|
||||
|
||||
// MainThreadOperation
|
||||
var isCanceled = false {
|
||||
public var isCanceled = false {
|
||||
didSet {
|
||||
if isCanceled {
|
||||
didCancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
var id: Int?
|
||||
weak var operationDelegate: MainThreadOperationDelegate?
|
||||
var name: String?
|
||||
var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
public var id: Int?
|
||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||
public var name: String?
|
||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
func run() {
|
||||
public init() {}
|
||||
|
||||
open func run() {
|
||||
}
|
||||
|
||||
func didFinish() {
|
||||
open func didFinish() {
|
||||
if !isCanceled {
|
||||
operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
downloadProgress?.completeTask()
|
||||
}
|
||||
|
||||
func didFinish(with error: Error) {
|
||||
open func didFinish(with error: Error) {
|
||||
delegate?.feedlyOperation(self, didFailWith: error)
|
||||
didFinish()
|
||||
}
|
||||
|
||||
func didCancel() {
|
||||
open func didCancel() {
|
||||
didFinish()
|
||||
}
|
||||
}
|
||||
@@ -10,36 +10,36 @@ import Foundation
|
||||
import Parser
|
||||
import os.log
|
||||
|
||||
protocol FeedlyParsedItemsByFeedProviding {
|
||||
var parsedItemsByFeedProviderName: String { get }
|
||||
var parsedItemsKeyedByFeedID: [String: Set<ParsedItem>] { get }
|
||||
public protocol FeedlyParsedItemsByFeedProviding {
|
||||
|
||||
@MainActor var parsedItemsByFeedProviderName: String { get }
|
||||
@MainActor var parsedItemsKeyedByFeedID: [String: Set<ParsedItem>] { get }
|
||||
}
|
||||
|
||||
/// Group articles by their feeds.
|
||||
final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding {
|
||||
public final class FeedlyOrganiseParsedItemsByFeedOperation: FeedlyOperation, FeedlyParsedItemsByFeedProviding {
|
||||
|
||||
private let account: Account
|
||||
private let parsedItemProvider: FeedlyParsedItemProviding
|
||||
private let log: OSLog
|
||||
|
||||
var parsedItemsByFeedProviderName: String {
|
||||
public var parsedItemsByFeedProviderName: String {
|
||||
return name ?? String(describing: Self.self)
|
||||
}
|
||||
|
||||
var parsedItemsKeyedByFeedID: [String : Set<ParsedItem>] {
|
||||
public var parsedItemsKeyedByFeedID: [String : Set<ParsedItem>] {
|
||||
precondition(Thread.isMainThread) // Needs to be on main thread because Feed is a main-thread-only model type.
|
||||
return itemsKeyedByFeedID
|
||||
}
|
||||
|
||||
private var itemsKeyedByFeedID = [String: Set<ParsedItem>]()
|
||||
|
||||
init(account: Account, parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
|
||||
self.account = account
|
||||
public init(parsedItemProvider: FeedlyParsedItemProviding, log: OSLog) {
|
||||
|
||||
self.parsedItemProvider = parsedItemProvider
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
defer {
|
||||
didFinish()
|
||||
}
|
||||
@@ -15,19 +15,18 @@ protocol FeedlyRequestStreamsOperationDelegate: AnyObject {
|
||||
|
||||
/// Create one stream request operation for one Feedly collection.
|
||||
/// This is the start of the process of refreshing the entire contents of a Folder.
|
||||
final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
||||
public final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
||||
|
||||
weak var queueDelegate: FeedlyRequestStreamsOperationDelegate?
|
||||
|
||||
let collectionsProvider: FeedlyCollectionProviding
|
||||
let service: FeedlyGetStreamContentsService
|
||||
let account: Account
|
||||
let log: OSLog
|
||||
let newerThan: Date?
|
||||
let unreadOnly: Bool?
|
||||
|
||||
init(account: Account, collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, service: FeedlyGetStreamContentsService, log: OSLog) {
|
||||
self.account = account
|
||||
init(collectionsProvider: FeedlyCollectionProviding, newerThan: Date?, unreadOnly: Bool?, service: FeedlyGetStreamContentsService, log: OSLog) {
|
||||
|
||||
self.service = service
|
||||
self.collectionsProvider = collectionsProvider
|
||||
self.newerThan = newerThan
|
||||
@@ -35,7 +34,7 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
defer {
|
||||
didFinish()
|
||||
}
|
||||
@@ -46,12 +45,11 @@ final class FeedlyRequestStreamsOperation: FeedlyOperation {
|
||||
|
||||
for collection in collectionsProvider.collections {
|
||||
let resource = FeedlyCategoryResourceID(id: collection.id)
|
||||
let operation = FeedlyGetStreamContentsOperation(account: account,
|
||||
resource: resource,
|
||||
service: service,
|
||||
newerThan: newerThan,
|
||||
unreadOnly: unreadOnly,
|
||||
log: log)
|
||||
let operation = FeedlyGetStreamContentsOperation(resource: resource,
|
||||
service: service,
|
||||
newerThan: newerThan,
|
||||
unreadOnly: unreadOnly,
|
||||
log: log)
|
||||
queueDelegate?.feedlyRequestStreamsOperation(self, enqueue: operation)
|
||||
}
|
||||
|
||||
@@ -8,30 +8,30 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlySearchService: AnyObject {
|
||||
public protocol FeedlySearchService: AnyObject {
|
||||
func getFeeds(for query: String, count: Int, locale: String, completion: @escaping (Result<FeedlyFeedsSearchResponse, Error>) -> ())
|
||||
}
|
||||
|
||||
protocol FeedlySearchOperationDelegate: AnyObject {
|
||||
public protocol FeedlySearchOperationDelegate: AnyObject {
|
||||
@MainActor func feedlySearchOperation(_ operation: FeedlySearchOperation, didGet response: FeedlyFeedsSearchResponse)
|
||||
}
|
||||
|
||||
/// Find one and only one feed for a given query (usually, a URL).
|
||||
/// What happens when a feed is found for the URL is delegated to the `searchDelegate`.
|
||||
class FeedlySearchOperation: FeedlyOperation {
|
||||
public class FeedlySearchOperation: FeedlyOperation {
|
||||
|
||||
let query: String
|
||||
let locale: Locale
|
||||
let searchService: FeedlySearchService
|
||||
weak var searchDelegate: FeedlySearchOperationDelegate?
|
||||
public weak var searchDelegate: FeedlySearchOperationDelegate?
|
||||
|
||||
init(query: String, locale: Locale = .current, service: FeedlySearchService) {
|
||||
public init(query: String, locale: Locale = .current, service: FeedlySearchService) {
|
||||
self.query = query
|
||||
self.locale = locale
|
||||
self.searchService = service
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
searchService.getFeeds(for: query, count: 1, locale: locale.identifier) { result in
|
||||
switch result {
|
||||
case .success(let response):
|
||||
@@ -11,21 +11,20 @@ import Articles
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
|
||||
|
||||
/// Take changes to statuses of articles locally and apply them to the corresponding the articles remotely.
|
||||
final class FeedlySendArticleStatusesOperation: FeedlyOperation {
|
||||
public final class FeedlySendArticleStatusesOperation: FeedlyOperation {
|
||||
|
||||
private let database: SyncDatabase
|
||||
private let log: OSLog
|
||||
private let service: FeedlyMarkArticlesService
|
||||
|
||||
init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) {
|
||||
public init(database: SyncDatabase, service: FeedlyMarkArticlesService, log: OSLog) {
|
||||
self.database = database
|
||||
self.service = service
|
||||
self.log = log
|
||||
}
|
||||
|
||||
override func run() {
|
||||
public override func run() {
|
||||
os_log(.debug, log: log, "Sending article statuses...")
|
||||
|
||||
Task { @MainActor in
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetCollectionsService: AnyObject {
|
||||
public protocol FeedlyGetCollectionsService: AnyObject {
|
||||
func getCollections(completion: @escaping (Result<[FeedlyCollection], Error>) -> ())
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetEntriesService: AnyObject {
|
||||
public protocol FeedlyGetEntriesService: AnyObject {
|
||||
func getEntries(for ids: Set<String>, completion: @escaping (Result<[FeedlyEntry], Error>) -> ())
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetStreamContentsService: AnyObject {
|
||||
public protocol FeedlyGetStreamContentsService: AnyObject {
|
||||
func getStreamContents(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStream, Error>) -> ())
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
protocol FeedlyGetStreamIDsService: AnyObject {
|
||||
public protocol FeedlyGetStreamIDsService: AnyObject {
|
||||
func getStreamIDs(for resource: FeedlyResourceID, continuation: String?, newerThan: Date?, unreadOnly: Bool?, completion: @escaping (Result<FeedlyStreamIDs, Error>) -> ())
|
||||
}
|
||||
@@ -8,28 +8,29 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
enum FeedlyMarkAction: String {
|
||||
case read
|
||||
case unread
|
||||
case saved
|
||||
case unsaved
|
||||
|
||||
public enum FeedlyMarkAction: String, Sendable {
|
||||
|
||||
case read
|
||||
case unread
|
||||
case saved
|
||||
case unsaved
|
||||
|
||||
/// These values are paired with the "action" key in POST requests to the markers API.
|
||||
/// See for example: https://developer.feedly.com/v3/markers/#mark-one-or-multiple-articles-as-read
|
||||
var actionValue: String {
|
||||
switch self {
|
||||
case .read:
|
||||
return "markAsRead"
|
||||
case .unread:
|
||||
return "keepUnread"
|
||||
case .saved:
|
||||
return "markAsSaved"
|
||||
case .unsaved:
|
||||
return "markAsUnsaved"
|
||||
}
|
||||
}
|
||||
}
|
||||
public var actionValue: String {
|
||||
switch self {
|
||||
case .read:
|
||||
return "markAsRead"
|
||||
case .unread:
|
||||
return "keepUnread"
|
||||
case .saved:
|
||||
return "markAsSaved"
|
||||
case .unsaved:
|
||||
return "markAsUnsaved"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protocol FeedlyMarkArticlesService: AnyObject {
|
||||
public protocol FeedlyMarkArticlesService: AnyObject {
|
||||
func mark(_ articleIDs: Set<String>, as action: FeedlyMarkAction, completion: @escaping (Result<Void, Error>) -> ())
|
||||
}
|
||||
12
Feedly/Tests/FeedlyTests/FeedlyTests.swift
Normal file
12
Feedly/Tests/FeedlyTests/FeedlyTests.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import XCTest
|
||||
@testable import Feedly
|
||||
|
||||
final class FeedlyTests: 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
|
||||
}
|
||||
}
|
||||
@@ -1392,6 +1392,7 @@
|
||||
84A3EE52223B667F00557320 /* DefaultFeeds.opml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = DefaultFeeds.opml; sourceTree = "<group>"; };
|
||||
84A699132BC34E8500605AB8 /* ArticleExtractor */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = ArticleExtractor; sourceTree = "<group>"; };
|
||||
84A699182BC3524C00605AB8 /* LocalAccount */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = LocalAccount; sourceTree = "<group>"; };
|
||||
84A699192BC36EDB00605AB8 /* Feedly */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Feedly; sourceTree = "<group>"; };
|
||||
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardFolder.swift; sourceTree = "<group>"; };
|
||||
84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = "<group>"; };
|
||||
84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineDataSource.swift; sourceTree = "<group>"; };
|
||||
@@ -2353,6 +2354,7 @@
|
||||
84FB9FAE2BC3494B00B7AFC3 /* FeedFinder */,
|
||||
84A699182BC3524C00605AB8 /* LocalAccount */,
|
||||
84FB9FAD2BC344F800B7AFC3 /* Feedbin */,
|
||||
84A699192BC36EDB00605AB8 /* Feedly */,
|
||||
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */,
|
||||
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,
|
||||
845F3D2B2BC268FE00AEBB68 /* CloudKitSync */,
|
||||
|
||||
Reference in New Issue
Block a user