metatext/DB/Sources/DB/Identity/IdentityDatabase.swift

264 lines
9.9 KiB
Swift
Raw Normal View History

// Copyright © 2020 Metabolist. All rights reserved.
import Combine
2020-09-05 02:31:43 +00:00
import Foundation
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-09-03 03:28:34 +00:00
public enum IdentityDatabaseError: Error {
2020-08-03 15:20:51 +00:00
case identityNotFound
}
2020-09-03 03:28:34 +00:00
public struct IdentityDatabase {
private let databaseQueue: DatabaseQueue
2020-09-08 02:12:38 +00:00
public init(inMemory: Bool, keychain: Keychain.Type) throws {
2020-09-03 03:28:34 +00:00
if inMemory {
databaseQueue = DatabaseQueue()
} else {
2020-09-04 06:12:06 +00:00
let path = try FileManager.default.databaseDirectoryURL(name: Self.name).path
var configuration = Configuration()
2020-09-03 01:14:33 +00:00
2020-09-07 14:25:26 +00:00
configuration.prepareDatabase {
2020-09-04 09:44:25 +00:00
try $0.usePassphrase(try Secrets.databaseKey(identityID: nil, keychain: keychain))
2020-09-04 06:12:06 +00:00
}
databaseQueue = try DatabaseQueue(path: path, configuration: configuration)
}
try Self.migrate(databaseQueue)
}
}
2020-09-03 03:28:34 +00:00
public extension IdentityDatabase {
2020-09-09 05:40:49 +00:00
func createIdentity(id: UUID, url: URL, authenticated: Bool) -> AnyPublisher<Never, Error> {
2020-08-04 20:26:09 +00:00
databaseQueue.writePublisher(
2020-09-05 02:05:15 +00:00
updates: IdentityRecord(
2020-08-04 20:26:09 +00:00
id: id,
url: url,
2020-09-09 05:40:49 +00:00
authenticated: authenticated,
2020-08-04 20:26:09 +00:00
lastUsedAt: Date(),
2020-08-07 01:41:59 +00:00
preferences: Identity.Preferences(),
2020-08-12 07:24:39 +00:00
instanceURI: nil,
2020-08-14 01:24:53 +00:00
lastRegisteredDeviceToken: nil,
pushSubscriptionAlerts: .initial)
2020-08-12 07:24:39 +00:00
.save)
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-02 07:02:03 +00:00
.eraseToAnyPublisher()
}
2020-08-26 09:19:38 +00:00
func deleteIdentity(id: UUID) -> AnyPublisher<Never, Error> {
2020-09-05 02:05:15 +00:00
databaseQueue.writePublisher(updates: IdentityRecord.filter(Column("id") == id).deleteAll)
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-08 07:43:06 +00:00
.eraseToAnyPublisher()
}
2020-08-26 09:19:38 +00:00
func updateLastUsedAt(identityID: UUID) -> AnyPublisher<Never, Error> {
2020-08-04 20:26:09 +00:00
databaseQueue.writePublisher {
2020-09-05 02:05:15 +00:00
try IdentityRecord
2020-08-04 20:26:09 +00:00
.filter(Column("id") == identityID)
.updateAll($0, Column("lastUsedAt").set(to: Date()))
}
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-04 20:26:09 +00:00
.eraseToAnyPublisher()
}
2020-08-26 09:19:38 +00:00
func updateInstance(_ instance: Instance, forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
databaseQueue.writePublisher {
try Identity.Instance(
uri: instance.uri,
streamingAPI: instance.urls.streamingApi,
title: instance.title,
thumbnail: instance.thumbnail)
.save($0)
2020-09-05 02:05:15 +00:00
try IdentityRecord
.filter(Column("id") == identityID)
.updateAll($0, Column("instanceURI").set(to: instance.uri))
}
2020-08-26 09:19:38 +00:00
.ignoreOutput()
.eraseToAnyPublisher()
}
2020-08-26 09:19:38 +00:00
func updateAccount(_ account: Account, forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
2020-08-02 07:02:03 +00:00
databaseQueue.writePublisher(
updates: Identity.Account(
id: account.id,
identityID: identityID,
username: account.username,
2020-08-08 09:10:05 +00:00
displayName: account.displayName,
url: account.url,
avatar: account.avatar,
avatarStatic: account.avatarStatic,
header: account.header,
2020-08-08 09:10:05 +00:00
headerStatic: account.headerStatic,
emojis: account.emojis)
2020-08-02 07:02:03 +00:00
.save)
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-02 07:02:03 +00:00
.eraseToAnyPublisher()
2020-08-07 10:14:14 +00:00
}
2020-09-07 16:33:36 +00:00
func updatePreferences(_ preferences: Mastodon.Preferences,
2020-08-26 09:19:38 +00:00
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
2020-08-07 10:14:14 +00:00
databaseQueue.writePublisher {
2020-09-07 16:33:36 +00:00
guard let storedPreferences = try IdentityRecord.filter(Column("id") == identityID)
.fetchOne($0)?
.preferences else {
throw IdentityDatabaseError.identityNotFound
}
2020-08-07 10:14:14 +00:00
2020-09-07 16:33:36 +00:00
try Self.writePreferences(storedPreferences.updated(from: preferences), id: identityID)($0)
2020-08-07 10:14:14 +00:00
}
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-07 10:14:14 +00:00
.eraseToAnyPublisher()
}
2020-09-07 16:33:36 +00:00
func updatePreferences(_ preferences: Identity.Preferences,
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
databaseQueue.writePublisher(updates: Self.writePreferences(preferences, id: identityID))
.ignoreOutput()
.eraseToAnyPublisher()
}
2020-08-14 21:41:55 +00:00
func updatePushSubscription(alerts: PushSubscription.Alerts,
2020-09-06 21:37:54 +00:00
deviceToken: Data? = nil,
2020-08-26 09:19:38 +00:00
forIdentityID identityID: UUID) -> AnyPublisher<Never, Error> {
2020-08-12 07:24:39 +00:00
databaseQueue.writePublisher {
2020-09-05 02:05:15 +00:00
let data = try IdentityRecord.databaseJSONEncoder(for: "pushSubscriptionAlerts").encode(alerts)
2020-08-12 07:24:39 +00:00
2020-09-05 02:05:15 +00:00
try IdentityRecord
2020-08-12 07:24:39 +00:00
.filter(Column("id") == identityID)
.updateAll($0, Column("pushSubscriptionAlerts").set(to: data))
2020-08-14 21:41:55 +00:00
if let deviceToken = deviceToken {
2020-09-05 02:05:15 +00:00
try IdentityRecord
2020-08-14 21:41:55 +00:00
.filter(Column("id") == identityID)
.updateAll($0, Column("lastRegisteredDeviceToken").set(to: deviceToken))
}
2020-08-12 07:24:39 +00:00
}
2020-08-26 09:19:38 +00:00
.ignoreOutput()
2020-08-12 07:24:39 +00:00
.eraseToAnyPublisher()
}
func identityObservation(id: UUID) -> AnyPublisher<Identity, Error> {
2020-08-02 07:02:03 +00:00
ValueObservation.tracking(
2020-09-05 02:05:15 +00:00
IdentityRecord
2020-08-02 07:02:03 +00:00
.filter(Column("id") == id)
2020-09-05 02:05:15 +00:00
.including(optional: IdentityRecord.instance)
.including(optional: IdentityRecord.account)
2020-08-02 07:02:03 +00:00
.asRequest(of: IdentityResult.self)
.fetchOne)
.removeDuplicates()
.publisher(in: databaseQueue, scheduling: .immediate)
2020-08-03 15:20:51 +00:00
.tryMap {
guard let result = $0 else { throw IdentityDatabaseError.identityNotFound }
2020-08-02 07:02:03 +00:00
return Identity(result: result)
}
.eraseToAnyPublisher()
}
2020-08-04 20:26:09 +00:00
2020-08-08 07:43:06 +00:00
func identitiesObservation() -> AnyPublisher<[Identity], Error> {
ValueObservation.tracking(Self.identitiesRequest().fetchAll)
2020-08-04 20:26:09 +00:00
.removeDuplicates()
2020-09-09 05:40:49 +00:00
.publisher(in: databaseQueue)
2020-08-04 20:26:09 +00:00
.map { $0.map(Identity.init(result:)) }
.eraseToAnyPublisher()
}
func recentIdentitiesObservation(excluding: UUID) -> AnyPublisher<[Identity], Error> {
2020-08-08 07:43:06 +00:00
ValueObservation.tracking(
Self.identitiesRequest()
.filter(Column("id") != excluding)
.limit(9)
.fetchAll)
2020-08-04 20:26:09 +00:00
.removeDuplicates()
2020-09-09 05:40:49 +00:00
.publisher(in: databaseQueue)
2020-08-05 11:48:50 +00:00
.map { $0.map(Identity.init(result:)) }
2020-08-04 20:26:09 +00:00
.eraseToAnyPublisher()
}
2020-08-09 05:37:04 +00:00
func mostRecentlyUsedIdentityIDObservation() -> AnyPublisher<UUID?, Error> {
2020-09-05 02:05:15 +00:00
ValueObservation.tracking(IdentityRecord.select(Column("id")).order(Column("lastUsedAt").desc).fetchOne)
2020-08-09 05:37:04 +00:00
.removeDuplicates()
.publisher(in: databaseQueue, scheduling: .immediate)
.eraseToAnyPublisher()
2020-08-04 20:26:09 +00:00
}
2020-08-12 07:24:39 +00:00
2020-09-06 21:37:54 +00:00
func identitiesWithOutdatedDeviceTokens(deviceToken: Data) -> AnyPublisher<[Identity], Error> {
2020-08-12 07:24:39 +00:00
databaseQueue.readPublisher(
value: Self.identitiesRequest()
.filter(Column("lastRegisteredDeviceToken") != deviceToken)
.fetchAll)
.map { $0.map(Identity.init(result:)) }
.eraseToAnyPublisher()
}
}
private extension IdentityDatabase {
2020-09-04 06:12:06 +00:00
private static let name = "Identity"
2020-08-08 07:43:06 +00:00
private static func identitiesRequest() -> QueryInterfaceRequest<IdentityResult> {
2020-09-05 02:05:15 +00:00
IdentityRecord
2020-08-07 22:31:01 +00:00
.order(Column("lastUsedAt").desc)
2020-09-05 02:05:15 +00:00
.including(optional: IdentityRecord.instance)
.including(optional: IdentityRecord.account)
2020-08-07 22:31:01 +00:00
.asRequest(of: IdentityResult.self)
}
2020-09-07 16:33:36 +00:00
private static func writePreferences(_ preferences: Identity.Preferences, id: UUID) -> (Database) throws -> Void {
{
let data = try IdentityRecord.databaseJSONEncoder(for: "preferences").encode(preferences)
try IdentityRecord
.filter(Column("id") == id)
.updateAll($0, Column("preferences").set(to: data))
}
}
private static func migrate(_ writer: DatabaseWriter) throws {
var migrator = DatabaseMigrator()
migrator.registerMigration("createIdentities") { db in
try db.create(table: "instance", ifNotExists: true) { t in
t.column("uri", .text).notNull().primaryKey(onConflict: .replace)
t.column("streamingAPI", .text)
t.column("title", .text)
t.column("thumbnail", .text)
}
2020-09-05 02:05:15 +00:00
try db.create(table: "identityRecord", ifNotExists: true) { t in
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
t.column("url", .text).notNull()
2020-09-09 05:40:49 +00:00
t.column("authenticated", .boolean).notNull()
2020-08-04 20:26:09 +00:00
t.column("lastUsedAt", .datetime).notNull()
t.column("instanceURI", .text)
.indexed()
.references("instance", column: "uri")
2020-08-07 01:41:59 +00:00
t.column("preferences", .blob).notNull()
2020-08-14 01:24:53 +00:00
t.column("pushSubscriptionAlerts", .blob).notNull()
2020-09-06 21:37:54 +00:00
t.column("lastRegisteredDeviceToken", .blob)
}
try db.create(table: "account", ifNotExists: true) { t in
t.column("id", .text).notNull().primaryKey(onConflict: .replace)
t.column("identityID", .text)
.notNull()
.indexed()
2020-09-05 02:05:15 +00:00
.references("identityRecord", column: "id", onDelete: .cascade)
t.column("username", .text).notNull()
2020-08-08 09:10:05 +00:00
t.column("displayName", .text).notNull()
t.column("url", .text).notNull()
t.column("avatar", .text).notNull()
t.column("avatarStatic", .text).notNull()
t.column("header", .text).notNull()
t.column("headerStatic", .text).notNull()
2020-08-08 09:10:05 +00:00
t.column("emojis", .blob).notNull()
}
}
try migrator.migrate(writer)
}
}