mirror of https://github.com/oxen-io/session-ios
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
231 lines
10 KiB
Swift
231 lines
10 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
//
|
|
// stringlint:disable
|
|
|
|
import Foundation
|
|
import CryptoKit
|
|
import GRDB
|
|
import SessionUtil
|
|
import SessionUtilitiesKit
|
|
|
|
/// This migration makes the neccessary changes to support the updated user config syncing system
|
|
enum _013_SessionUtilChanges: Migration {
|
|
static let target: TargetMigrations.Identifier = .messagingKit
|
|
static let identifier: String = "SessionUtilChanges"
|
|
static let minExpectedRunDuration: TimeInterval = 0.4
|
|
static let createdTables: [(TableRecord & FetchableRecord).Type] = [ConfigDump.self]
|
|
|
|
static func migrate(_ db: Database, using dependencies: Dependencies) throws {
|
|
// Add `markedAsUnread` to the thread table
|
|
try db.alter(table: "thread") { t in
|
|
t.add(column: "markedAsUnread", .boolean)
|
|
t.add(column: "pinnedPriority", .integer)
|
|
}
|
|
|
|
// Add `lastNameUpdate` and `lastProfilePictureUpdate` columns to the profile table
|
|
try db.alter(table: "profile".self) { t in
|
|
t.add(column: "lastNameUpdate", .integer).defaults(to: 0)
|
|
t.add(column: "lastProfilePictureUpdate", .integer).defaults(to: 0)
|
|
}
|
|
|
|
try db.create(table: "tmpGroupMember") { t in
|
|
// Note: Since we don't know whether this will be stored against a 'ClosedGroup' or
|
|
// an 'OpenGroup' we add the foreign key constraint against the thread itself (which
|
|
// shares the same 'id' as the 'groupId') so we can cascade delete automatically
|
|
t.column("groupId", .text)
|
|
.notNull()
|
|
.references("thread", onDelete: .cascade) // Delete if Thread deleted
|
|
t.column("profileId", .text)
|
|
.notNull()
|
|
t.column("role", .integer).notNull()
|
|
t.column("isHidden", .boolean)
|
|
.notNull()
|
|
.defaults(to: false)
|
|
|
|
t.primaryKey(["groupId", "profileId", "role"])
|
|
}
|
|
|
|
// Retrieve the non-duplicate group member entries from the old table
|
|
try db.execute(sql: """
|
|
INSERT INTO tmpGroupMember (groupId, profileId, role, isHidden)
|
|
SELECT groupId, profileId, role, MAX(isHidden) AS isHidden
|
|
FROM groupMember
|
|
GROUP BY groupId, profileId, role
|
|
""")
|
|
|
|
// Insert into the new table, drop the old table and rename the new table to be the old one
|
|
try db.drop(table: "groupMember")
|
|
try db.rename(table: "tmpGroupMember", to: "groupMember")
|
|
|
|
// Need to create the indexes separately from creating 'tmpGroupMember' to ensure they
|
|
// have the correct names
|
|
try db.create(indexOn: "groupMember", columns: ["groupId"])
|
|
try db.create(indexOn: "groupMember", columns: ["profileId"])
|
|
|
|
try db.alter(table: "closedGroupKeyPair") { t in
|
|
t.add(column: "threadKeyPairHash", .text).defaults(to: "")
|
|
}
|
|
try db.create(table: "tmpClosedGroupKeyPair") { t in
|
|
t.column("threadId", .text)
|
|
.notNull()
|
|
.references("closedGroup", onDelete: .cascade) // Delete if ClosedGroup deleted
|
|
t.column("publicKey", .blob).notNull()
|
|
t.column("secretKey", .blob).notNull()
|
|
t.column("receivedTimestamp", .double)
|
|
.notNull()
|
|
t.column("threadKeyPairHash", .integer)
|
|
.notNull()
|
|
.unique()
|
|
}
|
|
|
|
// Insert into the new table, drop the old table and rename the new table to be the old one
|
|
let existingKeyPairs: [Row] = try Row.fetchAll(db, sql: """
|
|
SELECT threadId, publicKey, secretKey, receivedTimestamp
|
|
FROM closedGroupKeyPair
|
|
""")
|
|
existingKeyPairs.forEach { row in
|
|
let threadId: String = row["threadId"]
|
|
let publicKey: Data = row["publicKey"]
|
|
let secretKey: Data = row["secretKey"]
|
|
|
|
// Optional try as we want to ignore duplicate values
|
|
try? db.execute(
|
|
sql: """
|
|
INSERT INTO tmpClosedGroupKeyPair (threadId, publicKey, secretKey, receivedTimestamp, threadKeyPairHash)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
FROM groupMember
|
|
GROUP BY groupId, profileId, role
|
|
""",
|
|
arguments: [
|
|
threadId,
|
|
publicKey,
|
|
secretKey,
|
|
row["receivedTimestamp"],
|
|
ClosedGroupKeyPair.generateHash(
|
|
threadId: threadId,
|
|
publicKey: publicKey,
|
|
secretKey: secretKey
|
|
)
|
|
]
|
|
)
|
|
}
|
|
try db.drop(table: "closedGroupKeyPair")
|
|
try db.rename(table: "tmpClosedGroupKeyPair", to: "closedGroupKeyPair")
|
|
|
|
// Add an index for the 'ClosedGroupKeyPair' so we can lookup existing keys more easily
|
|
//
|
|
// Note: Need to create the indexes separately from creating 'TmpClosedGroupKeyPair' to ensure they
|
|
// have the correct names
|
|
try db.create(indexOn: "closedGroupKeyPair", columns: ["threadId"])
|
|
try db.create(indexOn: "closedGroupKeyPair", columns: ["receivedTimestamp"])
|
|
try db.create(indexOn: "closedGroupKeyPair", columns: ["threadKeyPairHash"])
|
|
try db.create(indexOn: "closedGroupKeyPair", columns: ["threadId", "threadKeyPairHash"])
|
|
|
|
// Add an index for the 'Quote' table to speed up queries
|
|
try db.create(indexOn: "quote", columns: ["timestampMs"])
|
|
|
|
// New table for storing the latest config dump for each type
|
|
try db.create(table: "configDump") { t in
|
|
t.column("variant", .text)
|
|
.notNull()
|
|
t.column("publicKey", .text)
|
|
.notNull()
|
|
.indexed()
|
|
t.column("data", .blob)
|
|
.notNull()
|
|
t.column("timestampMs", .integer)
|
|
.notNull()
|
|
.defaults(to: 0)
|
|
|
|
t.primaryKey(["variant", "publicKey"])
|
|
}
|
|
|
|
// Migrate the 'isPinned' value to 'pinnedPriority'
|
|
try db.execute(sql: "UPDATE thread SET pinnedPriority = 1 WHERE isPinned = true")
|
|
|
|
// If we don't have an ed25519 key then no need to create cached dump data
|
|
let userSessionId: SessionId = MigrationHelper.userSessionId(db)
|
|
|
|
/// Remove any hidden threads to avoid syncing them (they are basically shadow threads created by starting a conversation
|
|
/// but not sending a message so can just be cleared out)
|
|
///
|
|
/// **Note:** Our settings defer foreign key checks to the end of the migration, unfortunately the `PRAGMA foreign_keys`
|
|
/// setting is also a no-on during transactions so we can't enable it for the delete action, as a result we need to manually clean
|
|
/// up any data associated with the threads we want to delete, at the time of this migration the following tables should cascade
|
|
/// delete when a thread is deleted:
|
|
/// - DisappearingMessagesConfiguration
|
|
/// - ClosedGroup
|
|
/// - GroupMember
|
|
/// - Interaction
|
|
/// - ThreadTypingIndicator
|
|
/// - PendingReadReceipt
|
|
let threadIdsToDelete: String = try String
|
|
.fetchAll(
|
|
db,
|
|
sql: """
|
|
SELECT id
|
|
FROM thread
|
|
WHERE (
|
|
shouldBeVisible = false AND
|
|
id != ?
|
|
)
|
|
""",
|
|
arguments: [userSessionId.hexString]
|
|
)
|
|
.map { "'\($0)'" }
|
|
.joined(separator: ", ")
|
|
try db.execute(sql: "DELETE FROM thread WHERE id IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM disappearingMessagesConfiguration WHERE threadId IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM closedGroup WHERE threadId IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM groupMember WHERE groupId IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM interaction WHERE threadId IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM threadTypingIndicator WHERE threadId IN (\(threadIdsToDelete))")
|
|
try db.execute(sql: "DELETE FROM pendingReadReceipt WHERE threadId IN (\(threadIdsToDelete))")
|
|
|
|
/// There was previously a bug which allowed users to fully delete the 'Note to Self' conversation but we don't want that, so
|
|
/// create it again if it doesn't exists
|
|
///
|
|
/// **Note:** Since migrations are run when running tests creating a random SessionThread will result in unexpected thread
|
|
/// counts so don't do this when running tests (this logic is the same as in `MainAppContext.isRunningTests`
|
|
if ProcessInfo.processInfo.environment["XCTestConfigurationFilePath"] == nil {
|
|
let threadExists: Bool? = try Bool.fetchOne(
|
|
db,
|
|
sql: "SELECT EXISTS (SELECT * FROM thread WHERE id = '\(userSessionId.hexString)')"
|
|
)
|
|
|
|
if threadExists == false {
|
|
try db.execute(
|
|
sql: """
|
|
INSERT INTO thread (
|
|
id,
|
|
variant,
|
|
creationDateTimestamp,
|
|
shouldBeVisible,
|
|
isPinned,
|
|
messageDraft,
|
|
notificationSound,
|
|
mutedUntilTimestamp,
|
|
onlyNotifyForMentions,
|
|
markedAsUnread,
|
|
pinnedPriority
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, NULL, NULL, NULL, ?, ?, ?)
|
|
""",
|
|
arguments: [
|
|
userSessionId.hexString,
|
|
SessionThread.Variant.contact.rawValue,
|
|
(dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000),
|
|
false, // Not visible
|
|
false,
|
|
false,
|
|
false,
|
|
-1, // Hidden priority at the time of writing
|
|
]
|
|
)
|
|
}
|
|
}
|
|
|
|
Storage.update(progress: 1, for: self, in: target, using: dependencies)
|
|
}
|
|
}
|