|  |  |  | // 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] | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | } |