Started adding migration logic for contacts
Updated the getUserHexEncodedPublicKey to take an optional db value so we can retrieve it during the initial migration
This commit is contained in:
parent
72eeb1c796
commit
4ee4b3ffb3
|
@ -231,7 +231,6 @@
|
|||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
||||
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
|
||||
B8AF4BB426A5204600583500 /* SendSeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AF4BB326A5204600583500 /* SendSeedModal.swift */; };
|
||||
B8B32021258B1A650020074B /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32020258B1A650020074B /* Contact.swift */; };
|
||||
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32032258B235D0020074B /* Storage+Contacts.swift */; };
|
||||
B8B3204E258C15C80020074B /* ContactsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B32044258C117C0020074B /* ContactsMigration.swift */; };
|
||||
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B320B6258C30D70020074B /* HTMLMetadata.swift */; };
|
||||
|
@ -749,6 +748,8 @@
|
|||
FD09796727F6B0B600936362 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
|
||||
FD09796927F6BEA700936362 /* SwarmSnode.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796827F6BEA700936362 /* SwarmSnode.swift */; };
|
||||
FD09796B27F6C67500936362 /* Failable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796A27F6C67500936362 /* Failable.swift */; };
|
||||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796D27FA6D0000936362 /* Contact.swift */; };
|
||||
FD09797027FA6FF300936362 /* Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09796F27FA6FF300936362 /* Profile.swift */; };
|
||||
FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */; };
|
||||
FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; };
|
||||
FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; };
|
||||
|
@ -1239,7 +1240,6 @@
|
|||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
|
||||
B8AF4BB326A5204600583500 /* SendSeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendSeedModal.swift; sourceTree = "<group>"; };
|
||||
B8B32020258B1A650020074B /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
|
||||
B8B32032258B235D0020074B /* Storage+Contacts.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Contacts.swift"; sourceTree = "<group>"; };
|
||||
B8B32044258C117C0020074B /* ContactsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContactsMigration.swift; sourceTree = "<group>"; };
|
||||
B8B320B6258C30D70020074B /* HTMLMetadata.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = HTMLMetadata.swift; sourceTree = "<group>"; };
|
||||
|
@ -1793,6 +1793,8 @@
|
|||
FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; };
|
||||
FD09796827F6BEA700936362 /* SwarmSnode.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwarmSnode.swift; sourceTree = "<group>"; };
|
||||
FD09796A27F6C67500936362 /* Failable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Failable.swift; sourceTree = "<group>"; };
|
||||
FD09796D27FA6D0000936362 /* Contact.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Contact.swift; sourceTree = "<group>"; };
|
||||
FD09796F27FA6FF300936362 /* Profile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Profile.swift; sourceTree = "<group>"; };
|
||||
FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = "<group>"; };
|
||||
FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = "<group>"; };
|
||||
|
@ -2406,7 +2408,6 @@
|
|||
B8B3201F258B1A540020074B /* Contacts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B8B32020258B1A650020074B /* Contact.swift */,
|
||||
);
|
||||
path = Contacts;
|
||||
sourceTree = "<group>";
|
||||
|
@ -2687,6 +2688,7 @@
|
|||
children = (
|
||||
FD17D79A27F40ADA00122BE0 /* LegacyDatabase */,
|
||||
FD17D79427F3E03300122BE0 /* Migrations */,
|
||||
FD09796C27FA6C8B00936362 /* Models */,
|
||||
B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */,
|
||||
C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */,
|
||||
C33FDB07255A580700E217F9 /* OWSBackupFragment.m */,
|
||||
|
@ -3604,6 +3606,15 @@
|
|||
path = Utilities;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD09796C27FA6C8B00936362 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FD09796D27FA6D0000936362 /* Contact.swift */,
|
||||
FD09796F27FA6FF300936362 /* Profile.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FD17D79427F3E03300122BE0 /* Migrations */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4854,7 +4865,6 @@
|
|||
C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */,
|
||||
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
|
||||
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */,
|
||||
B8B32021258B1A650020074B /* Contact.swift in Sources */,
|
||||
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
|
||||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */,
|
||||
|
@ -4929,6 +4939,7 @@
|
|||
C3A3A0EC256E1949004D228D /* OWSRecipientIdentity.m in Sources */,
|
||||
B8F5F56525EC8453003BF8D4 /* Notification+Contacts.swift in Sources */,
|
||||
C32C5AB2256DBE8F003C73A2 /* TSMessage.m in Sources */,
|
||||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
||||
C3A3A0FE256E1A3C004D228D /* TSDatabaseSecondaryIndexes.m in Sources */,
|
||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
|
||||
C32C5B1C256DC19D003C73A2 /* TSQuotedMessage.m in Sources */,
|
||||
|
@ -4954,6 +4965,7 @@
|
|||
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
|
||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
|
||||
FD09797027FA6FF300936362 /* Profile.swift in Sources */,
|
||||
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
|
||||
C32C5D23256DD4C0003C73A2 /* Mention.swift in Sources */,
|
||||
C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */,
|
||||
|
|
|
@ -66,6 +66,11 @@ enum MockDataGenerator {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - Generation
|
||||
|
||||
static var printProgress: Bool = true
|
||||
static var hasStartedGenerationThisRun: Bool = false
|
||||
|
||||
static func generateMockData() {
|
||||
// Don't re-generate the mock data if it already exists
|
||||
var existingMockDataThread: TSContactThread?
|
||||
|
@ -74,29 +79,43 @@ enum MockDataGenerator {
|
|||
existingMockDataThread = TSContactThread.getWithContactSessionID("MockDatabaseThread", transaction: transaction)
|
||||
}
|
||||
|
||||
guard existingMockDataThread == nil else { return }
|
||||
guard !hasStartedGenerationThisRun && existingMockDataThread == nil else {
|
||||
hasStartedGenerationThisRun = true
|
||||
return
|
||||
}
|
||||
|
||||
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will also take a long time):
|
||||
/// Generating the threads & content - ~3s per 100
|
||||
/// Writing to the database - ~10s per 1000
|
||||
/// Updating the UI - ~10s per 1000
|
||||
let dmThreadCount: Int = 100
|
||||
let closedGroupThreadCount: Int = 0
|
||||
let openGroupThreadCount: Int = 0
|
||||
let maxMessagesPerThread: Int = 50
|
||||
let dmThreadCount: Int = 1000
|
||||
let closedGroupThreadCount: Int = 50
|
||||
let openGroupThreadCount: Int = 20
|
||||
let messageRangePerThread: [ClosedRange<Int>] = [(0...500)]
|
||||
let dmRandomSeed: Int = 1111
|
||||
let cgRandomSeed: Int = 2222
|
||||
let ogRandomSeed: Int = 3333
|
||||
let logProgress: (String, String) -> () = { title, event in
|
||||
guard printProgress else { return }
|
||||
|
||||
print("[MockDataGenerator] (\(Date().timeIntervalSince1970)) \(title) - \(event)")
|
||||
}
|
||||
|
||||
hasStartedGenerationThisRun = true
|
||||
|
||||
// FIXME: Make sure this data doesn't go off device somehow?
|
||||
Storage.shared.write { anyTransaction in
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else { return }
|
||||
guard let transaction: YapDatabaseReadWriteTransaction = anyTransaction as? YapDatabaseReadWriteTransaction else {
|
||||
return
|
||||
}
|
||||
|
||||
// First create the thread used to indicate that the mock data has been generated
|
||||
logProgress("", "Start")
|
||||
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
|
||||
|
||||
// Multiple spaces to make it look more like words
|
||||
let stringContent: [String] = "abcdefghijklmnopqrstuvwxyz ABCDEFGHIJKLMNOPQRSTUVWXYZ 0123456789 ".map { String($0) }
|
||||
let wordContent: [String] = ["alias", "consequatur", "aut", "perferendis", "sit", "voluptatem", "accusantium", "doloremque", "aperiam", "eaque", "ipsa", "quae", "ab", "illo", "inventore", "veritatis", "et", "quasi", "architecto", "beatae", "vitae", "dicta", "sunt", "explicabo", "aspernatur", "aut", "odit", "aut", "fugit", "sed", "quia", "consequuntur", "magni", "dolores", "eos", "qui", "ratione", "voluptatem", "sequi", "nesciunt", "neque", "dolorem", "ipsum", "quia", "dolor", "sit", "amet", "consectetur", "adipisci", "velit", "sed", "quia", "non", "numquam", "eius", "modi", "tempora", "incidunt", "ut", "labore", "et", "dolore", "magnam", "aliquam", "quaerat", "voluptatem", "ut", "enim", "ad", "minima", "veniam", "quis", "nostrum", "exercitationem", "ullam", "corporis", "nemo", "enim", "ipsam", "voluptatem", "quia", "voluptas", "sit", "suscipit", "laboriosam", "nisi", "ut", "aliquid", "ex", "ea", "commodi", "consequatur", "quis", "autem", "vel", "eum", "iure", "reprehenderit", "qui", "in", "ea", "voluptate", "velit", "esse", "quam", "nihil", "molestiae", "et", "iusto", "odio", "dignissimos", "ducimus", "qui", "blanditiis", "praesentium", "laudantium", "totam", "rem", "voluptatum", "deleniti", "atque", "corrupti", "quos", "dolores", "et", "quas", "molestias", "excepturi", "sint", "occaecati", "cupiditate", "non", "provident", "sed", "ut", "perspiciatis", "unde", "omnis", "iste", "natus", "error", "similique", "sunt", "in", "culpa", "qui", "officia", "deserunt", "mollitia", "animi", "id", "est", "laborum", "et", "dolorum", "fuga", "et", "harum", "quidem", "rerum", "facilis", "est", "et", "expedita", "distinctio", "nam", "libero", "tempore", "cum", "soluta", "nobis", "est", "eligendi", "optio", "cumque", "nihil", "impedit", "quo", "porro", "quisquam", "est", "qui", "minus", "id", "quod", "maxime", "placeat", "facere", "possimus", "omnis", "voluptas", "assumenda", "est", "omnis", "dolor", "repellendus", "temporibus", "autem", "quibusdam", "et", "aut", "consequatur", "vel", "illum", "qui", "dolorem", "eum", "fugiat", "quo", "voluptas", "nulla", "pariatur", "at", "vero", "eos", "et", "accusamus", "officiis", "debitis", "aut", "rerum", "necessitatibus", "saepe", "eveniet", "ut", "et", "voluptates", "repudiandae", "sint", "et", "molestiae", "non", "recusandae", "itaque", "earum", "rerum", "hic", "tenetur", "a", "sapiente", "delectus", "ut", "aut", "reiciendis", "voluptatibus", "maiores", "doloribus", "asperiores", "repellat"]
|
||||
let timestampNow: TimeInterval = Date().timeIntervalSince1970
|
||||
let userSessionId: String = getUserHexEncodedPublicKey()
|
||||
|
||||
|
@ -104,40 +123,46 @@ enum MockDataGenerator {
|
|||
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
|
||||
|
||||
(0..<dmThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
|
||||
logProgress("DM Threads", "Start Generating \(dmThreadCount) threads")
|
||||
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &dmThreadRandomGenerator) })
|
||||
let randomSessionId: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let isMessageRequest: Bool = Bool.random(using: &dmThreadRandomGenerator)
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = (messageRangePerThread[threadIndex % messageRangePerThread.count]
|
||||
.randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the thread
|
||||
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
|
||||
// Generate the contact
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
let contact = Legacy.Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
contact.isApproved = (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator))
|
||||
contact.didApproveMe = (!isMessageRequest && Bool.random(using: &dmThreadRandomGenerator))
|
||||
contact.didApproveMe = (
|
||||
!isMessageRequest &&
|
||||
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
|
||||
)
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("DM Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||
(0..<numMessages).forEach { index in
|
||||
let isIncoming: Bool = (
|
||||
Bool.random(using: &dmThreadRandomGenerator) &&
|
||||
(!isMessageRequest || contact.isApproved)
|
||||
)
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let messageWords: Int = ((1..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (isIncoming ? randomSessionId : userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)) * 1000)
|
||||
message.text = (0..<messageWords)
|
||||
.compactMap { _ in wordContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined(separator: " ")
|
||||
|
||||
if isIncoming {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
|
@ -151,29 +176,36 @@ enum MockDataGenerator {
|
|||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
logProgress("DM Thread \(threadIndex)", "Done")
|
||||
}
|
||||
logProgress("DM Threads", "Done")
|
||||
|
||||
// MARK: - -- Closed Group
|
||||
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
|
||||
logProgress("Closed Group Threads", "Start Generating \(closedGroupThreadCount) threads")
|
||||
|
||||
(0..<closedGroupThreadCount).forEach { threadIndex in
|
||||
logProgress("Closed Group Thread \(threadIndex)", "Start")
|
||||
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
let groupNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let groupName: String = (0..<groupNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
let numGroupMembers: Int = ((0..<5).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = ((0..<maxMessagesPerThread).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numGroupMembers: Int = ((0..<10).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = (messageRangePerThread[threadIndex % messageRangePerThread.count]
|
||||
.randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
var members: [String] = [userSessionId]
|
||||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &cgThreadRandomGenerator) })
|
||||
let randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let contact = Contact(sessionID: randomSessionId)
|
||||
let contact = Legacy.Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined()
|
||||
|
@ -201,14 +233,16 @@ enum MockDataGenerator {
|
|||
Storage.shared.addClosedGroupEncryptionKeyPair(encryptionKeyPair, for: randomGroupPublicKey, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||
|
||||
(0..<numMessages).forEach { index in
|
||||
let messageLength: Int = ((3..<40).randomElement(using: &dmThreadRandomGenerator) ?? 0)
|
||||
let messageWords: Int = ((1..<20).randomElement(using: &cgThreadRandomGenerator) ?? 0)
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)))
|
||||
message.text = (0..<messageLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
|
||||
.joined()
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)) * 1000)
|
||||
message.text = (0..<messageWords)
|
||||
.compactMap { _ in wordContent.randomElement(using: &cgThreadRandomGenerator) }
|
||||
.joined(separator: " ")
|
||||
|
||||
if message.sender != userSessionId {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
|
@ -222,14 +256,17 @@ enum MockDataGenerator {
|
|||
|
||||
// Save the thread
|
||||
thread.save(with: transaction)
|
||||
logProgress("Closed Group Thread \(threadIndex)", "Done")
|
||||
}
|
||||
logProgress("Closed Group Threads", "Done")
|
||||
|
||||
// MARK: - --Open Group
|
||||
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
|
||||
|
||||
(0..<openGroupThreadCount).forEach { threadIndex in
|
||||
let data = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let randomGroupPublicKey: String = try! Identity.generate(from: data).x25519KeyPair.hexEncodedPublicKey
|
||||
logProgress("Open Group Thread \(threadIndex)", "Start")
|
||||
|
||||
let randomGroupPublicKey: String = ((0..<32).map { _ in UInt8.random(in: UInt8.min...UInt8.max, using: &dmThreadRandomGenerator) }).toHexString()
|
||||
let serverNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let roomNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let serverName: String = (0..<serverNameLength)
|
||||
|
@ -238,18 +275,65 @@ enum MockDataGenerator {
|
|||
let roomName: String = (0..<roomNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
let numGroupMembers: Int = ((0..<250).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let numMessages: Int = (messageRangePerThread[threadIndex % messageRangePerThread.count]
|
||||
.randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
|
||||
// Generate the Contacts in the group
|
||||
var members: [String] = [userSessionId]
|
||||
logProgress("Open Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
|
||||
|
||||
(0..<numGroupMembers).forEach { _ in
|
||||
let contactData = Data((0..<16).map { _ in UInt8.random(in: (UInt8.min...UInt8.max), using: &ogThreadRandomGenerator) })
|
||||
let randomSessionId: String = try! Identity.generate(from: contactData).x25519KeyPair.hexEncodedPublicKey
|
||||
let contactNameLength: Int = ((5..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let contact = Legacy.Contact(sessionID: randomSessionId)
|
||||
contact.name = (0..<contactNameLength)
|
||||
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined()
|
||||
Storage.shared.setContact(contact, using: transaction)
|
||||
|
||||
members.append(randomSessionId)
|
||||
}
|
||||
|
||||
// Create the open group model and the thread
|
||||
let openGroup: OpenGroupV2 = OpenGroupV2(server: serverName, room: roomName, name: roomName, publicKey: randomGroupPublicKey, imageID: nil)
|
||||
let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
let model = TSGroupModel(title: openGroup.name, memberIds: [ userSessionId ], image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
||||
let model = TSGroupModel(title: openGroup.name, memberIds: members, image: nil, groupId: groupId, groupType: .openGroup, adminIds: [])
|
||||
|
||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction)
|
||||
thread.shouldBeVisible = true
|
||||
thread.save(with: transaction)
|
||||
|
||||
Storage.shared.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||
logProgress("Open Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||
|
||||
(0..<numMessages).forEach { index in
|
||||
let messageWords: Int = ((1..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
|
||||
let message: VisibleMessage = VisibleMessage()
|
||||
message.sender = (members.randomElement(using: &ogThreadRandomGenerator) ?? userSessionId)
|
||||
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)) * 1000)
|
||||
message.text = (0..<messageWords)
|
||||
.compactMap { _ in wordContent.randomElement(using: &ogThreadRandomGenerator) }
|
||||
.joined(separator: " ")
|
||||
|
||||
if message.sender != userSessionId {
|
||||
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
else {
|
||||
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
|
||||
tsMessage.save(with: transaction)
|
||||
}
|
||||
}
|
||||
|
||||
logProgress("Open Group Thread \(threadIndex)", "Done")
|
||||
}
|
||||
|
||||
logProgress("Open Group Threads", "Done")
|
||||
logProgress("", "Complete")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,127 +0,0 @@
|
|||
import Foundation
|
||||
|
||||
@objc(SNContact)
|
||||
public class Contact : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
@objc public let sessionID: String
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
@objc public var profilePictureURL: String?
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
@objc public var profilePictureFileName: String?
|
||||
/// The key with which the profile is encrypted.
|
||||
@objc public var profileEncryptionKey: OWSAES256Key?
|
||||
/// The ID of the thread associated with this contact.
|
||||
@objc public var threadID: String?
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
@objc public var isTrusted = false
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
@objc public var isApproved = false
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
@objc public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
@objc public var didApproveMe = false
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
@objc public var hasBeenBlocked = false
|
||||
|
||||
// MARK: Name
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
@objc public var name: String?
|
||||
/// The contact's nickname, if the user set one.
|
||||
@objc public var nickname: String?
|
||||
/// The name to display in the UI. For local use only.
|
||||
@objc public func displayName(for context: Context) -> String? {
|
||||
if let nickname = nickname { return nickname }
|
||||
switch context {
|
||||
case .regular: return name
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
guard let name = name else { return nil }
|
||||
let endIndex = sessionID.endIndex
|
||||
let cutoffIndex = sessionID.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(sessionID[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Context
|
||||
@objc(SNContactContext)
|
||||
public enum Context : Int {
|
||||
case regular, openGroup
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public init(sessionID: String) {
|
||||
self.sessionID = sessionID
|
||||
super.init()
|
||||
}
|
||||
|
||||
private override init() { preconditionFailure("Use init(sessionID:) instead.") }
|
||||
|
||||
// MARK: Validation
|
||||
public var isValid: Bool {
|
||||
if profilePictureURL != nil { return (profileEncryptionKey != nil) }
|
||||
if profileEncryptionKey != nil { return (profilePictureURL != nil) }
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String? else { return nil }
|
||||
self.sessionID = sessionID
|
||||
isTrusted = coder.decodeBool(forKey: "isTrusted")
|
||||
if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name }
|
||||
if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
|
||||
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
|
||||
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
|
||||
|
||||
let isBlockedFlag: Bool = coder.decodeBool(forKey: "isBlocked")
|
||||
isApproved = coder.decodeBool(forKey: "isApproved")
|
||||
isBlocked = isBlockedFlag
|
||||
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
|
||||
hasBeenBlocked = (coder.decodeBool(forKey: "hasBeenBlocked") || isBlockedFlag)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(sessionID, forKey: "sessionID")
|
||||
coder.encode(name, forKey: "displayName")
|
||||
coder.encode(nickname, forKey: "nickname")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profilePictureFileName, forKey: "profilePictureFileName")
|
||||
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(isTrusted, forKey: "isTrusted")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
coder.encode(hasBeenBlocked, forKey: "hasBeenBlocked")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? Contact else { return false }
|
||||
return sessionID == other.sessionID
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return sessionID.hash
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String {
|
||||
nickname ?? name ?? sessionID
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
@objc(contextForThread:)
|
||||
public static func context(for thread: TSThread) -> Context {
|
||||
return ((thread as? TSGroupThread)?.isOpenGroup == true) ? .openGroup : .regular
|
||||
}
|
||||
}
|
|
@ -2,5 +2,212 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
enum Legacy {
|
||||
public enum Legacy {
|
||||
// MARK: - Collections and Keys
|
||||
|
||||
internal static let contactThreadPrefix = "c"
|
||||
internal static let threadCollection = "TSThread"
|
||||
internal static let contactCollection = "LokiContactCollection"
|
||||
|
||||
// MARK: - Types
|
||||
|
||||
public typealias Contact = _LegacyContact
|
||||
|
||||
@objc(SNProfile)
|
||||
public class Profile: NSObject, NSCoding {
|
||||
public var displayName: String?
|
||||
public var profileKey: Data?
|
||||
public var profilePictureURL: String?
|
||||
|
||||
internal init(displayName: String, profileKey: Data? = nil, profilePictureURL: String? = nil) {
|
||||
self.displayName = displayName
|
||||
self.profileKey = profileKey
|
||||
self.profilePictureURL = profilePictureURL
|
||||
}
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
if let displayName = coder.decodeObject(forKey: "displayName") as! String? { self.displayName = displayName }
|
||||
if let profileKey = coder.decodeObject(forKey: "profileKey") as! Data? { self.profileKey = profileKey }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(profileKey, forKey: "profileKey")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
}
|
||||
|
||||
public static func fromProto(_ proto: SNProtoDataMessage) -> Profile? {
|
||||
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
|
||||
let profileKey = proto.profileKey
|
||||
let profilePictureURL = profileProto.profilePicture
|
||||
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
return Profile(displayName: displayName, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
||||
} else {
|
||||
return Profile(displayName: displayName)
|
||||
}
|
||||
}
|
||||
|
||||
public func toProto() -> SNProtoDataMessage? {
|
||||
guard let displayName = displayName else {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
let dataMessageProto = SNProtoDataMessage.builder()
|
||||
let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
profileProto.setDisplayName(displayName)
|
||||
if let profileKey = profileKey, let profilePictureURL = profilePictureURL {
|
||||
dataMessageProto.setProfileKey(profileKey)
|
||||
profileProto.setProfilePicture(profilePictureURL)
|
||||
}
|
||||
do {
|
||||
dataMessageProto.setProfile(try profileProto.build())
|
||||
return try dataMessageProto.build()
|
||||
} catch {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
public override var description: String {
|
||||
"""
|
||||
Profile(
|
||||
displayName: \(displayName ?? "null"),
|
||||
profileKey: \(profileKey?.description ?? "null"),
|
||||
profilePictureURL: \(profilePictureURL ?? "null")
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Note: Looks like Swift doesn't expose nested types well (in the `-Swift` header this was
|
||||
// appearing with `SWIFT_CLASS_NAME("Contact")` which conflicts with the new type and has a
|
||||
// different structure) as a result we cannot nest this cleanly
|
||||
@objc(SNContact)
|
||||
public class _LegacyContact: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility
|
||||
@objc public let sessionID: String
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
@objc public var profilePictureURL: String?
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
@objc public var profilePictureFileName: String?
|
||||
/// The key with which the profile is encrypted.
|
||||
@objc public var profileEncryptionKey: OWSAES256Key?
|
||||
/// The ID of the thread associated with this contact.
|
||||
@objc public var threadID: String?
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
@objc public var isTrusted = false
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
@objc public var isApproved = false
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
@objc public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
@objc public var didApproveMe = false
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
@objc public var hasBeenBlocked = false
|
||||
|
||||
// MARK: Name
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
@objc public var name: String?
|
||||
/// The contact's nickname, if the user set one.
|
||||
@objc public var nickname: String?
|
||||
/// The name to display in the UI. For local use only.
|
||||
@objc public func displayName(for context: Context) -> String? {
|
||||
if let nickname = nickname { return nickname }
|
||||
switch context {
|
||||
case .regular: return name
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
guard let name = name else { return nil }
|
||||
let endIndex = sessionID.endIndex
|
||||
let cutoffIndex = sessionID.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(sessionID[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Context
|
||||
@objc(SNContactContext)
|
||||
public enum Context : Int {
|
||||
case regular, openGroup
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public init(sessionID: String) {
|
||||
self.sessionID = sessionID
|
||||
super.init()
|
||||
}
|
||||
|
||||
private override init() { preconditionFailure("Use init(sessionID:) instead.") }
|
||||
|
||||
// MARK: Validation
|
||||
public var isValid: Bool {
|
||||
if profilePictureURL != nil { return (profileEncryptionKey != nil) }
|
||||
if profileEncryptionKey != nil { return (profilePictureURL != nil) }
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
|
||||
public required init?(coder: NSCoder) {
|
||||
guard let sessionID = coder.decodeObject(forKey: "sessionID") as! String? else { return nil }
|
||||
self.sessionID = sessionID
|
||||
isTrusted = coder.decodeBool(forKey: "isTrusted")
|
||||
if let name = coder.decodeObject(forKey: "displayName") as! String? { self.name = name }
|
||||
if let nickname = coder.decodeObject(forKey: "nickname") as! String? { self.nickname = nickname }
|
||||
if let profilePictureURL = coder.decodeObject(forKey: "profilePictureURL") as! String? { self.profilePictureURL = profilePictureURL }
|
||||
if let profilePictureFileName = coder.decodeObject(forKey: "profilePictureFileName") as! String? { self.profilePictureFileName = profilePictureFileName }
|
||||
if let profileEncryptionKey = coder.decodeObject(forKey: "profilePictureEncryptionKey") as! OWSAES256Key? { self.profileEncryptionKey = profileEncryptionKey }
|
||||
if let threadID = coder.decodeObject(forKey: "threadID") as! String? { self.threadID = threadID }
|
||||
|
||||
let isBlockedFlag: Bool = coder.decodeBool(forKey: "isBlocked")
|
||||
isApproved = coder.decodeBool(forKey: "isApproved")
|
||||
isBlocked = isBlockedFlag
|
||||
didApproveMe = coder.decodeBool(forKey: "didApproveMe")
|
||||
hasBeenBlocked = (coder.decodeBool(forKey: "hasBeenBlocked") || isBlockedFlag)
|
||||
}
|
||||
|
||||
public func encode(with coder: NSCoder) {
|
||||
coder.encode(sessionID, forKey: "sessionID")
|
||||
coder.encode(name, forKey: "displayName")
|
||||
coder.encode(nickname, forKey: "nickname")
|
||||
coder.encode(profilePictureURL, forKey: "profilePictureURL")
|
||||
coder.encode(profilePictureFileName, forKey: "profilePictureFileName")
|
||||
coder.encode(profileEncryptionKey, forKey: "profilePictureEncryptionKey")
|
||||
coder.encode(threadID, forKey: "threadID")
|
||||
coder.encode(isTrusted, forKey: "isTrusted")
|
||||
coder.encode(isApproved, forKey: "isApproved")
|
||||
coder.encode(isBlocked, forKey: "isBlocked")
|
||||
coder.encode(didApproveMe, forKey: "didApproveMe")
|
||||
coder.encode(hasBeenBlocked, forKey: "hasBeenBlocked")
|
||||
}
|
||||
|
||||
// MARK: Equality
|
||||
override public func isEqual(_ other: Any?) -> Bool {
|
||||
guard let other = other as? _LegacyContact else { return false }
|
||||
return sessionID == other.sessionID
|
||||
}
|
||||
|
||||
// MARK: Hashing
|
||||
override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:)
|
||||
return sessionID.hash
|
||||
}
|
||||
|
||||
// MARK: Description
|
||||
override public var description: String {
|
||||
nickname ?? name ?? sessionID
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
@objc(contextForThread:)
|
||||
public static func context(for thread: TSThread) -> Context {
|
||||
return ((thread as? TSGroupThread)?.isOpenGroup == true) ? .openGroup : .regular
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,26 +4,40 @@ import Foundation
|
|||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
// TODO: Remove/Move these
|
||||
struct Place: Codable, FetchableRecord, PersistableRecord, ColumnExpressible {
|
||||
static var databaseTableName: String { "place" }
|
||||
|
||||
public enum Columns: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
case name
|
||||
}
|
||||
|
||||
let id: String
|
||||
let name: String
|
||||
}
|
||||
|
||||
enum _001_InitialSetupMigration: Migration {
|
||||
static let identifier: String = "initialSetup"
|
||||
|
||||
static func migrate(_ db: Database) throws {
|
||||
try db.create(table: Place.self) { t in
|
||||
t.column(.id, .text).notNull().primaryKey()
|
||||
try db.create(table: Contact.self) { t in
|
||||
t.column(.id, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
t.column(.isTrusted, .boolean)
|
||||
.notNull()
|
||||
.defaults(to: false)
|
||||
t.column(.isApproved, .boolean)
|
||||
.notNull()
|
||||
.defaults(to: false)
|
||||
t.column(.isBlocked, .boolean)
|
||||
.notNull()
|
||||
.defaults(to: false)
|
||||
t.column(.didApproveMe, .boolean)
|
||||
.notNull()
|
||||
.defaults(to: false)
|
||||
t.column(.hasBeenBlocked, .boolean)
|
||||
.notNull()
|
||||
.defaults(to: false)
|
||||
}
|
||||
|
||||
try db.create(table: Profile.self) { t in
|
||||
t.column(.id, .text)
|
||||
.notNull()
|
||||
.primaryKey()
|
||||
t.column(.name, .text).notNull()
|
||||
t.column(.nickname, .text)
|
||||
t.column(.profilePictureUrl, .text)
|
||||
t.column(.profilePictureFileName, .text)
|
||||
t.column(.profileEncryptionKey, .blob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,8 +7,65 @@ import SessionUtilitiesKit
|
|||
enum _002_YDBToGRDBMigration: Migration {
|
||||
static let identifier: String = "YDBToGRDBMigration"
|
||||
|
||||
// TODO: Autorelease pool???.
|
||||
static func migrate(_ db: Database) throws {
|
||||
// MARK: - Contacts
|
||||
|
||||
var contacts: Set<Legacy.Contact> = []
|
||||
var contactThreadIds: Set<String> = []
|
||||
|
||||
Storage.read { transaction in
|
||||
// Process the Contacts
|
||||
transaction.enumerateRows(inCollection: Legacy.contactCollection) { _, object, _, _ in
|
||||
guard let contact = object as? Legacy.Contact else { return }
|
||||
contacts.insert(contact)
|
||||
}
|
||||
|
||||
// Process the contact threads (only want to create "real" contacts in the new structure)
|
||||
transaction.enumerateKeys(inCollection: Legacy.threadCollection) { key, _ in
|
||||
guard key.starts(with: Legacy.contactThreadPrefix) else { return }
|
||||
contactThreadIds.insert(key)
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the data into GRDB
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
try contacts.forEach { contact in
|
||||
let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
|
||||
let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
|
||||
|
||||
// Determine if this contact is a "real" contact
|
||||
if
|
||||
// TODO: Thread.shouldBeVisible???
|
||||
isCurrentUser ||
|
||||
contactThreadIds.contains(contactThreadId) ||
|
||||
contact.isApproved ||
|
||||
contact.didApproveMe ||
|
||||
contact.isBlocked ||
|
||||
contact.hasBeenBlocked {
|
||||
// Create the contact
|
||||
// TODO: Closed group admins???
|
||||
try Contact(
|
||||
id: contact.sessionID,
|
||||
isTrusted: (isCurrentUser || contact.isTrusted),
|
||||
isApproved: (isCurrentUser || contact.isApproved),
|
||||
isBlocked: (!isCurrentUser && contact.isBlocked),
|
||||
didApproveMe: (isCurrentUser || contact.didApproveMe),
|
||||
hasBeenBlocked: (!isCurrentUser && (contact.hasBeenBlocked || contact.isBlocked))
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Create the "Profile" for the legacy contact
|
||||
try Profile(
|
||||
id: contact.sessionID,
|
||||
name: (contact.name ?? contact.sessionID),
|
||||
nickname: contact.nickname,
|
||||
profilePictureUrl: contact.profilePictureURL,
|
||||
profilePictureFileName: contact.profilePictureFileName,
|
||||
profileEncryptionKey: contact.profileEncryptionKey
|
||||
).insert(db)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Contact: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
|
||||
public static var databaseTableName: String { "contact" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
|
||||
case isTrusted
|
||||
case isApproved
|
||||
case isBlocked
|
||||
case didApproveMe
|
||||
case hasBeenBlocked
|
||||
}
|
||||
|
||||
/// The id for the contact (Note: This could be a sessionId, a blindedId or some future variant)
|
||||
public let id: String
|
||||
|
||||
/// This flag is used to determine whether we should auto-download files sent by this contact.
|
||||
public var isTrusted = false
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are approved
|
||||
public var isApproved = false
|
||||
|
||||
/// This flag is used to determine whether message requests from this contact are blocked
|
||||
public var isBlocked = false {
|
||||
didSet {
|
||||
if isBlocked {
|
||||
hasBeenBlocked = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// This flag is used to determine whether this contact has approved the current users message request
|
||||
public var didApproveMe = false
|
||||
|
||||
/// This flag is used to determine whether this contact has ever been blocked (will be included in the config message if so)
|
||||
public var hasBeenBlocked = false
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct Profile: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, CustomStringConvertible {
|
||||
public static var databaseTableName: String { "profile" }
|
||||
|
||||
public typealias Columns = CodingKeys
|
||||
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
||||
case id
|
||||
|
||||
case name = "displayName"
|
||||
case nickname
|
||||
|
||||
case profilePictureUrl = "profilePictureURL"
|
||||
case profilePictureFileName
|
||||
case profileEncryptionKey
|
||||
}
|
||||
|
||||
/// The id for the user that owns the profile (Note: This could be a sessionId, a blindedId or some future variant)
|
||||
public let id: String
|
||||
|
||||
/// The name of the contact. Use this whenever you need the "real", underlying name of a user (e.g. when sending a message).
|
||||
public var name: String
|
||||
|
||||
/// A custom name for the profile set by the current user
|
||||
public var nickname: String?
|
||||
|
||||
/// The URL from which to fetch the contact's profile picture.
|
||||
public var profilePictureUrl: String?
|
||||
|
||||
/// The file name of the contact's profile picture on local storage.
|
||||
public var profilePictureFileName: String?
|
||||
|
||||
/// The key with which the profile is encrypted.
|
||||
public var profileEncryptionKey: OWSAES256Key?
|
||||
|
||||
// MARK: - Description
|
||||
|
||||
public var description: String {
|
||||
"""
|
||||
Profile(
|
||||
displayName: \(name),
|
||||
profileKey: \(profileEncryptionKey?.keyData.description ?? "null"),
|
||||
profilePictureURL: \(profilePictureUrl ?? "null")
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public extension Profile {
|
||||
init(from decoder: Decoder) throws {
|
||||
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
var profileKey: OWSAES256Key?
|
||||
var profilePictureUrl: String?
|
||||
|
||||
// If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
|
||||
if
|
||||
let profileKeyData: Data = try? container.decode(Data.self, forKey: .profileEncryptionKey),
|
||||
let profilePictureUrlValue: String = try? container.decode(String.self, forKey: .profilePictureUrl)
|
||||
{
|
||||
guard let validProfileKey: OWSAES256Key = OWSAES256Key(data: profileKeyData) else {
|
||||
owsFailDebug("Failed to make profile key for key data")
|
||||
throw GRDBStorageError.decodingFailed
|
||||
}
|
||||
|
||||
profileKey = validProfileKey
|
||||
profilePictureUrl = profilePictureUrlValue
|
||||
}
|
||||
|
||||
self = Profile(
|
||||
id: try container.decode(String.self, forKey: .id),
|
||||
name: try container.decode(String.self, forKey: .name),
|
||||
nickname: try? container.decode(String.self, forKey: .nickname),
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profilePictureFileName: try? container.decode(String.self, forKey: .profilePictureFileName),
|
||||
profileEncryptionKey: profileKey
|
||||
)
|
||||
}
|
||||
|
||||
func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(id, forKey: .id)
|
||||
try container.encode(name, forKey: .name)
|
||||
try container.encode(nickname, forKey: .nickname)
|
||||
try container.encode(profilePictureUrl, forKey: .profilePictureUrl)
|
||||
try container.encode(profilePictureFileName, forKey: .profilePictureFileName)
|
||||
try container.encode(profileEncryptionKey?.keyData, forKey: .profileEncryptionKey)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Protobuf
|
||||
|
||||
public extension Profile {
|
||||
static func fromProto(_ proto: SNProtoDataMessage, id: String) -> Profile? {
|
||||
guard let profileProto = proto.profile, let displayName = profileProto.displayName else { return nil }
|
||||
|
||||
var profileKey: OWSAES256Key?
|
||||
var profilePictureUrl: String?
|
||||
|
||||
// If we have both a `profileKey` and a `profilePicture` then the key MUST be valid
|
||||
if let profileKeyData: Data = proto.profileKey, profileProto.profilePicture != nil {
|
||||
guard let validProfileKey: OWSAES256Key = OWSAES256Key(data: profileKeyData) else {
|
||||
owsFailDebug("Failed to make profile key for key data")
|
||||
return nil
|
||||
}
|
||||
|
||||
profileKey = validProfileKey
|
||||
profilePictureUrl = profileProto.profilePicture
|
||||
}
|
||||
|
||||
return Profile(
|
||||
id: id,
|
||||
name: displayName,
|
||||
nickname: nil,
|
||||
profilePictureUrl: profilePictureUrl,
|
||||
profilePictureFileName: nil,
|
||||
profileEncryptionKey: profileKey
|
||||
)
|
||||
}
|
||||
|
||||
func toProto() -> SNProtoDataMessage? {
|
||||
let dataMessageProto = SNProtoDataMessage.builder()
|
||||
let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
profileProto.setDisplayName(name)
|
||||
|
||||
if let profileKey: OWSAES256Key = profileEncryptionKey, let profilePictureUrl: String = profilePictureUrl {
|
||||
dataMessageProto.setProfileKey(profileKey.keyData)
|
||||
profileProto.setProfilePicture(profilePictureUrl)
|
||||
}
|
||||
|
||||
do {
|
||||
dataMessageProto.setProfile(try profileProto.build())
|
||||
return try dataMessageProto.build()
|
||||
}
|
||||
catch {
|
||||
SNLog("Couldn't construct profile proto from: \(self).")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
public extension Profile {
|
||||
// MARK: - Context
|
||||
|
||||
enum Context: Int {
|
||||
case regular
|
||||
case openGroup
|
||||
}
|
||||
|
||||
/// The name to display in the UI. For local use only.
|
||||
func displayName(for context: Context) -> String? {
|
||||
if let nickname: String = nickname { return nickname }
|
||||
|
||||
switch context {
|
||||
case .regular: return name
|
||||
|
||||
case .openGroup:
|
||||
// In open groups, where it's more likely that multiple users have the same name, we display a bit of the Session ID after
|
||||
// a user's display name for added context.
|
||||
let endIndex = id.endIndex
|
||||
let cutoffIndex = id.index(endIndex, offsetBy: -8)
|
||||
return "\(name) (...\(id[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -10,7 +10,7 @@ public enum Legacy {
|
|||
internal static let snodePoolCollection = "LokiSnodePoolCollection"
|
||||
internal static let onionRequestPathCollection = "LokiOnionRequestPathCollection"
|
||||
internal static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection"
|
||||
internal static let lastMessageHashCollection = "LokiLastMessageHashCollection" // TODO: Remove this one? (make it a query??)
|
||||
internal static let lastMessageHashCollection = "LokiLastMessageHashCollection"
|
||||
internal static let receivedMessagesCollection = "LokiReceivedMessagesCollection"
|
||||
|
||||
// MARK: - Types
|
||||
|
|
|
@ -69,8 +69,8 @@ public extension Identity {
|
|||
}
|
||||
}
|
||||
|
||||
static func fetchUserKeyPair() -> ECKeyPair? {
|
||||
return GRDBStorage.shared.read { db -> ECKeyPair? in
|
||||
static func fetchUserKeyPair(_ db: Database? = nil) -> ECKeyPair? {
|
||||
let fetchKeys: (Database) -> ECKeyPair? = { db in
|
||||
guard
|
||||
let publicKey: Identity = try? Identity.fetchOne(db, id: .x25519PublicKey),
|
||||
let privateKey: Identity = try? Identity.fetchOne(db, id: .x25519PrivateKey)
|
||||
|
@ -83,6 +83,14 @@ public extension Identity {
|
|||
privateKeyData: privateKey.data
|
||||
)
|
||||
}
|
||||
|
||||
if let db: Database = db {
|
||||
return fetchKeys(db)
|
||||
}
|
||||
|
||||
return GRDBStorage.shared.read { db -> ECKeyPair? in
|
||||
return fetchKeys(db)
|
||||
}
|
||||
}
|
||||
|
||||
static func fetchUserEd25519KeyPair() -> Box.KeyPair? {
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import Curve25519Kit
|
||||
|
||||
public enum General {
|
||||
|
@ -20,10 +21,10 @@ public class GeneralUtilities: NSObject {
|
|||
}
|
||||
}
|
||||
|
||||
public func getUserHexEncodedPublicKey() -> String {
|
||||
public func getUserHexEncodedPublicKey(_ db: Database? = nil) -> String {
|
||||
if let cachedKey: String = General.Cache.cachedEncodedPublicKey.wrappedValue { return cachedKey }
|
||||
|
||||
if let keyPair: ECKeyPair = Identity.fetchUserKeyPair() { // Can be nil under some circumstances
|
||||
if let keyPair: ECKeyPair = Identity.fetchUserKeyPair(db) { // Can be nil under some circumstances
|
||||
General.Cache.cachedEncodedPublicKey.mutate { $0 = keyPair.hexEncodedPublicKey }
|
||||
return keyPair.hexEncodedPublicKey
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue