Further work on the DB refactoring

Added the rest of the interaction structure to the database (testing some migration logic now - still needs to be finalised)
Updated the YDBToGRDB migrations to wrap their inserts in autorelease pools (helps memory slightly, unfortunately it's caching the YDB data which uses the most memory but we have opted for speed over RAM at the moment)
Updated the MockDataGenerator so it should now "chunk" the code generation (crazy large figures were previously resulting in excessive memory usage)
This commit is contained in:
Morgan Pretty 2022-04-08 16:56:33 +10:00
parent cf66edb723
commit 4380f1975c
28 changed files with 1610 additions and 399 deletions

View File

@ -753,6 +753,10 @@
FD09798B27FD1CFE00936362 /* Capability.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798A27FD1CFE00936362 /* Capability.swift */; };
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */; };
FD09799127FD499200936362 /* BoxKeyPair+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */; };
FD09799527FE7B8E00936362 /* Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799227FE693200936362 /* Interaction.swift */; };
FD09799727FFA84A00936362 /* RecipientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799627FFA84900936362 /* RecipientState.swift */; };
FD09799927FFC1A300936362 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799827FFC1A300936362 /* Attachment.swift */; };
FD09799B27FFC82D00936362 /* Quote.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD09799A27FFC82D00936362 /* Quote.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 */; };
@ -798,6 +802,7 @@
FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; };
FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; };
FDC4389E27BA2B8A00C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; };
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */; };
FDFD645827EC1F4000808CA1 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645727EC1F4000808CA1 /* Atomic.swift */; };
/* End PBXBuildFile section */
@ -1800,6 +1805,10 @@
FD09798A27FD1CFE00936362 /* Capability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capability.swift; sourceTree = "<group>"; };
FD09798C27FD1D8900936362 /* DisappearingMessageConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DisappearingMessageConfiguration.swift; sourceTree = "<group>"; };
FD09799027FD499200936362 /* BoxKeyPair+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "BoxKeyPair+Utilities.swift"; sourceTree = "<group>"; };
FD09799227FE693200936362 /* Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Interaction.swift; sourceTree = "<group>"; };
FD09799627FFA84900936362 /* RecipientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecipientState.swift; sourceTree = "<group>"; };
FD09799827FFC1A300936362 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
FD09799A27FFC82D00936362 /* Quote.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Quote.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>"; };
@ -1845,6 +1854,7 @@
FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = "<group>"; };
FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = "<group>"; };
FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = "<group>"; };
FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinkPreview.swift; sourceTree = "<group>"; };
FDFD645727EC1F4000808CA1 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = "<group>"; };
FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = "<group>"; };
@ -3610,6 +3620,11 @@
FD09798827FD1C5A00936362 /* OpenGroup.swift */,
FD09798627FD1B7800936362 /* GroupMember.swift */,
FD09798A27FD1CFE00936362 /* Capability.swift */,
FD09799227FE693200936362 /* Interaction.swift */,
FD09799627FFA84900936362 /* RecipientState.swift */,
FD09799827FFC1A300936362 /* Attachment.swift */,
FD09799A27FFC82D00936362 /* Quote.swift */,
FDF0B73B27FFD3D6004C14C5 /* LinkPreview.swift */,
);
path = Models;
sourceTree = "<group>";
@ -4842,6 +4857,7 @@
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */,
FD09799927FFC1A300936362 /* Attachment.swift in Sources */,
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
C352A32F2557549C00338F3E /* NotifyPNServerJob.swift in Sources */,
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */,
@ -4860,6 +4876,7 @@
C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */,
C3227FF6260AAD66006EA627 /* OpenGroupMessageV2.swift in Sources */,
C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */,
FD09799727FFA84A00936362 /* RecipientState.swift in Sources */,
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */,
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
@ -4872,6 +4889,7 @@
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */,
C32C5E75256DE020003C73A2 /* YapDatabaseTransaction+OWS.m in Sources */,
FDF0B73C27FFD3D6004C14C5 /* LinkPreview.swift in Sources */,
FD09797527FAB64300936362 /* ProfileManager.swift in Sources */,
C3BBE0802554CDD70050F1E3 /* Storage.swift in Sources */,
C3DB66AC260ACA42001EFC55 /* OpenGroupManagerV2.swift in Sources */,
@ -4930,6 +4948,7 @@
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift in Sources */,
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
FD09799527FE7B8E00936362 /* Interaction.swift in Sources */,
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */,
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
C32C5EB9256DE130003C73A2 /* OWSQuotedReplyModel+Conversion.swift in Sources */,
@ -4958,6 +4977,7 @@
B8856CA8256F0F42001CE70E /* OWSBackupFragment.m in Sources */,
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
B87EF17126367CF800124B3C /* FileServerAPIV2.swift in Sources */,
FD09799B27FFC82D00936362 /* Quote.swift in Sources */,
C3A3A18A256E2092004D228D /* SignalRecipient.m in Sources */,
C3C2A74425539EB700C340D1 /* Message.swift in Sources */,
C32C5F11256DF79A003C73A2 /* SSKIncrementingIdFinder.swift in Sources */,

View File

@ -396,7 +396,9 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
}
@objc private func handleProfileDidChangeNotification(_ notification: Notification) {
tableView.reloadData() // TODO: Just reload the affected cell
DispatchQueue.main.async {
self.tableView.reloadData() // TODO: Just reload the affected cell
}
}
@objc private func handleLocalProfileDidChangeNotification(_ notification: Notification) {

View File

@ -197,7 +197,7 @@ public class NotificationPresenter: NSObject, NotificationsProtocol {
return
}
let senderName = Profile.displayName(for: incomingMessage.authorId, thread: incomingMessage.thread)
let senderName = Profile.displayName(for: incomingMessage.authorId, thread: thread)
let notificationTitle: String?
var notificationBody: String?

View File

@ -1,5 +1,9 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionSnodeKit
import SessionMessagingKit
@objc(LKBackgroundPoller)
public final class BackgroundPoller : NSObject {
@ -13,6 +17,7 @@ public final class BackgroundPoller : NSObject {
promises = []
promises.append(pollForMessages())
promises.append(contentsOf: pollForClosedGroupMessages())
let v2OpenGroupServers = Set(Storage.shared.getAllV2OpenGroups().values.map { $0.server })
v2OpenGroupServers.forEach { server in
let poller = OpenGroupPollerV2(for: server)

View File

@ -96,6 +96,11 @@ enum MockDataGenerator {
let dmRandomSeed: Int = 1111
let cgRandomSeed: Int = 2222
let ogRandomSeed: Int = 3333
let chunkSize: Int = 1000 // Chunk up the thread writing to prevent memory issues
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()
let logProgress: (String, String) -> () = { title, event in
guard printProgress else { return }
@ -105,240 +110,270 @@ enum MockDataGenerator {
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
}
// First create the thread used to indicate that the mock data has been generated
logProgress("", "Start")
logProgress("", "Start")
// First create the thread used to indicate that the mock data has been generated
Storage.write { transaction in
_ = TSContactThread.getOrCreateThread(withContactSessionID: "MockDatabaseThread", transaction: transaction)
}
// MARK: - -- DM Thread
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
var dmThreadIndex: Int = 0
logProgress("DM Threads", "Start Generating \(dmThreadCount) threads")
while dmThreadIndex < dmThreadCount {
let remainingThreads: Int = (dmThreadCount - dmThreadIndex)
// 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()
// MARK: - -- DM Thread
var dmThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: dmRandomSeed)
(0..<dmThreadCount).forEach { threadIndex in
logProgress("DM Threads", "Start Generating \(dmThreadCount) threads")
(0..<min(chunkSize, remainingThreads)).forEach { index in
let threadIndex: Int = (dmThreadIndex + index)
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 = (messageRangePerThread[threadIndex % messageRangePerThread.count]
.randomElement(using: &dmThreadRandomGenerator) ?? 0)
Storage.write { transaction in
logProgress("DM Thread \(threadIndex)", "Start")
// Generate the thread
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
thread.shouldBeVisible = true
// Generate the contact
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 &&
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
// 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 messageWords: Int = ((1..<20).randomElement(using: &dmThreadRandomGenerator) ?? 0)
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 = (messageRangePerThread[threadIndex % messageRangePerThread.count]
.randomElement(using: &dmThreadRandomGenerator) ?? 0)
let message: VisibleMessage = VisibleMessage()
message.sender = (isIncoming ? randomSessionId : userSessionId)
message.sentTimestamp = UInt64(floor(timestampNow - Double(index * 5)) * 1000)
message.text = (0..<messageWords)
.compactMap { _ in wordContent.randomElement(using: &dmThreadRandomGenerator) }
.joined(separator: " ")
// Generate the thread
let thread: TSContactThread = TSContactThread.getOrCreateThread(withContactSessionID: randomSessionId, transaction: transaction)
thread.shouldBeVisible = true
if isIncoming {
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)
}
}
// 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..<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)
// Generate the contact
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 &&
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
)
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
// 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 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)) * 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)
tsMessage.save(with: transaction)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
}
}
// Save the thread
thread.save(with: transaction)
logProgress("DM Thread \(threadIndex)", "Done")
}
}
dmThreadIndex += chunkSize
}
logProgress("DM Threads", "Done")
// MARK: - -- Closed Group
var cgThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: cgRandomSeed)
var cgThreadIndex: Int = 0
logProgress("Closed Group Threads", "Start Generating \(closedGroupThreadCount) threads")
while cgThreadIndex < closedGroupThreadCount {
let remainingThreads: Int = (closedGroupThreadCount - cgThreadIndex)
(0..<min(chunkSize, remainingThreads)).forEach { index in
let threadIndex: Int = (cgThreadIndex + index)
Storage.write { transaction 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()
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
let numGroupMembers: Int = ((0..<10).randomElement(using: &cgThreadRandomGenerator) ?? 0)
let numMessages: Int = (messageRangePerThread[threadIndex % messageRangePerThread.count]
.randomElement(using: &cgThreadRandomGenerator) ?? 0)
members.append(randomSessionId)
}
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
let group: TSGroupModel = TSGroupModel(
title: groupName,
memberIds: members,
image: nil,
groupId: groupId,
groupType: .closedGroup,
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
)
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
// Add the group to the user's set of public keys to poll for and store the key pair
let encryptionKeyPair = Curve25519.generateKeyPair()
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, 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 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)) * 1000)
message.text = (0..<messageWords)
.compactMap { _ in wordContent.randomElement(using: &cgThreadRandomGenerator) }
.joined(separator: " ")
// Generate the Contacts in the group
var members: [String] = [userSessionId]
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numGroupMembers) Contacts")
if message.sender != userSessionId {
let tsMessage: TSOutgoingMessage = TSOutgoingMessage.from(message, associatedWith: thread, using: transaction)
tsMessage.save(with: transaction)
(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 = Legacy.Contact(sessionID: randomSessionId)
contact.name = (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined()
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
members.append(randomSessionId)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
let groupId: Data = LKGroupUtilities.getEncodedClosedGroupIDAsData(randomGroupPublicKey)
let group: TSGroupModel = TSGroupModel(
title: groupName,
memberIds: members,
image: nil,
groupId: groupId,
groupType: .closedGroup,
adminIds: [members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId]
)
let thread = TSGroupThread.getOrCreateThread(with: group, transaction: transaction)
thread.shouldBeVisible = true
thread.save(with: transaction)
// Add the group to the user's set of public keys to poll for and store the key pair
let encryptionKeyPair = Curve25519.generateKeyPair()
let keyPair: Box.KeyPair = Box.KeyPair(
publicKey: encryptionKeyPair.publicKey.bytes,
secretKey: encryptionKeyPair.privateKey.bytes
)
Storage.shared.addClosedGroupPublicKey(randomGroupPublicKey, using: transaction)
Storage.shared.addClosedGroupEncryptionKeyPair(keyPair, for: randomGroupPublicKey, using: transaction)
Storage.shared.setClosedGroupFormationTimestamp(to: UInt64(floor(timestampNow) * 1000), 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 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)) * 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)
tsMessage.save(with: transaction)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
}
}
// Save the thread
thread.save(with: transaction)
logProgress("Closed Group Thread \(threadIndex)", "Done")
}
// 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
logProgress("Open Group Thread \(threadIndex)", "Start")
cgThreadIndex += chunkSize
}
logProgress("Closed Group Threads", "Done")
// MARK: - --Open Group
var ogThreadRandomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: ogRandomSeed)
var ogThreadIndex: Int = 0
logProgress("Open Group Threads", "Start Generating \(openGroupThreadCount) threads")
while ogThreadIndex < openGroupThreadCount {
let remainingThreads: Int = (openGroupThreadCount - ogThreadIndex)
(0..<min(chunkSize, remainingThreads)).forEach { index in
let threadIndex: Int = (ogThreadIndex + index)
Storage.write { transaction in
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)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
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)
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)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
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")
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: 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..<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()
transaction.setObject(contact, forKey: contact.sessionID, inCollection: Legacy.contactCollection)
(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)
members.append(randomSessionId)
}
else {
let tsMessage: TSIncomingMessage = TSIncomingMessage.from(message, quotedMessage: nil, linkPreview: nil, associatedWith: thread)
tsMessage.save(with: transaction)
}
}
// 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: 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")
logProgress("Open Group Thread \(threadIndex)", "Done")
(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")
ogThreadIndex += chunkSize
}
logProgress("Open Group Threads", "Done")
logProgress("", "Complete")
}
}

View File

@ -16,10 +16,22 @@ public enum Legacy {
public static let contactCollection = "LokiContactCollection"
internal static let threadCollection = "TSThread"
internal static let disappearingMessagesCollection = "OWSDisappearingMessagesConfiguration"
internal static let closedGroupPublicKeyCollection = "SNClosedGroupPublicKeyCollection"
internal static let closedGroupFormationTimestampCollection = "SNClosedGroupFormationTimestampCollection"
internal static let closedGroupZombieMembersCollection = "SNClosedGroupZombieMembersCollection"
internal static let openGroupCollection = "SNOpenGroupCollection"
internal static let openGroupUserCountCollection = "SNOpenGroupUserCountCollection"
internal static let openGroupImageCollection = "SNOpenGroupImageCollection"
internal static let openGroupLastMessageServerIDCollection = "SNLastMessageServerIDCollection"
internal static let openGroupLastDeletionServerIDCollection = "SNLastDeletionServerIDCollection"
internal static let openGroupServerIdToUniqueIdLookupCollection = "SNOpenGroupServerIdToUniqueIdLookup"
internal static let interactionCollection = "TSInteraction"
internal static let attachmentsCollection = "TSAttachements"
internal static let readReceiptManagerCollection = "kOutgoingReadReceiptManagerCollection"
// MARK: - Types
public typealias Contact = _LegacyContact
@ -221,5 +233,87 @@ public class _LegacyContact: NSObject, NSCoding { // NSObject/NSCoding conforman
@objc(contextForThread:)
public static func context(for thread: TSThread) -> Context {
return ((thread as? TSGroupThread)?.isOpenGroup == true) ? .openGroup : .regular
@objc(OWSDisappearingMessagesConfiguration)
public class _LegacyDisappearingMessagesConfiguration: MTLModel {
public let uniqueId: String
@objc public var isEnabled: Bool
@objc public var durationSeconds: UInt32
@objc public var durationIndex: UInt32 = 0
@objc public var durationString: String?
var originalDictionaryValue: [String: Any]?
@objc public var isNewRecord: Bool = false
@objc public static func defaultWith(_ threadId: String) -> Legacy.DisappearingMessagesConfiguration {
return Legacy.DisappearingMessagesConfiguration(
threadId: threadId,
enabled: false,
durationSeconds: (24 * 60 * 60)
)
}
public static func fetch(uniqueId: String, transaction: YapDatabaseReadTransaction? = nil) -> Legacy.DisappearingMessagesConfiguration? {
return nil
}
@objc public static func fetchObject(uniqueId: String) -> Legacy.DisappearingMessagesConfiguration? {
return nil
}
@objc public static func fetchOrBuildDefault(threadId: String, transaction: YapDatabaseReadTransaction) -> Legacy.DisappearingMessagesConfiguration? {
return defaultWith(threadId)
}
@objc public static var validDurationsSeconds: [UInt32] = []
// MARK: - Initialization
init(threadId: String, enabled: Bool, durationSeconds: UInt32) {
self.uniqueId = threadId
self.isEnabled = enabled
self.durationSeconds = durationSeconds
self.isNewRecord = true
super.init()
}
required init(coder: NSCoder) {
self.uniqueId = coder.decodeObject(forKey: "uniqueId") as! String
self.isEnabled = coder.decodeObject(forKey: "enabled") as! Bool
self.durationSeconds = coder.decodeObject(forKey: "durationSeconds") as! UInt32
// Intentionally not calling 'super.init(coder:) here
super.init()
}
required init(dictionary dictionaryValue: [String : Any]!) throws {
fatalError("init(dictionary:) has not been implemented")
}
// MARK: - Dirty Tracking
@objc public override static func storageBehaviorForProperty(withKey propertyKey: String) -> MTLPropertyStorage {
// Don't persist transient properties
if
propertyKey == "TAG" ||
propertyKey == "originalDictionaryValue" ||
propertyKey == "newRecord"
{
return MTLPropertyStorageNone
}
return super.storageBehaviorForProperty(withKey: propertyKey)
}
@objc public var dictionaryValueDidChange: Bool {
return false
}
@objc(saveWithTransaction:)
public func save(with transaction: YapDatabaseReadWriteTransaction) {
self.originalDictionaryValue = self.dictionaryValue
self.isNewRecord = false
}
}

View File

@ -49,15 +49,17 @@ enum _001_InitialSetupMigration: Migration {
t.column(.shouldBeVisible, .boolean).notNull()
t.column(.isPinned, .boolean).notNull()
t.column(.messageDraft, .text)
t.column(.notificationMode, .integer).notNull()
t.column(.notificationMode, .integer)
.notNull()
.defaults(to: SessionThread.NotificationMode.all)
t.column(.mutedUntilTimestamp, .double)
}
try db.create(table: DisappearingMessagesConfiguration.self) { t in
t.column(.id, .text)
t.column(.threadId, .text)
.notNull()
.primaryKey()
.references(SessionThread.self)
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
t.column(.isEnabled, .boolean)
.defaults(to: false)
.notNull()
@ -67,9 +69,10 @@ enum _001_InitialSetupMigration: Migration {
}
try db.create(table: ClosedGroup.self) { t in
t.column(.publicKey, .text)
t.column(.threadId, .text)
.notNull()
.primaryKey()
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
t.column(.name, .text).notNull()
t.column(.formationTimestamp, .double).notNull()
}
@ -77,18 +80,161 @@ enum _001_InitialSetupMigration: Migration {
try db.create(table: ClosedGroupKeyPair.self) { t in
t.column(.publicKey, .text)
.notNull()
.indexed()
.references(ClosedGroup.self)
.indexed() // Quicker querying
.references(ClosedGroup.self, onDelete: .cascade) // Delete if ClosedGroup deleted
t.column(.secretKey, .blob).notNull()
t.column(.receivedTimestamp, .double).notNull()
}
try db.create(table: OpenGroup.self) { t in
t.column(.threadId, .text)
.notNull()
.primaryKey()
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
t.column(.server, .text).notNull()
t.column(.room, .text).notNull()
t.column(.publicKey, .text).notNull()
t.column(.name, .text).notNull()
t.column(.groupDescription, .text)
t.column(.imageId, .text)
t.column(.imageData, .blob)
t.column(.userCount, .integer).notNull()
t.column(.infoUpdates, .integer).notNull()
}
try db.create(table: Capability.self) { t in
t.column(.openGroupId, .text)
.notNull()
.indexed() // Quicker querying
.references(OpenGroup.self, onDelete: .cascade) // Delete if OpenGroup deleted
t.column(.capability, .text).notNull()
t.column(.isMissing, .boolean).notNull()
t.primaryKey([.openGroupId, .capability])
}
try db.create(table: GroupMember.self) { t in
// Note: Not adding a "proper" foreign key constraint as this
// table gets used by both 'OpenGroup' and 'ClosedGroup' types
t.column(.groupId, .text)
.notNull()
.indexed()
.indexed() // Quicker querying
t.column(.profileId, .text).notNull()
t.column(.role, .integer).notNull()
}
try db.create(table: Interaction.self) { t in
t.column(.id, .integer)
.notNull()
.primaryKey(autoincrement: true)
t.column(.serverHash, .text)
t.column(.threadId, .text)
.notNull()
.indexed() // Quicker querying
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
t.column(.authorId, .text)
.notNull()
.indexed() // Quicker querying
.references(Profile.self)
t.column(.variant, .integer).notNull()
t.column(.body, .text)
t.column(.timestampMs, .double)
.notNull()
.indexed() // Quicker querying
t.column(.receivedAtTimestampMs, .double).notNull()
t.column(.expiresInSeconds, .double)
t.column(.expiresStartedAtMs, .double)
t.column(.openGroupInvitationName, .text)
t.column(.openGroupInvitationUrl, .text)
t.column(.openGroupServerMessageId, .integer)
.indexed() // Quicker querying
t.column(.openGroupWhisperMods, .boolean)
.notNull()
.defaults(to: false)
t.column(.openGroupWhisperTo, .text)
// Null is not unique in SQLite which allows us to do this and we do
// a joint constraint with the `threadId` on the off chance there is
// a collision between different hashes on different servers
t.uniqueKey([.threadId, .serverHash])
// The `openGroupServerMessageId` is unique on a per-thread basis
t.uniqueKey([.threadId, .openGroupServerMessageId])
// Note: The timestamp will be unique on a per-message basis so we
// need to add the below unique constraint to handle cases where
// the `serverHash` and `openGroupServerMessageId` can both be null
// to try and prevent duplicate messages (it's theoretically possible
// to get a collision with this constraint but is astronomically unlikely)
t.uniqueKey([.threadId, .serverHash, .openGroupServerMessageId, .timestampMs])
}
try db.create(table: RecipientState.self) { t in
t.column(.interactionId, .integer)
.notNull()
.indexed() // Quicker querying
.references(Interaction.self, onDelete: .cascade) // Delete if interaction deleted
t.column(.recipientId, .text)
.notNull()
.references(Profile.self)
t.column(.state, .integer).notNull()
t.column(.readTimestampMs, .double)
// We want to ensure that a recipient can only have a single state for
// each interaction
t.uniqueKey([.interactionId, .recipientId])
}
try db.create(table: Quote.self) { t in
t.column(.interactionId, .integer)
.notNull()
.primaryKey()
.references(Interaction.self, onDelete: .cascade) // Delete if interaction deleted
t.column(.authorId, .text)
.notNull()
.references(Profile.self)
t.column(.timestampMs, .double).notNull()
t.column(.body, .text)
}
try db.create(table: LinkPreview.self) { t in
t.column(.url, .text)
.notNull()
.primaryKey()
t.column(.interactionId, .integer)
.notNull()
.indexed() // Quicker querying
.references(Interaction.self, onDelete: .cascade) // Delete if interaction deleted
t.column(.title, .text)
}
try db.create(table: Attachment.self) { t in
t.column(.interactionId, .integer)
.notNull()
.indexed() // Quicker querying
.references(Interaction.self, onDelete: .cascade) // Delete if interaction deleted
t.column(.serverId, .text)
t.column(.variant, .integer).notNull()
t.column(.state, .integer).notNull()
t.column(.contentType, .text).notNull()
t.column(.byteCount, .integer)
.notNull()
.defaults(to: 0)
t.column(.creationTimestamp, .double)
t.column(.sourceFilename, .text)
t.column(.downloadUrl, .text)
t.column(.width, .integer)
t.column(.height, .integer)
t.column(.encryptionKey, .blob)
t.column(.digest, .blob)
t.column(.caption, .text)
t.column(.quoteId, .text)
.references(Quote.self, onDelete: .cascade) // Delete if Quote deleted
t.column(.linkPreviewUrl, .text)
.references(LinkPreview.self, onDelete: .cascade) // Delete if LinkPreview deleted
}
}
}

View File

@ -15,14 +15,25 @@ enum _002_YDBToGRDBMigration: Migration {
var shouldFailMigration: Bool = false
var contacts: Set<Legacy.Contact> = []
var contactThreadIds: Set<String> = []
var threads: Set<TSThread> = []
var disappearingMessagesConfiguration: [String: Legacy.DisappearingMessagesConfiguration] = [:]
var closedGroupKeys: [String: (timestamp: TimeInterval, keys: SessionUtilitiesKit.Legacy.KeyPair)] = [:]
var closedGroupName: [String: String] = [:]
var closedGroupFormation: [String: UInt64] = [:]
var closedGroupModel: [String: TSGroupModel] = [:]
var closedGroupZombieMemberIds: [String: Set<String>] = [:]
var openGroupInfo: [String: OpenGroupV2] = [:]
var openGroupUserCount: [String: Int] = [:]
var openGroupImage: [String: Data] = [:]
var openGroupLastMessageServerId: [String: Int64] = [:] // Optional
var openGroupLastDeletionServerId: [String: Int64] = [:] // Optional
var interactions: [String: [TSInteraction]] = [:]
var attachments: [String: TSAttachment] = [:]
var readReceipts: [String: [Double]] = [:]
Storage.read { transaction in
// Process the Contacts
@ -30,7 +41,8 @@ enum _002_YDBToGRDBMigration: Migration {
guard let contact = object as? Legacy.Contact else { return }
contacts.insert(contact)
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - Start")
let userClosedGroupPublicKeys: [String] = transaction.allKeys(inCollection: Legacy.closedGroupPublicKeyCollection)
// Process the threads
@ -52,6 +64,8 @@ enum _002_YDBToGRDBMigration: Migration {
.asType(Legacy.DisappearingMessagesConfiguration.self)
.defaulting(to: Legacy.DisappearingMessagesConfiguration.defaultWith(threadId))
// Process the interactions
// Process group-specific info
guard let groupThread: TSGroupThread = thread as? TSGroupThread else { return }
@ -64,15 +78,14 @@ enum _002_YDBToGRDBMigration: Migration {
guard
let groupIdData: Data = Data(base64Encoded: base64GroupId),
let groupId: String = String(data: groupIdData, encoding: .utf8),
let publicKey: String = groupId.split(separator: "!").last.map({ String($0) }),
let formationTimestamp: UInt64 = transaction.object(forKey: publicKey, inCollection: Legacy.closedGroupFormationTimestampCollection) as? UInt64
let publicKey: String = groupId.split(separator: "!").last.map({ String($0) })
else {
SNLog("Unable to decode Closed Group during migration")
SNLog("[Migration Error] Unable to decode Closed Group")
shouldFailMigration = true
return
}
guard userClosedGroupPublicKeys.contains(publicKey) else {
SNLog("Found unexpected invalid closed group public key during migration")
SNLog("[Migration Error] Found unexpected invalid closed group public key")
shouldFailMigration = true
return
}
@ -81,7 +94,7 @@ enum _002_YDBToGRDBMigration: Migration {
closedGroupName[threadId] = groupThread.name(with: transaction)
closedGroupModel[threadId] = groupThread.groupModel
closedGroupFormation[threadId] = formationTimestamp
closedGroupFormation[threadId] = ((transaction.object(forKey: publicKey, inCollection: Legacy.closedGroupFormationTimestampCollection) as? UInt64) ?? 0)
closedGroupZombieMemberIds[threadId] = transaction.object(
forKey: publicKey,
inCollection: Legacy.closedGroupZombieMembersCollection
@ -96,11 +109,48 @@ enum _002_YDBToGRDBMigration: Migration {
}
}
else if groupThread.isOpenGroup {
guard let openGroup: OpenGroupV2 = transaction.object(forKey: threadId, inCollection: Legacy.openGroupCollection) as? OpenGroupV2 else {
SNLog("[Migration Error] Unable to find open group info")
shouldFailMigration = true
return
}
openGroupInfo[threadId] = openGroup
openGroupUserCount[threadId] = ((transaction.object(forKey: openGroup.id, inCollection: Legacy.openGroupUserCountCollection) as? Int) ?? 0)
openGroupImage[threadId] = transaction.object(forKey: openGroup.id, inCollection: Legacy.openGroupImageCollection) as? Data
openGroupLastMessageServerId[threadId] = transaction.object(forKey: openGroup.id, inCollection: Legacy.openGroupLastMessageServerIDCollection) as? Int64
openGroupLastDeletionServerId[threadId] = transaction.object(forKey: openGroup.id, inCollection: Legacy.openGroupLastDeletionServerIDCollection) as? Int64
}
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process threads - End")
// Process interactions
print("RAWR [\(Date().timeIntervalSince1970)] - Process interactions - Start")
transaction.enumerateKeysAndObjects(inCollection: Legacy.interactionCollection) { _, object, _ in
guard let interaction: TSInteraction = object as? TSInteraction else {
SNLog("[Migration Error] Unable to process interaction")
shouldFailMigration = true
return
}
interactions[interaction.uniqueThreadId] = (interactions[interaction.uniqueThreadId] ?? [])
.appending(interaction)
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process interactions - End")
// Process attachments
print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - Start")
transaction.enumerateKeysAndObjects(inCollection: Legacy.attachmentsCollection) { key, object, _ in
guard let attachment: TSAttachment = object as? TSAttachment else {
SNLog("[Migration Error] Unable to process attachment")
shouldFailMigration = true
return
}
attachments[key] = attachment
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process attachments - End")
}
}
@ -111,46 +161,49 @@ enum _002_YDBToGRDBMigration: Migration {
// MARK: - Insert Contacts
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
try contacts.forEach { contact in
let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
try autoreleasepool {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(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)
// Determine if this contact is a "real" contact (don't want to create contacts for
// every user in the new structure but still want profiles for every user)
if
isCurrentUser ||
contactThreadIds.contains(contactThreadId) ||
contact.isApproved ||
contact.didApproveMe ||
contact.isBlocked ||
contact.hasBeenBlocked {
// Create the contact
// TODO: Closed group admins???
try Contact(
try contacts.forEach { contact in
let isCurrentUser: Bool = (contact.sessionID == currentUserPublicKey)
let contactThreadId: String = TSContactThread.threadID(fromContactSessionID: contact.sessionID)
// Create the "Profile" for the legacy contact
try Profile(
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))
name: (contact.name ?? contact.sessionID),
nickname: contact.nickname,
profilePictureUrl: contact.profilePictureURL,
profilePictureFileName: contact.profilePictureFileName,
profileEncryptionKey: contact.profileEncryptionKey
).insert(db)
// Determine if this contact is a "real" contact (don't want to create contacts for
// every user in the new structure but still want profiles for every user)
if
isCurrentUser ||
contactThreadIds.contains(contactThreadId) ||
contact.isApproved ||
contact.didApproveMe ||
contact.isBlocked ||
contact.hasBeenBlocked {
// Create the contact
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)
}
}
}
// MARK: - Insert Threads
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - Start")
try threads.forEach { thread in
guard let legacyThreadId: String = thread.uniqueId else { return }
@ -161,11 +214,17 @@ enum _002_YDBToGRDBMigration: Migration {
switch thread {
case let groupThread as TSGroupThread:
if groupThread.isOpenGroup {
id = legacyThreadId//openGroup.id
guard let openGroup: OpenGroupV2 = openGroupInfo[legacyThreadId] else {
SNLog("[Migration Error] Open group missing required data")
throw GRDBStorageError.migrationFailed
}
id = openGroup.id
variant = .openGroup
}
else {
guard let publicKey: Data = closedGroupKeys[legacyThreadId]?.keys.publicKey else {
SNLog("[Migration Error] Closed group missing public key")
throw GRDBStorageError.migrationFailed
}
@ -186,71 +245,262 @@ enum _002_YDBToGRDBMigration: Migration {
notificationMode = (thread.isMuted ? .none : .all)
}
try SessionThread(
id: id,
variant: variant,
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
shouldBeVisible: thread.shouldBeVisible,
isPinned: thread.isPinned,
messageDraft: thread.messageDraft,
notificationMode: notificationMode,
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
).insert(db)
// Disappearing Messages Configuration
if let config: Legacy.DisappearingMessagesConfiguration = disappearingMessagesConfiguration[id] {
try DisappearingMessagesConfiguration(
try autoreleasepool {
try SessionThread(
id: id,
isEnabled: config.isEnabled,
durationSeconds: TimeInterval(config.durationSeconds)
variant: variant,
creationDateTimestamp: thread.creationDate.timeIntervalSince1970,
shouldBeVisible: thread.shouldBeVisible,
isPinned: thread.isPinned,
messageDraft: thread.messageDraft,
notificationMode: notificationMode,
mutedUntilTimestamp: thread.mutedUntilDate?.timeIntervalSince1970
).insert(db)
// Disappearing Messages Configuration
if let config: Legacy.DisappearingMessagesConfiguration = disappearingMessagesConfiguration[id] {
try DisappearingMessagesConfiguration(
threadId: id,
isEnabled: config.isEnabled,
durationSeconds: TimeInterval(config.durationSeconds)
).insert(db)
}
// Closed Groups
if (thread as? TSGroupThread)?.isClosedGroup == true {
guard
let keyInfo = closedGroupKeys[legacyThreadId],
let name: String = closedGroupName[legacyThreadId],
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
else {
SNLog("[Migration Error] Closed group missing required data")
throw GRDBStorageError.migrationFailed
}
try ClosedGroup(
threadId: id,
name: name,
formationTimestamp: TimeInterval(formationTimestamp)
).insert(db)
try ClosedGroupKeyPair(
publicKey: keyInfo.keys.publicKey.toHexString(),
secretKey: keyInfo.keys.privateKey,
receivedTimestamp: keyInfo.timestamp
).insert(db)
try groupModel.groupMemberIds.forEach { memberId in
try GroupMember(
groupId: id,
profileId: memberId,
role: .standard
).insert(db)
}
try groupModel.groupAdminIds.forEach { adminId in
try GroupMember(
groupId: id,
profileId: adminId,
role: .admin
).insert(db)
}
try (closedGroupZombieMemberIds[legacyThreadId] ?? []).forEach { zombieId in
try GroupMember(
groupId: id,
profileId: zombieId,
role: .zombie
).insert(db)
}
}
// Open Groups
if (thread as? TSGroupThread)?.isOpenGroup == true {
guard let openGroup: OpenGroupV2 = openGroupInfo[legacyThreadId] else {
SNLog("[Migration Error] Open group missing required data")
throw GRDBStorageError.migrationFailed
}
try OpenGroup(
server: openGroup.server,
room: openGroup.room,
publicKey: openGroup.publicKey,
name: openGroup.name,
groupDescription: nil, // TODO: Add with SOGS V4
imageId: nil, // TODO: Add with SOGS V4
imageData: openGroupImage[legacyThreadId],
userCount: (openGroupUserCount[legacyThreadId] ?? 0), // Will be updated next poll
infoUpdates: 0 // TODO: Add with SOGS V4
).insert(db)
}
}
// Closed Groups
if (thread as? TSGroupThread)?.isClosedGroup == true {
guard
let keyInfo = closedGroupKeys[legacyThreadId],
let name: String = closedGroupName[legacyThreadId],
let groupModel: TSGroupModel = closedGroupModel[legacyThreadId],
let formationTimestamp: UInt64 = closedGroupFormation[legacyThreadId]
else { throw GRDBStorageError.migrationFailed }
try autoreleasepool {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
try ClosedGroup(
publicKey: keyInfo.keys.publicKey.toHexString(),
name: name,
formationTimestamp: TimeInterval(formationTimestamp)
).insert(db)
try ClosedGroupKeyPair(
publicKey: keyInfo.keys.publicKey.toHexString(),
secretKey: keyInfo.keys.privateKey,
receivedTimestamp: keyInfo.timestamp
).insert(db)
try groupModel.groupMemberIds.forEach { memberId in
try GroupMember(
groupId: id,
profileId: memberId,
role: .standard
).insert(db)
}
try groupModel.groupAdminIds.forEach { adminId in
try GroupMember(
groupId: id,
profileId: adminId,
role: .admin
).insert(db)
}
try (closedGroupZombieMemberIds[legacyThreadId] ?? []).forEach { zombieId in
try GroupMember(
groupId: id,
profileId: zombieId,
role: .zombie
).insert(db)
try interactions[legacyThreadId]?
.sorted(by: { lhs, rhs in lhs.sortId < rhs.sortId }) // Maintain sort order
.forEach { legacyInteraction in
let serverHash: String?
let variant: Interaction.Variant
let authorId: String
let body: String?
let expiresInSeconds: UInt32?
let expiresStartedAtMs: UInt64?
let openGroupInvitationName: String?
let openGroupInvitationUrl: String?
let openGroupServerMessageId: UInt64?
let recipientStateMap: [String: TSOutgoingMessageRecipientState]?
let attachmentIds: [String]
// Handle the common 'TSMessage' values first
if let legacyMessage: TSMessage = legacyInteraction as? TSMessage {
serverHash = legacyMessage.serverHash
openGroupInvitationName = legacyMessage.openGroupInvitationName
openGroupInvitationUrl = legacyMessage.openGroupInvitationURL
// The legacy code only considered '!= 0' ids as valid so set those
// values to be null to avoid the unique constraint (it's also more
// correct for the values to be null)
openGroupServerMessageId = (legacyMessage.openGroupServerMessageID == 0 ?
nil :
legacyMessage.openGroupServerMessageID
)
attachmentIds = try legacyMessage.attachmentIds.map { legacyId in
guard let attachmentId: String = legacyId as? String else {
SNLog("[Migration Error] Unable to process attachment id")
throw GRDBStorageError.migrationFailed
}
return attachmentId
}
}
else {
serverHash = nil
openGroupInvitationName = nil
openGroupInvitationUrl = nil
openGroupServerMessageId = nil
attachmentIds = []
}
// Then handle the behaviours for each message type
switch legacyInteraction {
case let incomingMessage as TSIncomingMessage:
variant = .standardIncoming
authorId = incomingMessage.authorId
body = incomingMessage.body
expiresInSeconds = incomingMessage.expiresInSeconds
expiresStartedAtMs = incomingMessage.expireStartedAt
recipientStateMap = [:]
case let outgoingMessage as TSOutgoingMessage:
variant = .standardOutgoing
authorId = currentUserPublicKey
body = outgoingMessage.body
expiresInSeconds = outgoingMessage.expiresInSeconds
expiresStartedAtMs = outgoingMessage.expireStartedAt
recipientStateMap = outgoingMessage.recipientStateMap
case let infoMessage as TSInfoMessage:
authorId = currentUserPublicKey
body = ((infoMessage.body ?? "").isEmpty ?
infoMessage.customMessage :
infoMessage.body
)
expiresInSeconds = nil // Info messages don't expire
expiresStartedAtMs = nil // Info messages don't expire
recipientStateMap = [:]
switch infoMessage.messageType {
case .groupCreated: variant = .infoClosedGroupCreated
case .groupUpdated: variant = .infoClosedGroupUpdated
case .groupCurrentUserLeft: variant = .infoClosedGroupCurrentUserLeft
case .disappearingMessagesUpdate: variant = .infoDisappearingMessagesUpdate
case .messageRequestAccepted: variant = .infoMessageRequestAccepted
case .screenshotNotification: variant = .infoScreenshotNotification
case .mediaSavedNotification: variant = .infoMediaSavedNotification
@unknown default:
SNLog("[Migration Error] Unsupported info message type")
throw GRDBStorageError.migrationFailed
}
default:
SNLog("[Migration Error] Unsupported interaction type")
throw GRDBStorageError.migrationFailed
}
// Insert the data
let interaction = try Interaction(
serverHash: serverHash,
threadId: id,
authorId: authorId,
variant: variant,
body: body,
timestampMs: Double(legacyInteraction.timestamp),
receivedAtTimestampMs: Double(legacyInteraction.receivedAtTimestamp),
expiresInSeconds: expiresInSeconds.map { TimeInterval($0) },
expiresStartedAtMs: expiresStartedAtMs.map { Double($0) },
openGroupInvitationName: openGroupInvitationName,
openGroupInvitationUrl: openGroupInvitationUrl,
openGroupServerMessageId: openGroupServerMessageId.map { Int64($0) },
openGroupWhisperMods: false, // TODO: This
openGroupWhisperTo: nil // TODO: This
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
SNLog("[Migration Error] Failed to insert interaction")
throw GRDBStorageError.migrationFailed
}
try recipientStateMap?.forEach { recipientId, legacyState in
try RecipientState(
interactionId: interactionId,
recipientId: recipientId,
state: {
switch legacyState.state {
case .failed: return .failed
case .sending: return .sending
case .skipped: return .skipped
case .sent: return .sent
@unknown default: throw GRDBStorageError.migrationFailed
}
}(),
readTimestampMs: legacyState.readTimestamp?.doubleValue
).insert(db)
}
try attachmentIds.forEach { attachmentId in
guard let attachment: TSAttachment = attachments[attachmentId] else {
SNLog("[Migration Error] Unsupported interaction type")
throw GRDBStorageError.migrationFailed
}
try Attachment(
interactionId: interactionId,
serverId: "\(attachment.serverId)",
variant: (attachment.isVoiceMessage ? .voiceMessage : .standard),
state: .pending, // TODO: This
contentType: attachment.contentType,
byteCount: UInt(attachment.byteCount),
creationTimestamp: 0, // TODO: This
sourceFilename: attachment.sourceFilename,
downloadUrl: attachment.downloadURL,
width: 0, // TODO: This attachment.mediaSize,
height: 0, // TODO: This attachment.mediaSize,
encryptionKey: attachment.encryptionKey,
digest: nil, // TODO: This attachment.digest,
caption: attachment.caption,
quoteId: nil, // TODO: THis
linkPreviewUrl: nil // TODO: This
).insert(db)
}
}
}
}
print("RAWR [\(Date().timeIntervalSince1970)] - Process thread inserts - End")
print("RAWR Done!!!")
}
}

View File

@ -0,0 +1,131 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct Attachment: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "attachment" }
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
internal static let quoteForeignKey = ForeignKey([Columns.quoteId], to: [Quote.Columns.interactionId])
internal static let linkPreviewForeignKey = ForeignKey(
[Columns.linkPreviewUrl],
to: [LinkPreview.Columns.url]
)
private static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
private static let quote = belongsTo(Quote.self, using: quoteForeignKey)
private static let linkPreview = belongsTo(LinkPreview.self, using: linkPreviewForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case interactionId
case serverId
case variant
case state
case contentType
case byteCount
case creationTimestamp
case sourceFilename
case downloadUrl
case width
case height
case encryptionKey
case digest
case caption
case quoteId
case linkPreviewUrl
}
public enum Variant: Int, Codable, DatabaseValueConvertible {
case standard
case voiceMessage
}
public enum State: Int, Codable, DatabaseValueConvertible {
case pending
case downloading
case downloaded
case uploading
case uploaded
case failed
}
/// The id for the interaction this attachment belongs to
public let interactionId: Int64
/// The id for the attachment returned by the server
///
/// This will be null for attachments which havent completed uploading
///
/// **Note:** This value is not unique as multiple SOGS could end up having the same file id
public let serverId: String?
/// The type of this attachment, used to distinguish logic handling
public let variant: Variant
/// The current state of the attachment
public let state: State
/// The MIMEType for the attachment
public let contentType: String
/// The size of the attachment in bytes
///
/// **Note:** This may be `0` for some legacy attachments
public let byteCount: UInt
/// Timestamp in seconds since epoch for when this attachment was created
///
/// **Uploaded:** This will be the timestamp the file finished uploading
/// **Downloaded:** This will be the timestamp the file finished downloading
public let creationTimestamp: TimeInterval?
/// Represents the "source" filename sent or received in the protos, not the filename on disk
public let sourceFilename: String?
/// The url the attachment can be downloaded from, this will be `null` for attachments which havent yet been uploaded
///
/// **Note:** The url is a fully constructed url but the clients just extract the id from the end of the url to perform the actual download
public let downloadUrl: String?
/// The width of the attachment, this will be `null` for non-visual attachment types
public let width: UInt?
/// The height of the attachment, this will be `null` for non-visual attachment types
public let height: UInt?
/// The key used to decrypt the attachment
public let encryptionKey: Data?
/// The computed digest for the attachment (generated from `iv || encrypted data || hmac`)
public let digest: Data?
/// Caption for the attachment
public let caption: String?
/// The id for the QuotedMessage if this attachment belongs to one
///
/// **Note:** If this value is present then this attachment shouldn't be returned as a
/// standard attachment for the interaction
public let quoteId: String?
/// The id for the LinkPreview if this attachment belongs to one
///
/// **Note:** If this value is present then this attachment shouldn't be returned as a
/// standard attachment for the interaction
public let linkPreviewUrl: String?
// MARK: - Relationships
public var interaction: QueryInterfaceRequest<Interaction> {
request(for: Attachment.interaction)
}
public var quote: QueryInterfaceRequest<Quote> {
request(for: Attachment.quote)
}
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
request(for: Attachment.linkPreview)
}
}

View File

@ -6,17 +6,23 @@ import SessionUtilitiesKit
public struct Capability: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "capability" }
internal static let openGroupForeignKey = ForeignKey([Columns.openGroupId], to: [OpenGroup.Columns.threadId])
private static let openGroup = belongsTo(OpenGroup.self, using: openGroupForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case server
case room
case openGroupId
case capability
case isMissing
}
public let server: String
public let room: String
public let openGroupId: String
public let capability: String
public let isMissing: Bool
// MARK: - Relationships
public var openGroup: QueryInterfaceRequest<OpenGroup> {
request(for: Capability.openGroup)
}
}

View File

@ -6,22 +6,37 @@ import SessionUtilitiesKit
public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "closedGroup" }
static let keyPairs = hasMany(ClosedGroupKeyPair.self)
static let members = hasMany(GroupMember.self)
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
private static let keyPairs = hasMany(
ClosedGroupKeyPair.self,
using: ClosedGroupKeyPair.closedGroupForeignKey
)
private static let members = hasMany(GroupMember.self, using: GroupMember.closedGroupForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case publicKey
case threadId
case name
case formationTimestamp
}
public var id: String { publicKey }
public var id: String { threadId } // Identifiable
public var publicKey: String { threadId }
public let publicKey: String
/// The id for the thread this closed group belongs to
///
/// **Note:** This value will always be publicKey for the closed group
public let threadId: String
public let name: String
public let formationTimestamp: TimeInterval
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
request(for: ClosedGroup.thread)
}
public var keyPairs: QueryInterfaceRequest<ClosedGroupKeyPair> {
request(for: ClosedGroup.keyPairs)
}
@ -45,4 +60,14 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
request(for: ClosedGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
}
// MARK: - Custom Database Interaction
public func delete(_ db: Database) throws -> Bool {
// Delete all 'GroupMember' records associated with this ClosedGroup (can't
// have a proper ForeignKey constraint as 'GroupMember' is reused for the
// 'OpenGroup' table as well)
try request(for: ClosedGroup.members).deleteAll(db)
return try performDelete(db)
}
}

View File

@ -6,6 +6,11 @@ import SessionUtilitiesKit
public struct ClosedGroupKeyPair: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "closedGroupKeyPair" }
internal static let closedGroupForeignKey = ForeignKey(
[Columns.publicKey],
to: [ClosedGroup.Columns.threadId]
)
private static let closedGroup = belongsTo(ClosedGroup.self, using: closedGroupForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -19,4 +24,10 @@ public struct ClosedGroupKeyPair: Codable, Identifiable, FetchableRecord, Persis
public let publicKey: String
public let secretKey: Data
public let receivedTimestamp: TimeInterval
// MARK: - Relationships
public var closedGroup: QueryInterfaceRequest<ClosedGroup> {
request(for: ClosedGroupKeyPair.closedGroup)
}
}

View File

@ -6,17 +6,27 @@ import SessionUtilitiesKit
public struct DisappearingMessagesConfiguration: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "disappearingMessagesConfiguration" }
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case id
case threadId
case isEnabled
case durationSeconds
}
public var id: String { threadId } // Identifiable
public let id: String
public let threadId: String
public let isEnabled: Bool
public let durationSeconds: TimeInterval
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
request(for: DisappearingMessagesConfiguration.thread)
}
}
// MARK: - Convenience

View File

@ -6,6 +6,12 @@ import SessionUtilitiesKit
public struct GroupMember: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "groupMember" }
internal static let openGroupForeignKey = ForeignKey([Columns.groupId], to: [OpenGroup.Columns.threadId])
internal static let closedGroupForeignKey = ForeignKey([Columns.groupId], to: [ClosedGroup.Columns.threadId])
internal static let profileForeignKey = ForeignKey([Columns.profileId], to: [Profile.Columns.id])
private static let openGroup = belongsTo(OpenGroup.self, using: openGroupForeignKey)
private static let closedGroup = belongsTo(ClosedGroup.self, using: closedGroupForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -24,4 +30,18 @@ public struct GroupMember: Codable, FetchableRecord, PersistableRecord, TableRec
public let groupId: String
public let profileId: String
public let role: Role
// MARK: - Relationships
public var openGroup: QueryInterfaceRequest<OpenGroup> {
request(for: GroupMember.openGroup)
}
public var closedGroup: QueryInterfaceRequest<ClosedGroup> {
request(for: GroupMember.closedGroup)
}
public var profile: QueryInterfaceRequest<Profile> {
request(for: GroupMember.profile)
}
}

View File

@ -0,0 +1,211 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct Interaction: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "interaction" }
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
private static let attachments = hasMany(Attachment.self, using: Attachment.interactionForeignKey)
private static let quote = hasOne(Quote.self, using: Quote.interactionForeignKey)
private static let linkPreview = hasOne(LinkPreview.self, using: LinkPreview.interactionForeignKey)
private static let recipientStates = hasMany(RecipientState.self, using: RecipientState.interactionForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case id
case serverHash
case threadId
case authorId
case variant
case body
case timestampMs
case receivedAtTimestampMs
case expiresInSeconds
case expiresStartedAtMs
case openGroupInvitationName
case openGroupInvitationUrl
// Open Group specific properties
case openGroupServerMessageId
case openGroupWhisperMods
case openGroupWhisperTo
}
public enum Variant: Int, Codable, DatabaseValueConvertible {
case standardIncoming
case standardOutgoing
// Info Message Types (spacing the values out to make it easier to extend)
case infoClosedGroupCreated = 1000
case infoClosedGroupUpdated
case infoClosedGroupCurrentUserLeft
case infoDisappearingMessagesUpdate = 2000
case infoScreenshotNotification = 3000
case infoMediaSavedNotification
case infoMessageRequestAccepted = 4000
}
/// The `id` value is auto incremented by the database, if the `Interaction` hasn't been inserted into
/// the database yet this value will be `nil`
public var id: Int64? = nil
/// The hash returned by the server when this message was created on the server
///
/// **Note:** This will only be populated for `standardIncoming`/`standardOutgoing` interactions
/// from either `contact` or `closedGroup` threads
public let serverHash: String?
/// The id of the thread that this interaction belongs to (used to expose the `thread` variable)
private let threadId: String
/// The id of the user who sent the message, also used to expose the `profile` variable)
public let authorId: String
/// The type of interaction
public let variant: Variant
/// The body of this interaction
public let body: String?
/// When the interaction was created in milliseconds since epoch
public let timestampMs: Double
/// When the interaction was received in milliseconds since epoch
public let receivedAtTimestampMs: Double
/// The number of seconds until this message should expire
public fileprivate(set) var expiresInSeconds: TimeInterval? = nil
/// The timestamp in milliseconds since 1970 at which this messages expiration timer started counting
/// down (this is stored in order to allow the `expiresInSeconds` value to be updated before a
/// message has expired)
public fileprivate(set) var expiresStartedAtMs: Double? = nil
/// When sending an Open Group invitation this will be populated with the name of the open group
public let openGroupInvitationName: String?
/// When sending an Open Group invitation this will be populated with the url of the open group
public let openGroupInvitationUrl: String?
// Open Group specific properties
/// The `openGroupServerMessageId` value will only be set for messages from SOGS
public fileprivate(set) var openGroupServerMessageId: Int64? = nil
/// This flag indicates whether this interaction is a whisper to the mods of an Open Group
public let openGroupWhisperMods: Bool
/// This value is the id of the user within an Open Group who is the target of this whisper interaction
public let openGroupWhisperTo: String?
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
request(for: Interaction.thread)
}
public var profile: QueryInterfaceRequest<Profile> {
request(for: Interaction.profile)
}
public var attachments: QueryInterfaceRequest<Attachment> {
request(for: Interaction.attachments)
.filter(
Attachment.Columns.quoteId == nil &&
Attachment.Columns.linkPreviewUrl == nil
)
}
public var quote: QueryInterfaceRequest<Quote> {
request(for: Interaction.quote)
}
public var linkPreview: QueryInterfaceRequest<LinkPreview> {
request(for: Interaction.linkPreview)
}
public var recipientStates: QueryInterfaceRequest<RecipientState> {
request(for: Interaction.recipientStates)
}
// MARK: - Initialization
// TODO: Do we actually want these values to have defaults? (check how messages are getting created - convenience constructors??)
init(
serverHash: String?,
threadId: String,
authorId: String,
variant: Variant,
body: String?,
timestampMs: Double,
receivedAtTimestampMs: Double,
expiresInSeconds: TimeInterval?,
expiresStartedAtMs: Double?,
openGroupInvitationName: String?,
openGroupInvitationUrl: String?,
openGroupServerMessageId: Int64?,
openGroupWhisperMods: Bool,
openGroupWhisperTo: String?
) {
self.serverHash = serverHash
self.threadId = threadId
self.authorId = authorId
self.variant = variant
self.body = body
self.timestampMs = timestampMs
self.receivedAtTimestampMs = receivedAtTimestampMs
self.expiresInSeconds = expiresInSeconds
self.expiresStartedAtMs = expiresStartedAtMs
self.openGroupInvitationName = openGroupInvitationName
self.openGroupInvitationUrl = openGroupInvitationUrl
self.openGroupServerMessageId = openGroupServerMessageId
self.openGroupWhisperMods = openGroupWhisperMods
self.openGroupWhisperTo = openGroupWhisperTo
}
// MARK: - Custom Database Interaction
public mutating func didInsert(with rowID: Int64, for column: String?) {
self.id = rowID
}
}
// MARK: - Convenience
public extension Interaction {
// MARK: - Variables
var isExpiringMessage: Bool {
guard variant == .standardIncoming || variant == .standardOutgoing else { return false }
return (expiresInSeconds ?? 0 > 0)
}
var openGroupWhisper: Bool { return (openGroupWhisperMods || (openGroupWhisperTo != nil)) }
// MARK: - Functions
func with(
expiresInSeconds: TimeInterval? = nil,
expiresStartedAtMs: Double? = nil,
openGroupServerMessageId: Int64? = nil
) -> Interaction {
var updatedInteraction: Interaction = self
updatedInteraction.expiresInSeconds = (expiresInSeconds ?? updatedInteraction.expiresInSeconds)
updatedInteraction.expiresStartedAtMs = (expiresStartedAtMs ?? updatedInteraction.expiresStartedAtMs)
updatedInteraction.openGroupServerMessageId = (openGroupServerMessageId ?? updatedInteraction.openGroupServerMessageId)
return updatedInteraction
}
}

View File

@ -0,0 +1,38 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct LinkPreview: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "linkPreview" }
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
private static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
private static let attachment = hasOne(Attachment.self, using: Attachment.linkPreviewForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case url
case interactionId
case title
}
/// The url for the link preview
public let url: String
/// The id for the interaction this LinkPreview belongs to
public let interactionId: Int64
/// The title for the link
public let title: String?
// MARK: - Relationships
public var interaction: QueryInterfaceRequest<Interaction> {
request(for: LinkPreview.interaction)
}
public var attachment: QueryInterfaceRequest<Attachment> {
request(for: LinkPreview.attachment)
}
}

View File

@ -6,11 +6,14 @@ import SessionUtilitiesKit
public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "openGroup" }
static let capabilities = hasMany(Capability.self)
static let members = hasMany(GroupMember.self)
internal static let threadForeignKey = ForeignKey([Columns.threadId], to: [SessionThread.Columns.id])
private static let thread = belongsTo(SessionThread.self, using: threadForeignKey)
private static let capabilities = hasMany(Capability.self, using: Capability.openGroupForeignKey)
private static let members = hasMany(GroupMember.self, using: GroupMember.openGroupForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case threadId
case server
case room
case publicKey
@ -22,18 +25,47 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco
case infoUpdates
}
public var id: String { "\(server).\(room)" }
public var id: String { threadId } // Identifiable
/// The id for the thread this open group belongs to
///
/// **Note:** This value will always be `\(server).\(room)` (This needs its own column to
/// allow for db joining to the Thread table)
public let threadId: String
/// The server for the group
public let server: String
/// The specific room on the server for the group
public let room: String
/// The public key for the group
public let publicKey: String
/// The name for the group
public let name: String
/// The description for the group
public let groupDescription: String?
/// The ID with which the image can be retrieved from the server
public let imageId: Int?
/// The image for the group
public let imageData: Data?
/// The number of users in the group
public let userCount: Int
/// Monotonic room information counter that increases each time the room's metadata changes
public let infoUpdates: Int
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
request(for: OpenGroup.thread)
}
public var capabilities: QueryInterfaceRequest<Capability> {
request(for: OpenGroup.capabilities)
}
@ -47,4 +79,40 @@ public struct OpenGroup: Codable, Identifiable, FetchableRecord, PersistableReco
request(for: OpenGroup.members)
.filter(GroupMember.Columns.role == GroupMember.Role.admin)
}
// MARK: - Initialization
init(
server: String,
room: String,
publicKey: String,
name: String,
groupDescription: String?,
imageId: Int?,
imageData: Data?,
userCount: Int,
infoUpdates: Int
) {
// Always force the server to lowercase
self.threadId = "\(server.lowercased()).\(room)" // TODO: Validate this (doesn't seem to happen in the old code...)
self.server = server.lowercased()
self.room = room
self.publicKey = publicKey
self.name = name
self.groupDescription = groupDescription
self.imageId = imageId
self.imageData = imageData
self.userCount = userCount
self.infoUpdates = infoUpdates
}
// MARK: - Custom Database Interaction
public func delete(_ db: Database) throws -> Bool {
// Delete all 'GroupMember' records associated with this OpenGroup (can't
// have a proper ForeignKey constraint as 'GroupMember' is reused for the
// 'ClosedGroup' table as well)
try request(for: OpenGroup.members).deleteAll(db)
return try performDelete(db)
}
}

View File

@ -65,6 +65,9 @@ public struct Profile: Codable, Identifiable, FetchableRecord, PersistableRecord
OWSFileSystem.deleteFileIfExists(path)
}
}
// Since it's possible this profile is currently being displayed, send notifications
// indicating that it has been updated
NotificationCenter.default.post(name: .profileUpdated, object: id)
if id == getUserHexEncodedPublicKey(db) {

View File

@ -0,0 +1,57 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct Quote: Codable, FetchableRecord, MutablePersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "quote" }
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
internal static let originalInteractionForeignKey = ForeignKey(
[Columns.timestampMs, Columns.authorId],
to: [Interaction.Columns.timestampMs, Interaction.Columns.authorId]
)
internal static let profileForeignKey = ForeignKey([Columns.authorId], to: [Profile.Columns.id])
private static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
private static let profile = hasOne(Profile.self, using: profileForeignKey)
private static let attachment = hasOne(Attachment.self, using: Attachment.quoteForeignKey)
private static let quotedInteraction = hasOne(Interaction.self, using: originalInteractionForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case interactionId
case authorId
case timestampMs
case body
}
/// The id for the interaction this Quote belongs to
public let interactionId: Int64
/// The id for the author this Quote belongs to
public let authorId: String
/// The timestamp in milliseconds since epoch when the quoted interaction was sent
public let timestampMs: Double
/// The body of the quoted message if the user is quoting a text message or an attachment with a caption
public let body: String?
// MARK: - Relationships
public var interaction: QueryInterfaceRequest<Interaction> {
request(for: Quote.interaction)
}
public var profile: QueryInterfaceRequest<Profile> {
request(for: Quote.profile)
}
public var attachment: QueryInterfaceRequest<Attachment> {
request(for: Quote.attachment)
}
public var originalInteraction: QueryInterfaceRequest<Interaction> {
request(for: Quote.quotedInteraction)
}
}

View File

@ -0,0 +1,55 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public struct RecipientState: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "recipientState" }
internal static let profileForeignKey = ForeignKey([Columns.recipientId], to: [Profile.Columns.id])
internal static let interactionForeignKey = ForeignKey([Columns.interactionId], to: [Interaction.Columns.id])
private static let profile = hasOne(Profile.self, using: profileForeignKey)
private static let interaction = belongsTo(Interaction.self, using: interactionForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case interactionId
case recipientId
case state
case readTimestampMs
}
public enum State: Int, Codable, DatabaseValueConvertible {
case failed
case sending
case skipped
case sent
}
/// The id for the interaction this state belongs to
public let interactionId: Int64
/// The id for the recipient this state belongs to
public let recipientId: String
/// The current state for the recipient
public let state: State
/// When the interaction was read in milliseconds since epoch
///
/// This value will be null for outgoing messages
///
/// **Note:** This currently will be set when opening the thread for the first time after receiving this interaction
/// rather than when the interaction actually appears on the screen
public fileprivate(set) var readTimestampMs: Double? = nil // TODO: Add setter
// MARK: - Relationships
public var interaction: QueryInterfaceRequest<Interaction> {
request(for: RecipientState.interaction)
}
public var profile: QueryInterfaceRequest<Profile> {
request(for: RecipientState.profile)
}
}

View File

@ -6,9 +6,13 @@ import SessionUtilitiesKit
public struct SessionThread: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "thread" }
static let disappearingMessagesConfiguration = hasOne(DisappearingMessagesConfiguration.self)
static let closedGroup = hasOne(ClosedGroup.self)
static let openGroup = hasOne(OpenGroup.self)
private static let closedGroup = hasOne(ClosedGroup.self, using: ClosedGroup.threadForeignKey)
private static let openGroup = hasOne(OpenGroup.self, using: OpenGroup.threadForeignKey)
private static let disappearingMessagesConfiguration = hasOne(
DisappearingMessagesConfiguration.self,
using: DisappearingMessagesConfiguration.threadForeignKey
)
private static let interactions = hasMany(Interaction.self, using: Interaction.threadForeignKey)
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
@ -29,8 +33,8 @@ public struct SessionThread: Codable, Identifiable, FetchableRecord, Persistable
}
public enum NotificationMode: Int, Codable, DatabaseValueConvertible {
case all
case none
case all
case mentionsOnly // Only applicable to group threads
}
@ -43,11 +47,7 @@ public struct SessionThread: Codable, Identifiable, FetchableRecord, Persistable
public let notificationMode: NotificationMode
public let mutedUntilTimestamp: TimeInterval?
public var disappearingMessagesConfiguration: QueryInterfaceRequest<DisappearingMessagesConfiguration> {
request(for: SessionThread.disappearingMessagesConfiguration)
}
// public var lastInteraction
// MARK: - Relationships
public var closedGroup: QueryInterfaceRequest<ClosedGroup> {
request(for: SessionThread.closedGroup)
@ -56,4 +56,13 @@ public struct SessionThread: Codable, Identifiable, FetchableRecord, Persistable
public var openGroup: QueryInterfaceRequest<OpenGroup> {
request(for: SessionThread.openGroup)
}
public var disappearingMessagesConfiguration: QueryInterfaceRequest<DisappearingMessagesConfiguration> {
request(for: SessionThread.disappearingMessagesConfiguration)
}
public var interactions: QueryInterfaceRequest<Interaction> {
request(for: SessionThread.interactions)
}
}

View File

@ -73,6 +73,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
@interface TSOutgoingMessage : TSMessage
@property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap;
- (instancetype)initMessageWithTimestamp:(uint64_t)timestamp
inThread:(nullable TSThread *)thread
messageBody:(nullable NSString *)body

View File

@ -79,7 +79,6 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
@property (atomic) NSString *customMessage;
@property (atomic) NSString *mostRecentFailureText;
@property (atomic) TSGroupMetaMessage groupMetaMessage;
@property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap;
@end

View File

@ -74,27 +74,29 @@ enum _002_YDBToGRDBMigration: Migration {
// Insert the data into GRDB
db[.lastSnodePoolRefreshDate] = lastSnodePoolRefreshDate
try snodeResult.forEach { legacySnode in
try Snode(
address: legacySnode.address,
port: legacySnode.port,
ed25519PublicKey: legacySnode.publicKeySet.ed25519Key,
x25519PublicKey: legacySnode.publicKeySet.x25519Key
).insert(db)
}
try snodeSetResult.forEach { key, legacySnodeSet in
try legacySnodeSet.enumerated().forEach { nodeIndex, legacySnode in
// Note: In this case the 'nodeIndex' is irrelivant
try SnodeSet(
key: key,
nodeIndex: nodeIndex,
try autoreleasepool {
db[.lastSnodePoolRefreshDate] = lastSnodePoolRefreshDate
try snodeResult.forEach { legacySnode in
try Snode(
address: legacySnode.address,
port: legacySnode.port
port: legacySnode.port,
ed25519PublicKey: legacySnode.publicKeySet.ed25519Key,
x25519PublicKey: legacySnode.publicKeySet.x25519Key
).insert(db)
}
try snodeSetResult.forEach { key, legacySnodeSet in
try legacySnodeSet.enumerated().forEach { nodeIndex, legacySnode in
// Note: In this case the 'nodeIndex' is irrelivant
try SnodeSet(
key: key,
nodeIndex: nodeIndex,
address: legacySnode.address,
port: legacySnode.port
).insert(db)
}
}
}
// MARK: - Received Messages & Last Message Hash
@ -121,22 +123,24 @@ enum _002_YDBToGRDBMigration: Migration {
}
}
try receivedMessageResults.forEach { key, hashes in
try hashes.forEach { hash in
try autoreleasepool {
try receivedMessageResults.forEach { key, hashes in
try hashes.forEach { hash in
try SnodeReceivedMessageInfo(
key: key,
hash: hash,
expirationDateMs: 0
).insert(db)
}
}
try lastMessageResults.forEach { key, data in
try SnodeReceivedMessageInfo(
key: key,
hash: hash,
expirationDateMs: 0
hash: data.hash,
expirationDateMs: ((data.json["expirationDate"] as? Int64) ?? 0)
).insert(db)
}
}
try lastMessageResults.forEach { key, data in
try SnodeReceivedMessageInfo(
key: key,
hash: data.hash,
expirationDateMs: ((data.json["expirationDate"] as? Int64) ?? 0)
).insert(db)
}
}
}

View File

@ -65,31 +65,32 @@ enum _002_YDBToGRDBMigration: Migration {
throw GRDBStorageError.migrationFailed
}
// Insert the data into GRDB
try Identity(
variant: .seed,
data: Data(hex: seedHexString)
).insert(db)
try Identity(
variant: .ed25519SecretKey,
data: Data(hex: userEd25519SecretKeyHexString)
).insert(db)
try Identity(
variant: .ed25519PublicKey,
data: Data(hex: userEd25519PublicKeyHexString)
).insert(db)
try Identity(
variant: .x25519PrivateKey,
data: userX25519KeyPair.privateKey
).insert(db)
try Identity(
variant: .x25519PublicKey,
data: userX25519KeyPair.publicKey
).insert(db)
try autoreleasepool {
// Insert the data into GRDB
try Identity(
variant: .seed,
data: Data(hex: seedHexString)
).insert(db)
try Identity(
variant: .ed25519SecretKey,
data: Data(hex: userEd25519SecretKeyHexString)
).insert(db)
try Identity(
variant: .ed25519PublicKey,
data: Data(hex: userEd25519PublicKeyHexString)
).insert(db)
try Identity(
variant: .x25519PrivateKey,
data: userX25519KeyPair.privateKey
).insert(db)
try Identity(
variant: .x25519PublicKey,
data: userX25519KeyPair.publicKey
).insert(db)
}
}
}

View File

@ -20,6 +20,10 @@ public class TypedTableDefinition<T> where T: TableRecord, T: ColumnExpressible
definition.primaryKey(columns.map { $0.name }, onConflict: onConflict)
}
public func uniqueKey(_ columns: [T.Columns], onConflict: Database.ConflictResolution? = nil) {
definition.uniqueKey(columns.map { $0.name }, onConflict: onConflict)
}
public func foreignKey<Other>(
_ columns: [T.Columns],
references table: Other.Type,

View File

@ -16,6 +16,11 @@ public final class Configuration : NSObject {
maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier)
)
SNMessagingKit.configure(storage: Storage.shared)
SNSnodeKit.configure()
}
@objc public static func performDatabaseSetup() {
if !isSetup {
isSetup = true
@ -30,8 +35,5 @@ public final class Configuration : NSObject {
]
)
}
SNMessagingKit.configure(storage: Storage.shared)
SNSnodeKit.configure()
}
}

View File

@ -100,6 +100,9 @@ NS_ASSUME_NONNULL_BEGIN
}];
});
}];
// Must happen after the performUpdateCheck above to ensure all legacy database migrations have run
[SNConfiguration performDatabaseSetup];
});
}