Coalesce the various Feedly model files into one file.

This commit is contained in:
Brent Simmons 2024-04-27 11:25:39 -07:00
parent 4f04578bd7
commit 47c68aa4cc
17 changed files with 457 additions and 592 deletions

View File

@ -0,0 +1,457 @@
//
// FeedlyModel.swift
//
//
// Created by Brent Simmons on 4/27/24.
// Includes text of a bunch of files created by Kiel Gillard 2019-2020
//
import Foundation
import Articles
import Parser
public struct FeedlyCategory: Decodable, Sendable, Equatable {
public let label: String
public let id: String
public static func ==(lhs: FeedlyCategory, rhs: FeedlyCategory) -> Bool {
lhs.label == rhs.label && lhs.id == rhs.id
}
}
public struct FeedlyCollection: Codable, Sendable {
public let feeds: [FeedlyFeed]
public let label: String
public let id: String
}
public struct FeedlyCollectionParser: Sendable {
public let collection: FeedlyCollection
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
public var folderName: String {
return rightToLeftTextSantizer.sanitize(collection.label) ?? ""
}
public var externalID: String {
return collection.id
}
public init(collection: FeedlyCollection) {
self.collection = collection
}
}
public struct FeedlyEntry: Decodable, Sendable, Hashable {
/// the unique, immutable ID for this particular article.
public let id: String
/// the articles title. This string does not contain any HTML markup.
public let title: String?
public struct Content: Decodable, Sendable, Equatable {
public enum Direction: String, Decodable, Sendable {
case leftToRight = "ltr"
case rightToLeft = "rtl"
}
public let content: String?
public let direction: Direction?
public static func ==(lhs: Content, rhs: Content) -> Bool {
lhs.content == rhs.content && lhs.direction == rhs.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.
public let content: Content?
/// content object the article summary. See the content object above.
public let summary: Content?
/// the authors name
public let author: String?
/// the immutable timestamp, in ms, when this article was processed by the feedly Cloud servers.
public let crawled: Date
/// the timestamp, in ms, when this article was re-processed and updated by the feedly Cloud servers.
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 feeds website.
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
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.
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.
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.
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.
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.
public let enclosure: [FeedlyLink]?
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public static func ==(lhs: FeedlyEntry, rhs: FeedlyEntry) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title && lhs.content == rhs.content && lhs.summary == rhs.summary && lhs.author == rhs.author && lhs.crawled == rhs.crawled && lhs.recrawled == rhs.recrawled && lhs.origin == rhs.origin && lhs.canonical == rhs.canonical && lhs.alternate == rhs.alternate && lhs.unread == rhs.unread && lhs.tags == rhs.tags && lhs.categories == rhs.categories && lhs.enclosure == rhs.enclosure
}
}
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)
}
}
public struct FeedlyEntryParser: Sendable {
public let entry: FeedlyEntry
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
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.
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.")
return nil
}
return id
}
/// Convoluted external URL logic "documented" here:
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
public var externalUrl: String? {
let multidimensionalArrayOfLinks = [entry.canonical, entry.alternate]
let withExistingValues = multidimensionalArrayOfLinks.compactMap { $0 }
let flattened = withExistingValues.flatMap { $0 }
let webPageLinks = flattened.filter { $0.type == nil || $0.type == "text/html" }
return webPageLinks.first?.href
}
public var title: String? {
return rightToLeftTextSantizer.sanitize(entry.title)
}
public var contentHMTL: String? {
return entry.content?.content ?? entry.summary?.content
}
public var contentText: String? {
// We could strip HTML from contentHTML?
return nil
}
public var summary: String? {
return rightToLeftTextSantizer.sanitize(entry.summary?.content)
}
public var datePublished: Date {
return entry.crawled
}
public var dateModified: Date? {
return entry.recrawled
}
public var authors: Set<ParsedAuthor>? {
guard let name = entry.author else {
return nil
}
return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)])
}
/// While there is not yet a tagging interface, articles can still be searched for by tags.
public var tags: Set<String>? {
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
return nil
}
return Set(labels)
}
public var attachments: Set<ParsedAttachment>? {
guard let enclosure = entry.enclosure, !enclosure.isEmpty else {
return nil
}
let attachments = enclosure.compactMap { ParsedAttachment(url: $0.href, mimeType: $0.type, title: nil, sizeInBytes: nil, durationInSeconds: nil) }
return attachments.isEmpty ? nil : Set(attachments)
}
public var parsedItemRepresentation: ParsedItem? {
guard let feedUrl = feedUrl else {
return nil
}
return ParsedItem(syncServiceID: id,
uniqueID: id, // This value seems to get ignored or replaced.
feedURL: feedUrl,
url: nil,
externalURL: externalUrl,
title: title,
language: nil,
contentHTML: contentHMTL,
contentText: contentText,
summary: summary,
imageURL: nil,
bannerImageURL: nil,
datePublished: datePublished,
dateModified: dateModified,
authors: authors,
tags: tags,
attachments: attachments)
}
}
public struct FeedlyFeed: Codable, Sendable {
public let id: String
public let title: String?
public let updated: Date?
public let website: String?
}
public struct FeedlyFeedParser: Sendable {
public let feed: FeedlyFeed
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
public var title: String? {
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
}
public var feedID: String {
return feed.id
}
public var url: String {
let resource = FeedlyFeedResourceID(id: feed.id)
return resource.url
}
public var homePageURL: String? {
return feed.website
}
public init(feed: FeedlyFeed) {
self.feed = feed
}
}
public struct FeedlyFeedsSearchResponse: Decodable, Sendable {
public struct Feed: Decodable, Sendable {
public let title: String
public let feedId: String
}
public let results: [Feed]
}
public struct FeedlyLink: Decodable, Sendable, Equatable {
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
public let type: String?
public static func ==(lhs: FeedlyLink, rhs: FeedlyLink) -> Bool {
lhs.href == rhs.href && lhs.type == rhs.type
}
}
public struct FeedlyOrigin: Decodable, Sendable, Equatable {
public let title: String?
public let streamID: String?
public let htmlURL: String?
public static func ==(lhs: FeedlyOrigin, rhs: FeedlyOrigin) -> Bool {
lhs.title == rhs.title && lhs.streamID == rhs.streamID && lhs.htmlURL == rhs.htmlURL
}
}
/// The kinds of Resource IDs is documented here: https://developer.feedly.com/cloud/
public protocol FeedlyResourceID {
/// The resource ID from Feedly.
@MainActor var id: String { get }
}
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
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.
/// This is basically assuming Feedly prefixes source feed URLs with `feed/`.
/// 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
public var url: String {
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
var mutant = id
mutant.removeSubrange(range)
return mutant
}
// 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)"
}
}
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.
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.
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)
}
}
}
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)
}
}
}
public struct FeedlyRTLTextSanitizer: Sendable {
private let rightToLeftPrefix = "<div style=\"direction:rtl;text-align:right\">"
private let rightToLeftSuffix = "</div>"
public func sanitize(_ sourceText: String?) -> String? {
guard let source = sourceText, !source.isEmpty else {
return sourceText
}
guard source.hasPrefix(rightToLeftPrefix) && source.hasSuffix(rightToLeftSuffix) else {
return source
}
let start = source.index(source.startIndex, offsetBy: rightToLeftPrefix.indices.count)
let end = source.index(source.endIndex, offsetBy: -rightToLeftSuffix.indices.count)
return String(source[start..<end])
}
}
public struct FeedlyStream: Decodable, Sendable {
public let id: String
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
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.
public let continuation: String?
public let items: [FeedlyEntry]
public var isStreamEnd: Bool {
return continuation == nil
}
}
public struct FeedlyStreamIDs: Decodable, Sendable {
public let continuation: String?
public let ids: [String]
public var isStreamEnd: Bool {
return continuation == nil
}
}
public struct FeedlyTag: Decodable, Sendable, Equatable {
public let id: String
public let label: String?
public static func ==(lhs: FeedlyTag, rhs: FeedlyTag) -> Bool {
lhs.id == rhs.id && lhs.label == rhs.label
}
}

View File

@ -1,19 +0,0 @@
//
// FeedlyCategory.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyCategory: Decodable, Sendable, Equatable {
public let label: String
public let id: String
public static func ==(lhs: FeedlyCategory, rhs: FeedlyCategory) -> Bool {
lhs.label == rhs.label && lhs.id == rhs.id
}
}

View File

@ -1,16 +0,0 @@
//
// FeedlyCollection.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyCollection: Codable, Sendable {
public let feeds: [FeedlyFeed]
public let label: String
public let id: String
}

View File

@ -1,28 +0,0 @@
//
// FeedlyCollectionParser.swift
// Account
//
// Created by Kiel Gillard on 28/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyCollectionParser: Sendable {
public let collection: FeedlyCollection
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
public var folderName: String {
return rightToLeftTextSantizer.sanitize(collection.label) ?? ""
}
public var externalID: String {
return collection.id
}
public init(collection: FeedlyCollection) {
self.collection = collection
}
}

View File

@ -1,79 +0,0 @@
//
// FeedlyEntry.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyEntry: Decodable, Sendable, Hashable {
/// the unique, immutable ID for this particular article.
public let id: String
/// the articles title. This string does not contain any HTML markup.
public let title: String?
public struct Content: Decodable, Sendable, Equatable {
public enum Direction: String, Decodable, Sendable {
case leftToRight = "ltr"
case rightToLeft = "rtl"
}
public let content: String?
public let direction: Direction?
public static func ==(lhs: Content, rhs: Content) -> Bool {
lhs.content == rhs.content && lhs.direction == rhs.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.
public let content: Content?
/// content object the article summary. See the content object above.
public let summary: Content?
/// the authors name
public let author: String?
/// the immutable timestamp, in ms, when this article was processed by the feedly Cloud servers.
public let crawled: Date
/// the timestamp, in ms, when this article was re-processed and updated by the feedly Cloud servers.
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 feeds website.
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
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.
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.
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.
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.
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.
public let enclosure: [FeedlyLink]?
public func hash(into hasher: inout Hasher) {
hasher.combine(id)
}
public static func ==(lhs: FeedlyEntry, rhs: FeedlyEntry) -> Bool {
lhs.id == rhs.id && lhs.title == rhs.title && lhs.content == rhs.content && lhs.summary == rhs.summary && lhs.author == rhs.author && lhs.crawled == rhs.crawled && lhs.recrawled == rhs.recrawled && lhs.origin == rhs.origin && lhs.canonical == rhs.canonical && lhs.alternate == rhs.alternate && lhs.unread == rhs.unread && lhs.tags == rhs.tags && lhs.categories == rhs.categories && lhs.enclosure == rhs.enclosure
}
}

View File

@ -1,30 +0,0 @@
//
// 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)
}
}

View File

@ -1,114 +0,0 @@
//
// FeedlyEntryParser.swift
// Account
//
// Created by Kiel Gillard on 3/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Articles
import Parser
public struct FeedlyEntryParser: Sendable {
public let entry: FeedlyEntry
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
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.
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.")
return nil
}
return id
}
/// Convoluted external URL logic "documented" here:
/// https://groups.google.com/forum/#!searchin/feedly-cloud/feed$20url%7Csort:date/feedly-cloud/Rx3dVd4aTFQ/Hf1ZfLJoCQAJ
public var externalUrl: String? {
let multidimensionalArrayOfLinks = [entry.canonical, entry.alternate]
let withExistingValues = multidimensionalArrayOfLinks.compactMap { $0 }
let flattened = withExistingValues.flatMap { $0 }
let webPageLinks = flattened.filter { $0.type == nil || $0.type == "text/html" }
return webPageLinks.first?.href
}
public var title: String? {
return rightToLeftTextSantizer.sanitize(entry.title)
}
public var contentHMTL: String? {
return entry.content?.content ?? entry.summary?.content
}
public var contentText: String? {
// We could strip HTML from contentHTML?
return nil
}
public var summary: String? {
return rightToLeftTextSantizer.sanitize(entry.summary?.content)
}
public var datePublished: Date {
return entry.crawled
}
public var dateModified: Date? {
return entry.recrawled
}
public var authors: Set<ParsedAuthor>? {
guard let name = entry.author else {
return nil
}
return Set([ParsedAuthor(name: name, url: nil, avatarURL: nil, emailAddress: nil)])
}
/// While there is not yet a tagging interface, articles can still be searched for by tags.
public var tags: Set<String>? {
guard let labels = entry.tags?.compactMap({ $0.label }), !labels.isEmpty else {
return nil
}
return Set(labels)
}
public var attachments: Set<ParsedAttachment>? {
guard let enclosure = entry.enclosure, !enclosure.isEmpty else {
return nil
}
let attachments = enclosure.compactMap { ParsedAttachment(url: $0.href, mimeType: $0.type, title: nil, sizeInBytes: nil, durationInSeconds: nil) }
return attachments.isEmpty ? nil : Set(attachments)
}
public var parsedItemRepresentation: ParsedItem? {
guard let feedUrl = feedUrl else {
return nil
}
return ParsedItem(syncServiceID: id,
uniqueID: id, // This value seems to get ignored or replaced.
feedURL: feedUrl,
url: nil,
externalURL: externalUrl,
title: title,
language: nil,
contentHTML: contentHMTL,
contentText: contentText,
summary: summary,
imageURL: nil,
bannerImageURL: nil,
datePublished: datePublished,
dateModified: dateModified,
authors: authors,
tags: tags,
attachments: attachments)
}
}

View File

@ -1,17 +0,0 @@
//
// FeedlyFeed.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyFeed: Codable, Sendable {
public let id: String
public let title: String?
public let updated: Date?
public let website: String?
}

View File

@ -1,38 +0,0 @@
//
// FeedlyFeedParser.swift
// Account
//
// Created by Kiel Gillard on 29/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyFeedParser: Sendable {
public let feed: FeedlyFeed
private let rightToLeftTextSantizer = FeedlyRTLTextSanitizer()
public var title: String? {
return rightToLeftTextSantizer.sanitize(feed.title) ?? ""
}
public var feedID: String {
return feed.id
}
public var url: String {
let resource = FeedlyFeedResourceID(id: feed.id)
return resource.url
}
public var homePageURL: String? {
return feed.website
}
public init(feed: FeedlyFeed) {
self.feed = feed
}
}

View File

@ -1,20 +0,0 @@
//
// FeedlyFeedsSearchResponse.swift
// Account
//
// Created by Kiel Gillard on 1/12/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyFeedsSearchResponse: Decodable, Sendable {
public struct Feed: Decodable, Sendable {
public let title: String
public let feedId: String
}
public let results: [Feed]
}

View File

@ -1,23 +0,0 @@
//
// FeedlyLink.swift
// Account
//
// Created by Kiel Gillard on 3/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyLink: Decodable, Sendable, Equatable {
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
public let type: String?
public static func ==(lhs: FeedlyLink, rhs: FeedlyLink) -> Bool {
lhs.href == rhs.href && lhs.type == rhs.type
}
}

View File

@ -1,21 +0,0 @@
//
// FeedlyOrigin.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyOrigin: Decodable, Sendable, Equatable {
public let title: String?
public let streamID: String?
public let htmlURL: String?
public static func ==(lhs: FeedlyOrigin, rhs: FeedlyOrigin) -> Bool {
lhs.title == rhs.title && lhs.streamID == rhs.streamID && lhs.htmlURL == rhs.htmlURL
}
}

View File

@ -1,29 +0,0 @@
//
// FeedlyRTLTextSanitizer.swift
// Account
//
// Created by Kiel Gillard on 28/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyRTLTextSanitizer: Sendable {
private let rightToLeftPrefix = "<div style=\"direction:rtl;text-align:right\">"
private let rightToLeftSuffix = "</div>"
public func sanitize(_ sourceText: String?) -> String? {
guard let source = sourceText, !source.isEmpty else {
return sourceText
}
guard source.hasPrefix(rightToLeftPrefix) && source.hasSuffix(rightToLeftSuffix) else {
return source
}
let start = source.index(source.startIndex, offsetBy: rightToLeftPrefix.indices.count)
let end = source.index(source.endIndex, offsetBy: -rightToLeftSuffix.indices.count)
return String(source[start..<end])
}
}

View File

@ -1,93 +0,0 @@
//
// FeedlyResourceID.swift
// Account
//
// Created by Kiel Gillard on 3/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
/// The kinds of Resource IDs is documented here: https://developer.feedly.com/cloud/
public protocol FeedlyResourceID {
/// The resource ID from Feedly.
@MainActor var id: String { get }
}
/// The Feed Resource is documented here: https://developer.feedly.com/cloud/
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.
/// This is basically assuming Feedly prefixes source feed URLs with `feed/`.
/// 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
public var url: String {
if let range = id.range(of: "feed/"), range.lowerBound == id.startIndex {
var mutant = id
mutant.removeSubrange(range)
return mutant
}
// 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)"
}
}
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.
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.
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)
}
}
}
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)
}
}
}

View File

@ -1,27 +0,0 @@
//
// FeedlyStream.swift
// Account
//
// Created by Kiel Gillard on 19/9/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyStream: Decodable, Sendable {
public let id: String
/// Of the most recent entry for this stream (regardless of continuation, newerThan, etc).
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.
public let continuation: String?
public let items: [FeedlyEntry]
public var isStreamEnd: Bool {
return continuation == nil
}
}

View File

@ -1,19 +0,0 @@
//
// FeedlyStreamIDs.swift
// Account
//
// Created by Kiel Gillard on 18/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyStreamIDs: Decodable, Sendable {
public let continuation: String?
public let ids: [String]
public var isStreamEnd: Bool {
return continuation == nil
}
}

View File

@ -1,19 +0,0 @@
//
// FeedlyTag.swift
// Account
//
// Created by Kiel Gillard on 3/10/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
public struct FeedlyTag: Decodable, Sendable, Equatable {
public let id: String
public let label: String?
public static func ==(lhs: FeedlyTag, rhs: FeedlyTag) -> Bool {
lhs.id == rhs.id && lhs.label == rhs.label
}
}