session-ios/Session/Utilities/MockDataGenerator.swift

435 lines
23 KiB
Swift

// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import Curve25519Kit
import SessionMessagingKit
enum MockDataGenerator {
// Note: This was taken from TensorFlow's Random (https://github.com/apple/swift/blob/bc8f9e61d333b8f7a625f74d48ef0b554726e349/stdlib/public/TensorFlow/Random.swift)
// the complex approach is needed due to an issue with Swift's randomElement(using:)
// generation (see https://stackoverflow.com/a/64897775 for more info)
struct ARC4RandomNumberGenerator: RandomNumberGenerator {
var state: [UInt8] = Array(0...255)
var iPos: UInt8 = 0
var jPos: UInt8 = 0
init<T: BinaryInteger>(seed: T) {
self.init(
seed: (0..<(UInt64.bitWidth / UInt64.bitWidth)).map { index in
UInt8(truncatingIfNeeded: seed >> (UInt8.bitWidth * index))
}
)
}
init(seed: [UInt8]) {
precondition(seed.count > 0, "Length of seed must be positive")
precondition(seed.count <= 256, "Length of seed must be at most 256")
// Note: Have to use a for loop instead of a 'forEach' otherwise
// it doesn't work properly (not sure why...)
var j: UInt8 = 0
for i: UInt8 in 0...255 {
j &+= S(i) &+ seed[Int(i) % seed.count]
swapAt(i, j)
}
}
/// Produce the next random UInt64 from the stream, and advance the internal state
mutating func next() -> UInt64 {
// Note: Have to use a for loop instead of a 'forEach' otherwise
// it doesn't work properly (not sure why...)
var result: UInt64 = 0
for _ in 0..<UInt64.bitWidth / UInt8.bitWidth {
result <<= UInt8.bitWidth
result += UInt64(nextByte())
}
return result
}
/// Helper to access the state
private func S(_ index: UInt8) -> UInt8 {
return state[Int(index)]
}
/// Helper to swap elements of the state
private mutating func swapAt(_ i: UInt8, _ j: UInt8) {
state.swapAt(Int(i), Int(j))
}
/// Generates the next byte in the keystream.
private mutating func nextByte() -> UInt8 {
iPos &+= 1
jPos &+= S(iPos)
swapAt(iPos, jPos)
return S(S(iPos) &+ S(jPos))
}
}
// MARK: - Generation
static var printProgress: Bool = true
static var hasStartedGenerationThisRun: Bool = false
static func generateMockData(_ db: Database) {
// Don't re-generate the mock data if it already exists
guard !hasStartedGenerationThisRun && !(try! SessionThread.exists(db, id: "MockDatabaseThread")) else {
hasStartedGenerationThisRun = true
return
}
/// The mock data generation is quite slow, there are 3 parts which take a decent amount of time (deleting the account afterwards will
/// also take a long time):
/// Generating the threads & content - ~3s per 100
/// Writing to the database - ~10s per 1000
/// Updating the UI - ~10s per 1000
let dmThreadCount: Int = 1000
let closedGroupThreadCount: Int = 50
let openGroupThreadCount: Int = 20
let messageRangePerThread: [ClosedRange<Int>] = [(0...500)]
let dmRandomSeed: Int = 1111
let cgRandomSeed: Int = 2222
let ogRandomSeed: Int = 3333
let 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(db)
let logProgress: (String, String) -> () = { title, event in
guard printProgress else { return }
print("[MockDataGenerator] (\(Date().timeIntervalSince1970)) \(title) - \(event)")
}
hasStartedGenerationThisRun = true
// FIXME: Make sure this data doesn't go off device somehow?
logProgress("", "Start")
// First create the thread used to indicate that the mock data has been generated
_ = try? SessionThread.fetchOrCreate(db, id: "MockDatabaseThread", variant: .contact)
// 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)
(0..<min(chunkSize, remainingThreads)).forEach { index in
let threadIndex: Int = (dmThreadIndex + index)
logProgress("DM Thread \(threadIndex)", "Start")
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)
// Generate the thread
let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomSessionId, variant: .contact)
.with(shouldBeVisible: true)
.saved(db)
// Generate the contact
let contact: Contact = try! Contact(
id: randomSessionId,
isTrusted: true,
isApproved: (!isMessageRequest || Bool.random(using: &dmThreadRandomGenerator)),
isBlocked: false,
didApproveMe: (
!isMessageRequest &&
(((0..<10).randomElement(using: &dmThreadRandomGenerator) ?? 0) < 8) // 80% approved the current user
),
hasBeenBlocked: false
)
.saved(db)
_ = try! Profile(
id: randomSessionId,
name: (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &dmThreadRandomGenerator) }
.joined()
)
.saved(db)
// 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)
_ = try! Interaction(
threadId: thread.id,
authorId: (isIncoming ? randomSessionId : userSessionId),
variant: (isIncoming ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)
.compactMap { _ in wordContent.randomElement(using: &dmThreadRandomGenerator) }
.joined(separator: " "),
timestampMs: Int64(floor(timestampNow - Double(index * 5)) * 1000)
)
.inserted(db)
}
logProgress("DM Thread \(threadIndex)", "Done")
}
logProgress("DM Threads", "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)
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)
_ = try! Contact(
id: randomSessionId,
isTrusted: true,
isApproved: true,
isBlocked: false,
didApproveMe: true,
hasBeenBlocked: false
)
.saved(db)
_ = try! Profile(
id: randomSessionId,
name: (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &cgThreadRandomGenerator) }
.joined()
)
.saved(db)
members.append(randomSessionId)
}
let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .closedGroup)
.with(shouldBeVisible: true)
.saved(db)
_ = try! ClosedGroup(
threadId: randomGroupPublicKey,
name: groupName,
formationTimestamp: timestampNow
)
.saved(db)
members.forEach { memberId in
_ = try! GroupMember(
groupId: randomGroupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
)
.saved(db)
}
[members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId].forEach { adminId in
_ = try! GroupMember(
groupId: randomGroupPublicKey,
profileId: adminId,
role: .admin,
isHidden: false
)
.saved(db)
}
// Add the group to the user's set of public keys to poll for and store the key pair
let encryptionKeyPair = Curve25519.generateKeyPair()
_ = try! ClosedGroupKeyPair(
threadId: randomGroupPublicKey,
publicKey: encryptionKeyPair.publicKey,
secretKey: encryptionKeyPair.privateKey,
receivedTimestamp: timestampNow
)
.saved(db)
// 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 senderId: String = (members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId)
_ = try! Interaction(
threadId: thread.id,
authorId: senderId,
variant: (senderId != userSessionId ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)
.compactMap { _ in wordContent.randomElement(using: &cgThreadRandomGenerator) }
.joined(separator: " "),
timestampMs: Int64(floor(timestampNow - Double(index * 5)) * 1000)
)
.inserted(db)
}
logProgress("Closed Group Thread \(threadIndex)", "Done")
}
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)
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 roomDescriptionLength: Int = ((10..<50).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 roomDescription: String = (0..<roomDescriptionLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
let numGroupMembers: Int64 = ((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)
_ = try! Contact(
id: randomSessionId,
isTrusted: true,
isApproved: true,
isBlocked: false,
didApproveMe: true,
hasBeenBlocked: false
)
.saved(db)
_ = try! Profile(
id: randomSessionId,
name: (0..<contactNameLength)
.compactMap { _ in stringContent.randomElement(using: &ogThreadRandomGenerator) }
.joined()
)
.saved(db)
members.append(randomSessionId)
}
// Create the open group model and the thread
let thread: SessionThread = try! SessionThread
.fetchOrCreate(db, id: randomGroupPublicKey, variant: .openGroup)
.with(shouldBeVisible: true)
.saved(db)
_ = try! OpenGroup(
server: serverName,
roomToken: roomName,
publicKey: randomGroupPublicKey,
isActive: true,
name: roomName,
roomDescription: roomDescription,
userCount: numGroupMembers,
infoUpdates: 0,
sequenceNumber: 0,
inboxLatestMessageId: 0,
outboxLatestMessageId: 0
)
.saved(db)
// Generate the capabilities object
let hasBlinding: Bool = Bool.random(using: &dmThreadRandomGenerator)
_ = try! Capability(
openGroupServer: serverName.lowercased(),
variant: .sogs,
isMissing: false
).saved(db)
if hasBlinding {
_ = try! Capability(
openGroupServer: serverName.lowercased(),
variant: .blind,
isMissing: false
).saved(db)
}
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
logProgress("Open Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
(0..<numMessages).forEach { index in
let messageWords: Int = ((1..<20).randomElement(using: &ogThreadRandomGenerator) ?? 0)
let senderId: String = (members.randomElement(using: &ogThreadRandomGenerator) ?? userSessionId)
_ = try! Interaction(
threadId: thread.id,
authorId: senderId,
variant: (senderId != userSessionId ? .standardIncoming : .standardOutgoing),
body: (0..<messageWords)
.compactMap { _ in wordContent.randomElement(using: &ogThreadRandomGenerator) }
.joined(separator: " "),
timestampMs: Int64(floor(timestampNow - Double(index * 5)) * 1000)
)
.inserted(db)
}
logProgress("Open Group Thread \(threadIndex)", "Done")
}
ogThreadIndex += chunkSize
}
logProgress("Open Group Threads", "Done")
logProgress("", "Complete")
}
}