// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation import GRDB import SessionUtil import SessionUtilitiesKit /// This migration goes through the current state of the database and generates config dumps for the user config types enum _014_GenerateInitialUserConfigDumps: Migration { static let target: TargetMigrations.Identifier = .messagingKit static let identifier: String = "GenerateInitialUserConfigDumps" // stringlint:disable static let needsConfigSync: Bool = true static let minExpectedRunDuration: TimeInterval = 4.0 static let fetchedTables: [(TableRecord & FetchableRecord).Type] = [ Identity.self, SessionThread.self, Contact.self, Profile.self, ClosedGroup.self, OpenGroup.self, DisappearingMessagesConfiguration.self, GroupMember.self, ConfigDump.self ] static let createdOrAlteredTables: [(TableRecord & FetchableRecord).Type] = [] static func migrate(_ db: Database) throws { // If we have no ed25519 key then there is no need to create cached dump data guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else { Storage.update(progress: 1, for: self, in: target) // In case this is the last migration return } // Create the initial config state let userPublicKey: String = getUserHexEncodedPublicKey(db) let timestampMs: Int64 = Int64(Date().timeIntervalSince1970 * 1000) SessionUtil.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey) // Retrieve all threads (we are going to base the config dump data on the active // threads rather than anything else in the database) let allThreads: [String: SessionThread] = try SessionThread .fetchAll(db) .reduce(into: [:]) { result, next in result[next.id] = next } // MARK: - UserProfile Config Dump try SessionUtil .config(for: .userProfile, publicKey: userPublicKey) .mutate { conf in try SessionUtil.update( profile: Profile.fetchOrCreateCurrentUser(db), in: conf ) try SessionUtil.updateNoteToSelf( priority: { guard allThreads[userPublicKey]?.shouldBeVisible == true else { return SessionUtil.hiddenPriority } return Int32(allThreads[userPublicKey]?.pinnedPriority ?? 0) }(), in: conf ) if config_needs_dump(conf) { try SessionUtil .createDump( conf: conf, for: .userProfile, publicKey: userPublicKey, timestampMs: timestampMs )? .save(db) } } // MARK: - Contact Config Dump try SessionUtil .config(for: .contacts, publicKey: userPublicKey) .mutate { conf in // Exclude Note to Self, community, group and outgoing blinded message requests let validContactIds: [String] = allThreads .values .filter { thread in thread.variant == .contact && thread.id != userPublicKey && SessionId(from: thread.id)?.prefix == .standard } .map { $0.id } let contactsData: [ContactInfo] = try Contact .filter( Contact.Columns.isBlocked == true || validContactIds.contains(Contact.Columns.id) ) .including(optional: Contact.profile) .asRequest(of: ContactInfo.self) .fetchAll(db) let threadIdsNeedingContacts: [String] = validContactIds .filter { contactId in !contactsData.contains(where: { $0.contact.id == contactId }) } try SessionUtil.upsert( contactData: contactsData .appending( contentsOf: threadIdsNeedingContacts .map { contactId in ContactInfo( contact: Contact.fetchOrCreate(db, id: contactId), profile: nil ) } ) .map { data in SessionUtil.SyncedContactInfo( id: data.contact.id, contact: data.contact, profile: data.profile, priority: { guard allThreads[data.contact.id]?.shouldBeVisible == true else { return SessionUtil.hiddenPriority } return Int32(allThreads[data.contact.id]?.pinnedPriority ?? 0) }(), created: allThreads[data.contact.id]?.creationDateTimestamp ) }, in: conf ) if config_needs_dump(conf) { try SessionUtil .createDump( conf: conf, for: .contacts, publicKey: userPublicKey, timestampMs: timestampMs )? .save(db) } } // MARK: - ConvoInfoVolatile Config Dump try SessionUtil .config(for: .convoInfoVolatile, publicKey: userPublicKey) .mutate { conf in let volatileThreadInfo: [SessionUtil.VolatileThreadInfo] = SessionUtil.VolatileThreadInfo .fetchAll(db, ids: Array(allThreads.keys)) try SessionUtil.upsert( convoInfoVolatileChanges: volatileThreadInfo, in: conf ) if config_needs_dump(conf) { try SessionUtil .createDump( conf: conf, for: .convoInfoVolatile, publicKey: userPublicKey, timestampMs: timestampMs )? .save(db) } } // MARK: - UserGroups Config Dump try SessionUtil .config(for: .userGroups, publicKey: userPublicKey) .mutate { conf in let legacyGroupData: [SessionUtil.LegacyGroupInfo] = try SessionUtil.LegacyGroupInfo.fetchAll(db) let communityData: [SessionUtil.OpenGroupUrlInfo] = try SessionUtil.OpenGroupUrlInfo .fetchAll(db, ids: Array(allThreads.keys)) try SessionUtil.upsert( legacyGroups: legacyGroupData, in: conf ) try SessionUtil.upsert( communities: communityData .map { urlInfo in SessionUtil.CommunityInfo( urlInfo: urlInfo, priority: Int32(allThreads[urlInfo.threadId]?.pinnedPriority ?? 0) ) }, in: conf ) if config_needs_dump(conf) { try SessionUtil .createDump( conf: conf, for: .userGroups, publicKey: userPublicKey, timestampMs: timestampMs )? .save(db) } } // MARK: - Threads try SessionUtil.updatingThreads(db, Array(allThreads.values)) // MARK: - Syncing // Enqueue a config sync job to ensure the generated configs get synced db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey) } Storage.update(progress: 1, for: self, in: target) // In case this is the last migration } struct ContactInfo: FetchableRecord, Decodable, ColumnExpressible { typealias Columns = CodingKeys enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { case contact case profile } let contact: Contact let profile: Profile? } struct GroupInfo: FetchableRecord, Decodable, ColumnExpressible { typealias Columns = CodingKeys enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable { case closedGroup case disappearingMessagesConfiguration case groupMembers } let closedGroup: ClosedGroup let disappearingMessagesConfiguration: DisappearingMessagesConfiguration? let groupMembers: [GroupMember] } }