NetNewsWire/Frameworks/Account/CloudKit/CloudKitArticlesZone.swift

249 lines
7.8 KiB
Swift
Raw Normal View History

2020-04-01 18:46:37 +02:00
//
// CloudKitArticlesZone.swift
// Account
//
// Created by Maurice Parker on 4/1/20.
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import os.log
import RSParser
2020-04-01 18:46:37 +02:00
import RSWeb
import CloudKit
2020-04-03 01:06:47 +02:00
import Articles
2020-04-01 18:46:37 +02:00
import SyncDatabase
final class CloudKitArticlesZone: CloudKitZone {
static var zoneID: CKRecordZone.ID {
return CKRecordZone.ID(zoneName: "Articles", ownerName: CKCurrentUserDefaultName)
}
var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
weak var container: CKContainer?
weak var database: CKDatabase?
var delegate: CloudKitZoneDelegate? = nil
2020-04-03 01:06:47 +02:00
struct CloudKitArticle {
static let recordType = "Article"
struct Fields {
static let articleStatus = "articleStatus"
static let webFeedURL = "webFeedURL"
2020-04-03 01:06:47 +02:00
static let uniqueID = "uniqueID"
static let title = "title"
static let contentHTML = "contentHTML"
static let contentText = "contentText"
static let url = "url"
static let externalURL = "externalURL"
static let summary = "summary"
static let imageURL = "imageURL"
static let datePublished = "datePublished"
static let dateModified = "dateModified"
static let parsedAuthors = "parsedAuthors"
2020-04-03 01:06:47 +02:00
}
}
2020-04-01 18:46:37 +02:00
struct CloudKitArticleStatus {
static let recordType = "ArticleStatus"
struct Fields {
static let webFeedExternalID = "webFeedExternalID"
2020-04-01 18:46:37 +02:00
static let read = "read"
static let starred = "starred"
}
}
init(container: CKContainer) {
self.container = container
self.database = container.privateCloudDatabase
}
func refreshArticlesAndStatuses(completion: @escaping ((Result<Void, Error>) -> Void)) {
fetchChangesInZone() { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.refreshArticlesAndStatuses(completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
}
}
func saveNewArticlesAndStatuses(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
}
2020-04-26 03:20:56 +02:00
var records = makeNewStatusRecords(articles)
for article in articles {
records.append(contentsOf: makeArticleRecords(article))
}
saveIfNew(records, completion: completion)
}
func deleteArticlesAndStatuses(_ articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
guard !articles.isEmpty else {
completion(.success(()))
return
}
let recordIDs = articles.map { CKRecord.ID(recordName: statusID($0.articleID), zoneID: Self.zoneID) }
delete(recordIDs: recordIDs, completion: completion)
}
func modifyArticlesAndStatuses(_ syncStatuses: [SyncStatus], articles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
var records = makeStatusRecords(syncStatuses, articles)
let saveArticles = articles.filter { $0.status.read == false || $0.status.starred == true }
for saveArticle in saveArticles {
records.append(contentsOf: makeArticleRecords(saveArticle))
}
let deleteArticleIDs = articles.subtracting(saveArticles).map {
return CKRecord.ID(recordName: articleID($0.articleID), zoneID: Self.zoneID)
}
self.modify(recordsToSave: records, recordIDsToDelete: deleteArticleIDs) { result in
switch result {
case .success:
completion(.success(()))
case .failure(let error):
self.handleSendArticleStatusError(error, syncStatuses: syncStatuses, starredArticles: articles, completion: completion)
}
}
}
func handleSendArticleStatusError(_ error: Error, syncStatuses: [SyncStatus], starredArticles: Set<Article>, completion: @escaping ((Result<Void, Error>) -> Void)) {
if case CloudKitZoneError.userDeletedZone = error {
self.createZoneRecord() { result in
switch result {
case .success:
self.modifyArticlesAndStatuses(syncStatuses, articles: starredArticles, completion: completion)
case .failure(let error):
completion(.failure(error))
}
}
} else {
completion(.failure(error))
}
}
2020-04-03 01:06:47 +02:00
}
private extension CloudKitArticlesZone {
func statusID(_ id: String) -> String {
return "s|\(id)"
}
func articleID(_ id: String) -> String {
return "a|\(id)"
}
func makeNewStatusRecords(_ articles: Set<Article>) -> [CKRecord] {
var records = [CKRecord]()
for article in articles {
let recordID = CKRecord.ID(recordName: statusID(article.articleID), zoneID: Self.zoneID)
let record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
if let webFeedExternalID = article.webFeed?.externalID {
record[CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
}
record[CloudKitArticleStatus.Fields.read] = "0"
records.append(record)
}
return records
}
func makeStatusRecords(_ syncStatuses: [SyncStatus], _ articles: Set<Article>) -> [CKRecord] {
var articleDict = [String: Article]()
for article in articles {
articleDict[article.articleID] = article
}
2020-04-01 18:46:37 +02:00
var records = [String: CKRecord]()
for status in syncStatuses {
var record = records[status.articleID]
if record == nil {
let recordID = CKRecord.ID(recordName: statusID(status.articleID), zoneID: Self.zoneID)
2020-04-01 18:46:37 +02:00
record = CKRecord(recordType: CloudKitArticleStatus.recordType, recordID: recordID)
records[status.articleID] = record
}
if let webFeedExternalID = articleDict[status.articleID]?.webFeed?.externalID {
record![CloudKitArticleStatus.Fields.webFeedExternalID] = webFeedExternalID
}
2020-04-01 18:46:37 +02:00
switch status.key {
case .read:
record![CloudKitArticleStatus.Fields.read] = status.flag ? "1" : "0"
case .starred:
record![CloudKitArticleStatus.Fields.starred] = status.flag ? "1" : "0"
}
}
2020-04-03 01:06:47 +02:00
return Array(records.values)
}
func makeArticleRecords(_ article: Article) -> [CKRecord] {
var records = [CKRecord]()
let recordID = CKRecord.ID(recordName: articleID(article.articleID), zoneID: Self.zoneID)
let articleRecord = CKRecord(recordType: CloudKitArticle.recordType, recordID: recordID)
let articleStatusRecordID = CKRecord.ID(recordName: article.articleID, zoneID: Self.zoneID)
articleRecord[CloudKitArticle.Fields.articleStatus] = CKRecord.Reference(recordID: articleStatusRecordID, action: .deleteSelf)
articleRecord[CloudKitArticle.Fields.webFeedURL] = article.webFeed?.url
articleRecord[CloudKitArticle.Fields.uniqueID] = article.uniqueID
articleRecord[CloudKitArticle.Fields.title] = article.title
articleRecord[CloudKitArticle.Fields.contentHTML] = article.contentHTML
articleRecord[CloudKitArticle.Fields.contentText] = article.contentText
articleRecord[CloudKitArticle.Fields.url] = article.url
articleRecord[CloudKitArticle.Fields.externalURL] = article.externalURL
articleRecord[CloudKitArticle.Fields.summary] = article.summary
articleRecord[CloudKitArticle.Fields.imageURL] = article.imageURL
articleRecord[CloudKitArticle.Fields.datePublished] = article.datePublished
articleRecord[CloudKitArticle.Fields.dateModified] = article.dateModified
let encoder = JSONEncoder()
var parsedAuthors = [String]()
if let authors = article.authors, !authors.isEmpty {
for author in authors {
let parsedAuthor = ParsedAuthor(name: author.name,
url: author.url,
avatarURL: author.avatarURL,
emailAddress: author.emailAddress)
if let data = try? encoder.encode(parsedAuthor), let encodedParsedAuthor = String(data: data, encoding: .utf8) {
parsedAuthors.append(encodedParsedAuthor)
}
2020-04-03 01:06:47 +02:00
}
articleRecord[CloudKitArticle.Fields.parsedAuthors] = parsedAuthors
2020-04-03 01:06:47 +02:00
}
records.append(articleRecord)
return records
2020-04-03 01:06:47 +02:00
}
2020-04-01 18:46:37 +02:00
}