2020-04-01 18:46:37 +02:00
|
|
|
//
|
|
|
|
// CloudKitArticlesZoneDelegate.swift
|
|
|
|
// Account
|
|
|
|
//
|
|
|
|
// Created by Maurice Parker on 4/1/20.
|
|
|
|
// Copyright © 2020 Ranchero Software, LLC. All rights reserved.
|
|
|
|
//
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
import os.log
|
2020-04-03 20:26:08 +02:00
|
|
|
import RSParser
|
2020-04-10 23:25:58 +02:00
|
|
|
import RSWeb
|
2020-04-01 18:46:37 +02:00
|
|
|
import CloudKit
|
2020-04-01 21:10:07 +02:00
|
|
|
import SyncDatabase
|
2020-04-11 19:22:28 +02:00
|
|
|
import Articles
|
|
|
|
import ArticlesDatabase
|
2020-04-01 18:46:37 +02:00
|
|
|
|
|
|
|
class CloudKitArticlesZoneDelegate: CloudKitZoneDelegate {
|
2020-04-01 19:22:59 +02:00
|
|
|
|
2020-04-01 18:46:37 +02:00
|
|
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
|
|
|
|
|
|
|
weak var account: Account?
|
2020-04-01 21:10:07 +02:00
|
|
|
var database: SyncDatabase
|
2020-04-03 18:25:01 +02:00
|
|
|
weak var articlesZone: CloudKitArticlesZone?
|
2020-04-25 22:09:03 +02:00
|
|
|
|
2020-04-26 03:20:56 +02:00
|
|
|
init(account: Account, database: SyncDatabase, articlesZone: CloudKitArticlesZone) {
|
2020-04-01 18:46:37 +02:00
|
|
|
self.account = account
|
2020-04-01 21:10:07 +02:00
|
|
|
self.database = database
|
2020-04-03 18:25:01 +02:00
|
|
|
self.articlesZone = articlesZone
|
2020-04-01 18:46:37 +02:00
|
|
|
}
|
|
|
|
|
2020-04-02 03:21:14 +02:00
|
|
|
func cloudKitDidModify(changed: [CKRecord], deleted: [CloudKitRecordKey], completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
2020-04-01 21:10:07 +02:00
|
|
|
database.selectPendingReadStatusArticleIDs() { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let pendingReadStatusArticleIDs):
|
|
|
|
|
|
|
|
self.database.selectPendingStarredStatusArticleIDs() { result in
|
|
|
|
switch result {
|
|
|
|
case .success(let pendingStarredStatusArticleIDs):
|
2020-04-30 02:13:50 +02:00
|
|
|
|
|
|
|
self.delete(recordKeys: deleted, pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs) {
|
|
|
|
self.update(records: changed,
|
|
|
|
pendingReadStatusArticleIDs: pendingReadStatusArticleIDs,
|
|
|
|
pendingStarredStatusArticleIDs: pendingStarredStatusArticleIDs,
|
|
|
|
completion: completion)
|
|
|
|
}
|
2020-04-01 21:10:07 +02:00
|
|
|
|
|
|
|
case .failure(let error):
|
|
|
|
os_log(.error, log: self.log, "Error occurred geting pending starred records: %@", error.localizedDescription)
|
2020-05-01 23:50:18 +02:00
|
|
|
completion(.failure(CloudKitZoneError.unknown))
|
2020-04-01 21:10:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
case .failure(let error):
|
|
|
|
os_log(.error, log: self.log, "Error occurred getting pending read status records: %@", error.localizedDescription)
|
2020-05-01 23:50:18 +02:00
|
|
|
completion(.failure(CloudKitZoneError.unknown))
|
2020-04-01 21:10:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2020-04-01 18:46:37 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
}
|
2020-04-01 21:10:07 +02:00
|
|
|
|
|
|
|
private extension CloudKitArticlesZoneDelegate {
|
2020-04-30 02:13:50 +02:00
|
|
|
|
|
|
|
func delete(recordKeys: [CloudKitRecordKey], pendingStarredStatusArticleIDs: Set<String>, completion: @escaping () -> Void) {
|
|
|
|
let receivedRecordIDs = recordKeys.filter({ $0.recordType == CloudKitArticlesZone.CloudKitArticleStatus.recordType }).map({ $0.recordID })
|
|
|
|
let receivedArticleIDs = Set(receivedRecordIDs.map({ stripPrefix($0.externalID) }))
|
|
|
|
let deletableArticleIDs = receivedArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
|
|
|
|
2020-04-30 02:17:02 +02:00
|
|
|
guard !deletableArticleIDs.isEmpty else {
|
|
|
|
completion()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2020-04-30 02:13:50 +02:00
|
|
|
database.deleteSelectedForProcessing(Array(deletableArticleIDs)) { _ in
|
|
|
|
self.account?.delete(articleIDs: deletableArticleIDs) { _ in
|
|
|
|
completion()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func update(records: [CKRecord], pendingReadStatusArticleIDs: Set<String>, pendingStarredStatusArticleIDs: Set<String>, completion: @escaping (Result<Void, Error>) -> Void) {
|
2020-04-01 21:10:07 +02:00
|
|
|
|
2020-04-28 01:09:10 +02:00
|
|
|
let receivedUnreadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "0" }).map({ stripPrefix($0.externalID) }))
|
|
|
|
let receivedReadArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.read] == "1" }).map({ stripPrefix($0.externalID) }))
|
|
|
|
let receivedUnstarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.starred] == "0" }).map({ stripPrefix($0.externalID) }))
|
|
|
|
let receivedStarredArticleIDs = Set(records.filter({ $0[CloudKitArticlesZone.CloudKitArticleStatus.Fields.starred] == "1" }).map({ stripPrefix($0.externalID) }))
|
2020-04-03 20:26:08 +02:00
|
|
|
|
2020-04-01 21:10:07 +02:00
|
|
|
let updateableUnreadArticleIDs = receivedUnreadArticleIDs.subtracting(pendingReadStatusArticleIDs)
|
|
|
|
let updateableReadArticleIDs = receivedReadArticleIDs.subtracting(pendingReadStatusArticleIDs)
|
|
|
|
let updateableUnstarredArticleIDs = receivedUnstarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
|
|
|
let updateableStarredArticleIDs = receivedStarredArticleIDs.subtracting(pendingStarredStatusArticleIDs)
|
|
|
|
|
2020-04-30 07:54:41 +02:00
|
|
|
var errorOccurred = false
|
2020-04-02 03:21:14 +02:00
|
|
|
let group = DispatchGroup()
|
|
|
|
|
|
|
|
group.enter()
|
2020-04-30 07:54:41 +02:00
|
|
|
account?.markAsUnread(updateableUnreadArticleIDs) { result in
|
|
|
|
if case .failure(let databaseError) = result {
|
|
|
|
errorOccurred = true
|
|
|
|
os_log(.error, log: self.log, "Error occurred while storing unread statuses: %@", databaseError.localizedDescription)
|
|
|
|
}
|
2020-04-26 03:20:56 +02:00
|
|
|
group.leave()
|
2020-04-02 03:21:14 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
group.enter()
|
2020-04-30 07:54:41 +02:00
|
|
|
account?.markAsRead(updateableReadArticleIDs) { result in
|
|
|
|
if case .failure(let databaseError) = result {
|
|
|
|
errorOccurred = true
|
|
|
|
os_log(.error, log: self.log, "Error occurred while storing read statuses: %@", databaseError.localizedDescription)
|
|
|
|
}
|
2020-04-02 03:21:14 +02:00
|
|
|
group.leave()
|
|
|
|
}
|
|
|
|
|
|
|
|
group.enter()
|
2020-04-30 07:54:41 +02:00
|
|
|
account?.markAsUnstarred(updateableUnstarredArticleIDs) { result in
|
|
|
|
if case .failure(let databaseError) = result {
|
|
|
|
errorOccurred = true
|
|
|
|
os_log(.error, log: self.log, "Error occurred while storing unstarred statuses: %@", databaseError.localizedDescription)
|
|
|
|
}
|
2020-04-02 03:21:14 +02:00
|
|
|
group.leave()
|
|
|
|
}
|
|
|
|
|
|
|
|
group.enter()
|
2020-04-30 07:54:41 +02:00
|
|
|
account?.markAsStarred(updateableStarredArticleIDs) { result in
|
|
|
|
if case .failure(let databaseError) = result {
|
|
|
|
errorOccurred = true
|
|
|
|
os_log(.error, log: self.log, "Error occurred while storing starred statuses: %@", databaseError.localizedDescription)
|
|
|
|
}
|
2020-04-02 03:21:14 +02:00
|
|
|
group.leave()
|
|
|
|
}
|
2020-04-03 18:25:01 +02:00
|
|
|
|
2020-04-26 11:28:42 +02:00
|
|
|
let parsedItems = records.compactMap { makeParsedItem($0) }
|
2020-04-26 03:20:56 +02:00
|
|
|
let webFeedIDsAndItems = Dictionary(grouping: parsedItems, by: { item in item.feedURL } ).mapValues { Set($0) }
|
|
|
|
for (webFeedID, parsedItems) in webFeedIDsAndItems {
|
|
|
|
group.enter()
|
2020-06-17 18:12:30 +02:00
|
|
|
self.account?.update(webFeedID, with: parsedItems, deleteOlder: false) { result in
|
2020-04-30 07:05:37 +02:00
|
|
|
switch result {
|
|
|
|
case .success(let articleChanges):
|
|
|
|
guard let deletes = articleChanges.deletedArticles, !deletes.isEmpty else {
|
|
|
|
group.leave()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
let syncStatuses = deletes.map { SyncStatus(articleID: $0.articleID, key: .deleted, flag: true) }
|
|
|
|
try? self.database.insertStatuses(syncStatuses)
|
|
|
|
group.leave()
|
|
|
|
case .failure(let databaseError):
|
2020-04-30 07:54:41 +02:00
|
|
|
errorOccurred = true
|
2020-04-26 03:20:56 +02:00
|
|
|
os_log(.error, log: self.log, "Error occurred while storing articles: %@", databaseError.localizedDescription)
|
2020-04-30 07:05:37 +02:00
|
|
|
group.leave()
|
2020-04-03 18:25:01 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-04-26 03:20:56 +02:00
|
|
|
|
2020-04-02 03:21:14 +02:00
|
|
|
group.notify(queue: DispatchQueue.main) {
|
2020-04-30 07:54:41 +02:00
|
|
|
if errorOccurred {
|
|
|
|
completion(.failure(CloudKitZoneError.unknown))
|
|
|
|
} else {
|
|
|
|
completion(.success(()))
|
|
|
|
}
|
2020-04-02 03:21:14 +02:00
|
|
|
}
|
2020-04-01 21:10:07 +02:00
|
|
|
}
|
2020-04-28 01:09:10 +02:00
|
|
|
|
|
|
|
func stripPrefix(_ externalID: String) -> String {
|
|
|
|
return String(externalID[externalID.index(externalID.startIndex, offsetBy: 2)..<externalID.endIndex])
|
|
|
|
}
|
2020-04-03 20:26:08 +02:00
|
|
|
|
|
|
|
func makeParsedItem(_ articleRecord: CKRecord) -> ParsedItem? {
|
2020-04-28 01:09:10 +02:00
|
|
|
guard articleRecord.recordType == CloudKitArticlesZone.CloudKitArticle.recordType else {
|
2020-04-26 11:28:42 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2020-04-03 20:26:08 +02:00
|
|
|
var parsedAuthors = Set<ParsedAuthor>()
|
|
|
|
|
|
|
|
let decoder = JSONDecoder()
|
|
|
|
|
|
|
|
if let encodedParsedAuthors = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.parsedAuthors] as? [String] {
|
|
|
|
for encodedParsedAuthor in encodedParsedAuthors {
|
|
|
|
if let data = encodedParsedAuthor.data(using: .utf8), let parsedAuthor = try? decoder.decode(ParsedAuthor.self, from: data) {
|
|
|
|
parsedAuthors.insert(parsedAuthor)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard let uniqueID = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.uniqueID] as? String,
|
2020-04-03 21:19:31 +02:00
|
|
|
let webFeedURL = articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.webFeedURL] as? String else {
|
2020-04-03 20:26:08 +02:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
let parsedItem = ParsedItem(syncServiceID: nil,
|
|
|
|
uniqueID: uniqueID,
|
2020-04-03 21:19:31 +02:00
|
|
|
feedURL: webFeedURL,
|
2020-04-03 20:26:08 +02:00
|
|
|
url: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.url] as? String,
|
|
|
|
externalURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.externalURL] as? String,
|
|
|
|
title: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.title] as? String,
|
2020-04-03 20:42:59 +02:00
|
|
|
language: nil,
|
2020-04-03 20:26:08 +02:00
|
|
|
contentHTML: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentHTML] as? String,
|
|
|
|
contentText: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.contentText] as? String,
|
|
|
|
summary: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.summary] as? String,
|
|
|
|
imageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String,
|
|
|
|
bannerImageURL: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.imageURL] as? String,
|
|
|
|
datePublished: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.datePublished] as? Date,
|
|
|
|
dateModified: articleRecord[CloudKitArticlesZone.CloudKitArticle.Fields.dateModified] as? Date,
|
|
|
|
authors: parsedAuthors,
|
|
|
|
tags: nil,
|
|
|
|
attachments: nil)
|
|
|
|
|
|
|
|
return parsedItem
|
|
|
|
}
|
2020-04-01 21:10:07 +02:00
|
|
|
|
|
|
|
}
|