Create Feedbin module.

This commit is contained in:
Brent Simmons 2024-04-07 14:34:06 -07:00
parent a51d161e35
commit 826ec7d413
25 changed files with 225 additions and 139 deletions

View File

@ -23,6 +23,7 @@ let package = Package(
.package(path: "../ReaderAPI"),
.package(path: "../CloudKitSync"),
.package(path: "../NewsBlur"),
.package(path: "../Feedbin"),
.package(path: "../CommonErrors")
],
targets: [
@ -41,6 +42,7 @@ let package = Package(
"ReaderAPI",
"NewsBlur",
"CloudKitSync",
"Feedbin",
"CommonErrors"
],
swiftSettings: [

View File

@ -13,6 +13,7 @@
import Foundation
import Web
import Secrets
import Feedbin
enum CreateSubscriptionResult {
case created(FeedbinSubscription)

View File

@ -14,6 +14,7 @@ import SyncDatabase
import os.log
import Secrets
import Core
import Feedbin
public enum FeedbinAccountDelegateError: String, Error {
case invalidParameter = "There was an invalid parameter passed."

View File

@ -1,73 +0,0 @@
//
// FeedbinFeed.swift
// Account
//
// Created by Brent Simmons on 12/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Parser
struct FeedbinSubscription: Hashable, Codable {
let subscriptionID: Int
let feedID: Int
let name: String?
let url: String
let homePageURL: String?
let jsonFeed: FeedbinSubscriptionJSONFeed?
enum CodingKeys: String, CodingKey {
case subscriptionID = "id"
case feedID = "feed_id"
case name = "title"
case url = "feed_url"
case homePageURL = "site_url"
case jsonFeed = "json_feed"
}
public func hash(into hasher: inout Hasher) {
hasher.combine(subscriptionID)
}
static func == (lhs: FeedbinSubscription, rhs: FeedbinSubscription) -> Bool {
return lhs.subscriptionID == rhs.subscriptionID
}
}
struct FeedbinSubscriptionJSONFeed: Codable {
let favicon: String?
let icon: String?
enum CodingKeys: String, CodingKey {
case favicon = "favicon"
case icon = "icon"
}
}
struct FeedbinCreateSubscription: Codable {
let feedURL: String
enum CodingKeys: String, CodingKey {
case feedURL = "feed_url"
}
}
struct FeedbinUpdateSubscription: Codable {
let title: String
enum CodingKeys: String, CodingKey {
case title
}
}
struct FeedbinSubscriptionChoice: Codable {
let name: String?
let url: String
enum CodingKeys: String, CodingKey {
case name = "title"
case url = "feed_url"
}
}

View File

@ -37,7 +37,7 @@ public extension URLRequest {
]
httpBody = postData.enhancedPercentEncodedQuery?.data(using: .utf8)
case .newsBlurSessionId:
setValue("\(NewsBlurAPICaller.SessionIdCookie)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
setValue("\(NewsBlurAPICaller.sessionIDCookieKey)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
httpShouldHandleCookies = true
case .readerBasic:
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")

8
Feedbin/.gitignore vendored Normal file
View File

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

31
Feedbin/Package.swift Normal file
View File

@ -0,0 +1,31 @@
// swift-tools-version: 5.10
import PackageDescription
let package = Package(
name: "Feedbin",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
.library(
name: "Feedbin",
targets: ["Feedbin"]),
],
dependencies: [
.package(path: "../Parser"),
],
targets: [
.target(
name: "Feedbin",
dependencies: [
"Parser"
],
swiftSettings: [
.enableExperimentalFeature("StrictConcurrency")
]
),
.testTarget(
name: "FeedbinTests",
dependencies: ["Feedbin"]),
]
)

View File

@ -8,7 +8,7 @@
import Foundation
struct FeedbinDate {
public struct FeedbinDate {
public static let formatter: DateFormatter = {
let formatter = DateFormatter()
@ -17,5 +17,4 @@ struct FeedbinDate {
formatter.timeZone = TimeZone(abbreviation: "GMT")
return formatter
}()
}

View File

@ -9,24 +9,24 @@
import Foundation
import Parser
final class FeedbinEntry: Decodable {
public final class FeedbinEntry: Decodable, @unchecked Sendable {
let articleID: Int
let feedID: Int
let title: String?
let url: String?
let authorName: String?
let contentHTML: String?
let summary: String?
let datePublished: String?
let dateArrived: String?
let jsonFeed: FeedbinEntryJSONFeed?
public let articleID: Int
public let feedID: Int
public let title: String?
public let url: String?
public let authorName: String?
public let contentHTML: String?
public let summary: String?
public let datePublished: String?
public let dateArrived: String?
public let jsonFeed: FeedbinEntryJSONFeed?
// Feedbin dates can't be decoded by the JSONDecoding 8601 decoding strategy. Feedbin
// requires a very specific date formatter to work and even then it fails occasionally.
// Rather than loose all the entries we only lose the one date by decoding as a string
// and letting the one date fail when parsed.
lazy var parsedDatePublished: Date? = {
public lazy var parsedDatePublished: Date? = {
if let datePublished = datePublished {
return RSDateWithString(datePublished)
}
@ -49,9 +49,10 @@ final class FeedbinEntry: Decodable {
}
}
struct FeedbinEntryJSONFeed: Decodable {
let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
let jsonFeedExternalURL: String?
public struct FeedbinEntryJSONFeed: Decodable, Sendable {
public let jsonFeedAuthor: FeedbinEntryJSONFeedAuthor?
public let jsonFeedExternalURL: String?
enum CodingKeys: String, CodingKey {
case jsonFeedAuthor = "author"
@ -74,9 +75,11 @@ struct FeedbinEntryJSONFeed: Decodable {
}
struct FeedbinEntryJSONFeedAuthor: Decodable {
let url: String?
let avatarURL: String?
public struct FeedbinEntryJSONFeedAuthor: Decodable, Sendable {
public let url: String?
public let avatarURL: String?
enum CodingKeys: String, CodingKey {
case url = "url"
case avatarURL = "avatar"

View File

@ -8,10 +8,10 @@
import Foundation
struct FeedbinImportResult: Codable {
let importResultID: Int
let complete: Bool
public struct FeedbinImportResult: Codable, Sendable {
public let importResultID: Int
public let complete: Bool
enum CodingKeys: String, CodingKey {
case importResultID = "id"

View File

@ -8,12 +8,16 @@
import Foundation
struct FeedbinStarredEntry: Codable {
let starredEntries: [Int]
public struct FeedbinStarredEntry: Codable, Sendable {
public let starredEntries: [Int]
enum CodingKeys: String, CodingKey {
case starredEntries = "starred_entries"
}
public init(starredEntries: [Int]) {
self.starredEntries = starredEntries
}
}

View File

@ -0,0 +1,89 @@
//
// FeedbinFeed.swift
// Account
//
// Created by Brent Simmons on 12/10/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Parser
public struct FeedbinSubscription: Hashable, Codable, Sendable {
public let subscriptionID: Int
public let feedID: Int
public let name: String?
public let url: String
public let homePageURL: String?
public let jsonFeed: FeedbinSubscriptionJSONFeed?
enum CodingKeys: String, CodingKey {
case subscriptionID = "id"
case feedID = "feed_id"
case name = "title"
case url = "feed_url"
case homePageURL = "site_url"
case jsonFeed = "json_feed"
}
public func hash(into hasher: inout Hasher) {
hasher.combine(subscriptionID)
}
public static func == (lhs: FeedbinSubscription, rhs: FeedbinSubscription) -> Bool {
return lhs.subscriptionID == rhs.subscriptionID
}
}
public struct FeedbinSubscriptionJSONFeed: Codable, Sendable {
public let favicon: String?
public let icon: String?
enum CodingKeys: String, CodingKey {
case favicon = "favicon"
case icon = "icon"
}
}
public struct FeedbinCreateSubscription: Codable, Sendable {
public let feedURL: String
enum CodingKeys: String, CodingKey {
case feedURL = "feed_url"
}
public init(feedURL: String) {
self.feedURL = feedURL
}
}
public struct FeedbinUpdateSubscription: Codable, Sendable {
public let title: String
enum CodingKeys: String, CodingKey {
case title
}
public init(title: String) {
self.title = title
}
}
public struct FeedbinSubscriptionChoice: Codable, Sendable {
public let name: String?
public let url: String
enum CodingKeys: String, CodingKey {
case name = "title"
case url = "feed_url"
}
}

View File

@ -8,36 +8,39 @@
import Foundation
struct FeedbinTag: Codable {
let tagID: Int
let name: String
public struct FeedbinTag: Codable, Sendable {
public let tagID: Int
public let name: String
enum CodingKeys: String, CodingKey {
case tagID = "id"
case name = "name"
}
}
struct FeedbinRenameTag: Codable {
let oldName: String
let newName: String
public struct FeedbinRenameTag: Codable, Sendable {
public let oldName: String
public let newName: String
enum CodingKeys: String, CodingKey {
case oldName = "old_name"
case newName = "new_name"
}
public init(oldName: String, newName: String) {
self.oldName = oldName
self.newName = newName
}
}
struct FeedbinDeleteTag: Codable {
let name: String
public struct FeedbinDeleteTag: Codable, Sendable {
public let name: String
enum CodingKeys: String, CodingKey {
case name
}
}

View File

@ -8,28 +8,32 @@
import Foundation
struct FeedbinTagging: Codable {
public struct FeedbinTagging: Codable, Sendable {
let taggingID: Int
let feedID: Int
let name: String
public let taggingID: Int
public let feedID: Int
public let name: String
enum CodingKeys: String, CodingKey {
case taggingID = "id"
case feedID = "feed_id"
case name = "name"
}
}
struct FeedbinCreateTagging: Codable {
let feedID: Int
let name: String
public struct FeedbinCreateTagging: Codable, Sendable {
public let feedID: Int
public let name: String
enum CodingKeys: String, CodingKey {
case feedID = "feed_id"
case name = "name"
}
public init(feedID: Int, name: String) {
self.feedID = feedID
self.name = name
}
}

View File

@ -8,12 +8,16 @@
import Foundation
struct FeedbinUnreadEntry: Codable {
let unreadEntries: [Int]
public struct FeedbinUnreadEntry: Codable, Sendable {
public let unreadEntries: [Int]
enum CodingKeys: String, CodingKey {
case unreadEntries = "unread_entries"
}
public init(unreadEntries: [Int]) {
self.unreadEntries = unreadEntries
}
}

View File

@ -0,0 +1,12 @@
import XCTest
@testable import Feedbin
final class FeedbinTests: XCTestCase {
func testExample() throws {
// XCTest Documentation
// https://developer.apple.com/documentation/xctest
// Defining Test Cases and Test Methods
// https://developer.apple.com/documentation/xctest/defining_test_cases_and_test_methods
}
}

View File

@ -1466,6 +1466,7 @@
84F9EAE2213660A100CF2DE4 /* establishMainWindowStartingState.applescript */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.applescript; path = establishMainWindowStartingState.applescript; sourceTree = "<group>"; };
84F9EAE4213660A100CF2DE4 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = NewsBlur; sourceTree = "<group>"; };
84FB9FAD2BC344F800B7AFC3 /* Feedbin */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Feedbin; sourceTree = "<group>"; };
84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaviconURLFinder.swift; sourceTree = "<group>"; };
B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = "<group>"; };
B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = "<group>"; };
@ -2358,6 +2359,7 @@
849C64611ED37A5D003D8FC0 /* Products */,
51C452B22265141B00C03939 /* Frameworks */,
51CD32C624D2DEF9009ABAEF /* Account */,
84FB9FAD2BC344F800B7AFC3 /* Feedbin */,
84FB9FAC2BC33AFE00B7AFC3 /* NewsBlur */,
84CC98D92BC1DD25006A05C9 /* ReaderAPI */,
845F3D2B2BC268FE00AEBB68 /* CloudKitSync */,

View File

@ -1,5 +1,4 @@
// swift-tools-version: 5.10
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
@ -7,7 +6,6 @@ let package = Package(
name: "NewsBlur",
platforms: [.macOS(.v14), .iOS(.v17)],
products: [
// Products define the executables and libraries a package produces, making them visible to other packages.
.library(
name: "NewsBlur",
targets: ["NewsBlur"]),
@ -18,8 +16,6 @@ let package = Package(
.package(path: "../Parser"),
],
targets: [
// Targets are the basic building blocks of a package, defining a module or a test suite.
// Targets can depend on other targets in this package and products from dependencies.
.target(
name: "NewsBlur",
dependencies: [

View File

@ -12,7 +12,7 @@ import Secrets
@MainActor public final class NewsBlurAPICaller: NSObject {
public static let SessionIdCookie = "newsblur_sessionid"
public static let sessionIDCookieKey = "newsblur_sessionid"
let baseURL = URL(string: "https://www.newsblur.com/")!
var transport: Transport!
@ -55,7 +55,7 @@ import Secrets
}
let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
for cookie in cookies where cookie.name == Self.SessionIdCookie {
for cookie in cookies where cookie.name == Self.sessionIDCookieKey {
let credentials = Credentials(type: .newsBlurSessionId, username: username, secret: cookie.value)
completion(.success(credentials))
return

View File

@ -36,7 +36,7 @@ public extension URLRequest {
} else if credentialsType == .newsBlurSessionId {
setValue("\(NewsBlurAPICaller.SessionIdCookie)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
setValue("\(NewsBlurAPICaller.sessionIDCookieKey)=\(credentials.secret)", forHTTPHeaderField: "Cookie")
httpShouldHandleCookies = true
}

View File

@ -10,7 +10,7 @@ import Foundation
public struct FeedParserError: Error, Sendable {
public enum FeedParserErrorType {
public enum FeedParserErrorType: Sendable {
case rssChannelNotFound
case rssItemsNotFound