2020-08-18 05:13:37 +00:00
|
|
|
// Copyright © 2020 Metabolist. All rights reserved.
|
|
|
|
|
|
|
|
import Combine
|
2020-09-05 02:31:43 +00:00
|
|
|
import Foundation
|
2020-08-18 05:13:37 +00:00
|
|
|
import GRDB
|
2020-09-04 06:12:06 +00:00
|
|
|
import Keychain
|
2020-08-30 23:33:11 +00:00
|
|
|
import Mastodon
|
2020-09-04 06:12:06 +00:00
|
|
|
import Secrets
|
2020-08-18 05:13:37 +00:00
|
|
|
|
2020-09-03 03:28:34 +00:00
|
|
|
public struct ContentDatabase {
|
2020-09-28 03:11:49 +00:00
|
|
|
private let databaseWriter: DatabaseWriter
|
2020-08-18 05:13:37 +00:00
|
|
|
|
2020-09-04 06:12:06 +00:00
|
|
|
public init(identityID: UUID, inMemory: Bool, keychain: Keychain.Type) throws {
|
2020-09-03 03:28:34 +00:00
|
|
|
if inMemory {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter = DatabaseQueue()
|
2020-08-18 05:13:37 +00:00
|
|
|
} else {
|
2020-09-04 06:12:06 +00:00
|
|
|
let path = try Self.fileURL(identityID: identityID).path
|
|
|
|
var configuration = Configuration()
|
|
|
|
|
2020-09-07 14:25:26 +00:00
|
|
|
configuration.prepareDatabase {
|
2020-09-28 03:11:49 +00:00
|
|
|
try $0.usePassphrase(Secrets.databaseKey(identityID: identityID, keychain: keychain))
|
2020-09-04 06:12:06 +00:00
|
|
|
}
|
|
|
|
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter = try DatabasePool(path: path, configuration: configuration)
|
2020-08-18 05:13:37 +00:00
|
|
|
}
|
|
|
|
|
2020-09-28 06:05:08 +00:00
|
|
|
try migrator.migrate(databaseWriter)
|
2020-09-29 06:35:11 +00:00
|
|
|
try clean()
|
2020-08-18 05:13:37 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-09-03 03:28:34 +00:00
|
|
|
public extension ContentDatabase {
|
2020-09-03 01:14:33 +00:00
|
|
|
static func delete(forIdentityID identityID: UUID) throws {
|
2020-09-15 23:58:04 +00:00
|
|
|
try FileManager.default.removeItem(at: fileURL(identityID: identityID))
|
2020-09-03 01:14:33 +00:00
|
|
|
}
|
|
|
|
|
2020-09-02 09:39:44 +00:00
|
|
|
func insert(status: Status) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher(updates: status.save)
|
2020-09-02 09:39:44 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func insert(statuses: [Status], timeline: Timeline) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-09-02 09:39:44 +00:00
|
|
|
try timeline.save($0)
|
2020-08-18 05:13:37 +00:00
|
|
|
|
|
|
|
for status in statuses {
|
2020-09-02 09:39:44 +00:00
|
|
|
try status.save($0)
|
2020-08-18 05:13:37 +00:00
|
|
|
|
2020-09-02 09:39:44 +00:00
|
|
|
try TimelineStatusJoin(timelineId: timeline.id, statusId: status.id).save($0)
|
2020-09-02 02:39:06 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func insert(context: Context, parentID: String) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-09-02 02:39:06 +00:00
|
|
|
for status in context.ancestors + context.descendants {
|
2020-09-02 09:39:44 +00:00
|
|
|
try status.save($0)
|
2020-09-02 02:39:06 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
for (section, statuses) in [(StatusContextJoin.Section.ancestors, context.ancestors),
|
|
|
|
(StatusContextJoin.Section.descendants, context.descendants)] {
|
|
|
|
for (index, status) in statuses.enumerated() {
|
|
|
|
try StatusContextJoin(
|
|
|
|
parentId: parentID,
|
|
|
|
statusId: status.id,
|
|
|
|
section: section,
|
|
|
|
index: index)
|
|
|
|
.save($0)
|
|
|
|
}
|
|
|
|
|
|
|
|
try StatusContextJoin.filter(
|
2020-09-29 06:06:25 +00:00
|
|
|
StatusContextJoin.Columns.parentId == parentID
|
|
|
|
&& StatusContextJoin.Columns.section == section.rawValue
|
|
|
|
&& !statuses.map(\.id).contains(StatusContextJoin.Columns.statusId))
|
2020-09-02 02:39:06 +00:00
|
|
|
.deleteAll($0)
|
2020-08-18 05:13:37 +00:00
|
|
|
}
|
|
|
|
}
|
2020-08-26 09:19:38 +00:00
|
|
|
.ignoreOutput()
|
2020-08-18 05:13:37 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-09-18 00:16:41 +00:00
|
|
|
func insert(pinnedStatuses: [Status], accountID: String) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-09-18 00:16:41 +00:00
|
|
|
for (index, status) in pinnedStatuses.enumerated() {
|
|
|
|
try status.save($0)
|
|
|
|
|
|
|
|
try AccountPinnedStatusJoin(accountId: accountID, statusId: status.id, index: index).save($0)
|
|
|
|
}
|
|
|
|
|
|
|
|
try AccountPinnedStatusJoin.filter(
|
2020-09-29 06:06:25 +00:00
|
|
|
AccountPinnedStatusJoin.Columns.accountId == accountID
|
|
|
|
&& !pinnedStatuses.map(\.id).contains(AccountPinnedStatusJoin.Columns.statusId))
|
2020-09-18 00:16:41 +00:00
|
|
|
.deleteAll($0)
|
|
|
|
}
|
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-09-28 22:40:03 +00:00
|
|
|
func append(accounts: [Account], toList list: AccountList) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-09-28 22:40:03 +00:00
|
|
|
try list.save($0)
|
|
|
|
|
|
|
|
let count = try list.accounts.fetchCount($0)
|
|
|
|
|
|
|
|
for (index, account) in accounts.enumerated() {
|
2020-09-22 06:53:11 +00:00
|
|
|
try account.save($0)
|
2020-09-28 22:40:03 +00:00
|
|
|
try AccountListJoin(accountId: account.id, listId: list.id, index: count + index).save($0)
|
2020-09-22 06:53:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-09-26 03:28:50 +00:00
|
|
|
func setLists(_ lists: [List]) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-08-29 03:50:58 +00:00
|
|
|
for list in lists {
|
|
|
|
try Timeline.list(list).save($0)
|
|
|
|
}
|
|
|
|
|
2020-10-01 02:35:06 +00:00
|
|
|
try TimelineRecord
|
|
|
|
.filter(!lists.map(\.id).contains(TimelineRecord.Columns.listId)
|
|
|
|
&& TimelineRecord.Columns.listTitle != nil)
|
2020-09-09 05:40:49 +00:00
|
|
|
.deleteAll($0)
|
2020-08-29 03:50:58 +00:00
|
|
|
}
|
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-09-26 03:28:50 +00:00
|
|
|
func createList(_ list: List) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher(updates: Timeline.list(list).save)
|
2020-08-29 03:50:58 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func deleteList(id: String) -> AnyPublisher<Never, Error> {
|
2020-10-01 02:35:06 +00:00
|
|
|
databaseWriter.writePublisher(updates: TimelineRecord.filter(TimelineRecord.Columns.listId == id).deleteAll)
|
2020-08-29 10:26:26 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func setFilters(_ filters: [Filter]) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher {
|
2020-08-29 10:26:26 +00:00
|
|
|
for filter in filters {
|
|
|
|
try filter.save($0)
|
|
|
|
}
|
|
|
|
|
2020-09-29 06:06:25 +00:00
|
|
|
try Filter.filter(!filters.map(\.id).contains(Filter.Columns.id)).deleteAll($0)
|
2020-08-29 10:26:26 +00:00
|
|
|
}
|
2020-08-29 03:50:58 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-08-29 10:26:26 +00:00
|
|
|
func createFilter(_ filter: Filter) -> AnyPublisher<Never, Error> {
|
2020-09-28 03:11:49 +00:00
|
|
|
databaseWriter.writePublisher(updates: filter.save)
|
2020-08-29 10:26:26 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func deleteFilter(id: String) -> AnyPublisher<Never, Error> {
|
2020-09-29 06:06:25 +00:00
|
|
|
databaseWriter.writePublisher(updates: Filter.filter(Filter.Columns.id == id).deleteAll)
|
2020-08-29 10:26:26 +00:00
|
|
|
.ignoreOutput()
|
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-10-02 03:19:14 +00:00
|
|
|
// Awkward maps explained: https://github.com/groue/GRDB.swift#valueobservation-performance
|
|
|
|
|
|
|
|
func observation(timeline: Timeline) -> AnyPublisher<[[Timeline.Item]], Error> {
|
|
|
|
ValueObservation.tracking { db -> ([StatusInfo], [StatusInfo]?, [LoadMore], [Filter]) in
|
|
|
|
let timelineRecord = TimelineRecord(timeline: timeline)
|
|
|
|
let statuses = try timelineRecord.statuses.fetchAll(db)
|
|
|
|
let loadMores = try timelineRecord.loadMores.fetchAll(db)
|
|
|
|
let filters = try Filter.active.fetchAll(db)
|
2020-09-02 02:39:06 +00:00
|
|
|
|
2020-10-01 02:35:06 +00:00
|
|
|
if case let .profile(accountId, profileCollection) = timeline, profileCollection == .statuses {
|
|
|
|
let pinnedStatuses = try AccountRecord.filter(AccountRecord.Columns.id == accountId)
|
2020-10-02 03:19:14 +00:00
|
|
|
.fetchOne(db)?.pinnedStatuses.fetchAll(db)
|
|
|
|
|
|
|
|
return (statuses, pinnedStatuses, loadMores, filters)
|
|
|
|
} else {
|
|
|
|
return (statuses, nil, loadMores, filters)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
.map { statuses, pinnedStatuses, loadMores, filters -> [[Timeline.Item]] in
|
|
|
|
var timelineItems = statuses.filtered(filters: filters, context: timeline.filterContext)
|
|
|
|
.map { Timeline.Item.status(.init(status: .init(info: $0))) }
|
|
|
|
|
|
|
|
for loadMore in loadMores {
|
|
|
|
guard let index = timelineItems.firstIndex(where: {
|
|
|
|
guard case let .status(configuration) = $0 else { return false }
|
|
|
|
|
|
|
|
return loadMore.afterStatusId < configuration.status.id
|
|
|
|
}) else { continue }
|
2020-09-02 02:39:06 +00:00
|
|
|
|
2020-10-02 03:19:14 +00:00
|
|
|
timelineItems.insert(.loadMore(loadMore), at: index)
|
|
|
|
}
|
|
|
|
|
|
|
|
if let pinnedStatuses = pinnedStatuses {
|
|
|
|
return [pinnedStatuses.filtered(filters: filters, context: timeline.filterContext)
|
|
|
|
.map { Timeline.Item.status(.init(status: .init(info: $0), pinned: true)) },
|
|
|
|
timelineItems]
|
2020-10-01 02:35:06 +00:00
|
|
|
} else {
|
2020-10-02 03:19:14 +00:00
|
|
|
return [timelineItems]
|
2020-10-01 02:35:06 +00:00
|
|
|
}
|
2020-09-02 02:39:06 +00:00
|
|
|
}
|
2020-08-19 22:16:03 +00:00
|
|
|
.removeDuplicates()
|
2020-10-01 02:35:06 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-08-19 22:16:03 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
2020-08-29 03:50:58 +00:00
|
|
|
|
2020-10-02 03:19:14 +00:00
|
|
|
func contextObservation(parentID: String) -> AnyPublisher<[[Timeline.Item]], Error> {
|
|
|
|
ValueObservation.tracking { db -> ([[StatusInfo]], [Filter]) in
|
2020-10-01 02:35:06 +00:00
|
|
|
guard let parent = try StatusInfo.request(StatusRecord.filter(StatusRecord.Columns.id == parentID))
|
2020-09-30 04:00:55 +00:00
|
|
|
.fetchOne(db) else {
|
2020-10-02 03:19:14 +00:00
|
|
|
return ([], [])
|
2020-09-30 04:00:55 +00:00
|
|
|
}
|
|
|
|
|
2020-10-01 02:35:06 +00:00
|
|
|
let ancestors = try parent.record.ancestors.fetchAll(db)
|
|
|
|
let descendants = try parent.record.descendants.fetchAll(db)
|
2020-09-30 04:00:55 +00:00
|
|
|
|
2020-10-02 03:19:14 +00:00
|
|
|
return ([ancestors, [parent], descendants], try Filter.active.fetchAll(db))
|
|
|
|
}
|
|
|
|
.map { statusSections, filters in
|
|
|
|
statusSections.map { section in
|
|
|
|
section.filtered(filters: filters, context: .thread)
|
|
|
|
.enumerated()
|
|
|
|
.map { index, statusInfo in
|
|
|
|
let isReplyInContext = index > 0
|
|
|
|
&& section[index - 1].record.id == statusInfo.record.inReplyToId
|
|
|
|
let hasReplyFollowing = section.count > index + 1
|
|
|
|
&& section[index + 1].record.inReplyToId == statusInfo.record.id
|
|
|
|
|
|
|
|
return Timeline.Item.status(
|
|
|
|
.init(status: .init(info: statusInfo),
|
|
|
|
isReplyInContext: isReplyInContext,
|
|
|
|
hasReplyFollowing: hasReplyFollowing))
|
|
|
|
}
|
|
|
|
}
|
2020-09-18 00:16:41 +00:00
|
|
|
}
|
|
|
|
.removeDuplicates()
|
2020-10-01 02:35:06 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-09-18 00:16:41 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-08-29 03:50:58 +00:00
|
|
|
func listsObservation() -> AnyPublisher<[Timeline], Error> {
|
2020-10-01 02:35:06 +00:00
|
|
|
ValueObservation.tracking(TimelineRecord.filter(TimelineRecord.Columns.listId != nil)
|
|
|
|
.order(TimelineRecord.Columns.listTitle.asc)
|
2020-08-29 03:50:58 +00:00
|
|
|
.fetchAll)
|
2020-08-29 10:26:26 +00:00
|
|
|
.removeDuplicates()
|
2020-10-01 02:35:06 +00:00
|
|
|
.map { $0.map(Timeline.init(record:)).compactMap { $0 } }
|
2020-09-28 03:11:49 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-08-29 10:26:26 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
2020-10-02 03:19:14 +00:00
|
|
|
func activeFiltersObservation(date: Date) -> AnyPublisher<[Filter], Error> {
|
2020-09-29 06:06:25 +00:00
|
|
|
ValueObservation.tracking(
|
|
|
|
Filter.filter(Filter.Columns.expiresAt == nil || Filter.Columns.expiresAt > date).fetchAll)
|
2020-08-30 00:32:34 +00:00
|
|
|
.removeDuplicates()
|
2020-10-01 02:35:06 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-08-30 00:32:34 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
|
|
|
|
|
|
|
func expiredFiltersObservation(date: Date) -> AnyPublisher<[Filter], Error> {
|
2020-09-29 06:06:25 +00:00
|
|
|
ValueObservation.tracking(Filter.filter(Filter.Columns.expiresAt < date).fetchAll)
|
2020-08-30 00:32:34 +00:00
|
|
|
.removeDuplicates()
|
2020-09-28 03:11:49 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-08-30 00:32:34 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
2020-09-22 06:53:11 +00:00
|
|
|
|
|
|
|
func accountObservation(id: String) -> AnyPublisher<Account?, Error> {
|
2020-09-29 23:56:09 +00:00
|
|
|
ValueObservation.tracking(AccountInfo.request(AccountRecord.filter(AccountRecord.Columns.id == id)).fetchOne)
|
2020-09-22 06:53:11 +00:00
|
|
|
.removeDuplicates()
|
|
|
|
.map {
|
2020-09-29 23:56:09 +00:00
|
|
|
if let info = $0 {
|
|
|
|
return Account(info: info)
|
2020-09-22 06:53:11 +00:00
|
|
|
} else {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
2020-10-01 02:35:06 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-09-22 06:53:11 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
2020-09-28 22:40:03 +00:00
|
|
|
|
|
|
|
func accountListObservation(_ list: AccountList) -> AnyPublisher<[Account], Error> {
|
|
|
|
ValueObservation.tracking(list.accounts.fetchAll)
|
|
|
|
.removeDuplicates()
|
2020-09-29 23:56:09 +00:00
|
|
|
.map { $0.map(Account.init(info:)) }
|
2020-10-01 02:35:06 +00:00
|
|
|
.publisher(in: databaseWriter)
|
2020-09-28 22:40:03 +00:00
|
|
|
.eraseToAnyPublisher()
|
|
|
|
}
|
2020-08-18 05:13:37 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private extension ContentDatabase {
|
2020-09-03 01:14:33 +00:00
|
|
|
static func fileURL(identityID: UUID) throws -> URL {
|
2020-09-04 06:12:06 +00:00
|
|
|
try FileManager.default.databaseDirectoryURL(name: identityID.uuidString)
|
2020-09-03 01:14:33 +00:00
|
|
|
}
|
|
|
|
|
2020-09-29 06:35:11 +00:00
|
|
|
func clean() throws {
|
|
|
|
try databaseWriter.write {
|
2020-10-01 02:35:06 +00:00
|
|
|
try TimelineRecord.deleteAll($0)
|
2020-09-29 06:35:11 +00:00
|
|
|
try StatusRecord.deleteAll($0)
|
|
|
|
try AccountRecord.deleteAll($0)
|
2020-09-28 22:40:03 +00:00
|
|
|
try AccountList.deleteAll($0)
|
2020-09-29 06:35:11 +00:00
|
|
|
}
|
2020-09-17 03:16:32 +00:00
|
|
|
}
|
2020-08-18 05:13:37 +00:00
|
|
|
}
|