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.
254 lines
11 KiB
Swift
254 lines
11 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 needsConfigSync: Bool = true
|
|
static let minExpectedRunDuration: TimeInterval = 0.4
|
|
static let fetchedTables: [(TableRecord & FetchableRecord).Type] = [
|
|
GroupMember.self, ClosedGroupKeyPair.self, SessionThread.self
|
|
]
|
|
static let createdOrAlteredTables: [(TableRecord & FetchableRecord).Type] = [
|
|
SessionThread.self, Profile.self, GroupMember.self, ClosedGroupKeyPair.self, ConfigDump.self
|
|
]
|
|
static let droppedTables: [(TableRecord & FetchableRecord).Type] = []
|
|
|
|
static func migrate(_ db: Database, using dependencies: Dependencies) throws {
|
|
// Add `markedAsUnread` to the thread table
|
|
try db.alter(table: SessionThread.self) { t in
|
|
t.add(.markedAsUnread, .boolean)
|
|
t.add(.pinnedPriority, .integer)
|
|
}
|
|
|
|
// Add `lastNameUpdate` and `lastProfilePictureUpdate` columns to the profile table
|
|
try db.alter(table: Profile.self) { t in
|
|
t.add(.lastNameUpdate, .integer).defaults(to: 0)
|
|
t.add(.lastProfilePictureUpdate, .integer).defaults(to: 0)
|
|
}
|
|
|
|
// SQLite doesn't support adding a new primary key after creation so we need to create a new table with
|
|
// the setup we want, copy data from the old table over, drop the old table and rename the new table
|
|
struct TmpGroupMember: Codable, TableRecord, FetchableRecord, PersistableRecord, ColumnExpressible {
|
|
static var databaseTableName: String { "tmpGroupMember" }
|
|
|
|
public typealias Columns = CodingKeys
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
case groupId
|
|
case profileId
|
|
case role
|
|
case isHidden
|
|
}
|
|
|
|
public let groupId: String
|
|
public let profileId: String
|
|
public let role: GroupMember.Role
|
|
public let isHidden: Bool
|
|
}
|
|
|
|
try db.create(table: TmpGroupMember.self) { 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(SessionThread.self, 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
|
|
let nonDuplicateGroupMembers: [TmpGroupMember] = try GroupMember
|
|
.select(.groupId, .profileId, .role, .isHidden)
|
|
.group(GroupMember.Columns.groupId, GroupMember.Columns.profileId, GroupMember.Columns.role)
|
|
.asRequest(of: TmpGroupMember.self)
|
|
.fetchAll(db)
|
|
|
|
// Insert into the new table, drop the old table and rename the new table to be the old one
|
|
try nonDuplicateGroupMembers.forEach { try $0.save(db) }
|
|
try db.drop(table: GroupMember.self)
|
|
try db.rename(table: TmpGroupMember.databaseTableName, to: GroupMember.databaseTableName)
|
|
|
|
// Need to create the indexes separately from creating 'TmpGroupMember' to ensure they
|
|
// have the correct names
|
|
try db.createIndex(on: GroupMember.self, columns: [.groupId])
|
|
try db.createIndex(on: GroupMember.self, columns: [.profileId])
|
|
|
|
// SQLite doesn't support removing unique constraints so we need to create a new table with
|
|
// the setup we want, copy data from the old table over, drop the old table and rename the new table
|
|
struct TmpClosedGroupKeyPair: Codable, TableRecord, FetchableRecord, PersistableRecord, ColumnExpressible {
|
|
static var databaseTableName: String { "tmpClosedGroupKeyPair" }
|
|
|
|
public typealias Columns = CodingKeys
|
|
public enum CodingKeys: String, CodingKey, ColumnExpression {
|
|
case threadId
|
|
case publicKey
|
|
case secretKey
|
|
case receivedTimestamp
|
|
case threadKeyPairHash
|
|
}
|
|
|
|
public let threadId: String
|
|
public let publicKey: Data
|
|
public let secretKey: Data
|
|
public let receivedTimestamp: TimeInterval
|
|
public let threadKeyPairHash: String
|
|
}
|
|
|
|
try db.alter(table: ClosedGroupKeyPair.self) { t in
|
|
t.add(.threadKeyPairHash, .text).defaults(to: "")
|
|
}
|
|
try db.create(table: TmpClosedGroupKeyPair.self) { t in
|
|
t.column(.threadId, .text)
|
|
.notNull()
|
|
.references(ClosedGroup.self, 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
|
|
try ClosedGroupKeyPair
|
|
.fetchAll(db)
|
|
.map { keyPair in
|
|
ClosedGroupKeyPair(
|
|
threadId: keyPair.threadId,
|
|
publicKey: keyPair.publicKey,
|
|
secretKey: keyPair.secretKey,
|
|
receivedTimestamp: keyPair.receivedTimestamp
|
|
)
|
|
}
|
|
.map { keyPair in
|
|
TmpClosedGroupKeyPair(
|
|
threadId: keyPair.threadId,
|
|
publicKey: keyPair.publicKey,
|
|
secretKey: keyPair.secretKey,
|
|
receivedTimestamp: keyPair.receivedTimestamp,
|
|
threadKeyPairHash: keyPair.threadKeyPairHash
|
|
)
|
|
}
|
|
.forEach { try? $0.insert(db) } // Ignore duplicate values
|
|
try db.drop(table: ClosedGroupKeyPair.self)
|
|
try db.rename(table: TmpClosedGroupKeyPair.databaseTableName, to: ClosedGroupKeyPair.databaseTableName)
|
|
|
|
// 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.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadId])
|
|
try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.receivedTimestamp])
|
|
try db.createIndex(on: ClosedGroupKeyPair.self, columns: [.threadKeyPairHash])
|
|
try db.createIndex(
|
|
on: ClosedGroupKeyPair.self,
|
|
columns: [.threadId, .threadKeyPairHash]
|
|
)
|
|
|
|
// Add an index for the 'Quote' table to speed up queries
|
|
try db.createIndex(
|
|
on: Quote.self,
|
|
columns: [.timestampMs]
|
|
)
|
|
|
|
// New table for storing the latest config dump for each type
|
|
try db.create(table: ConfigDump.self) { 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 SessionThread
|
|
.filter(sql: "isPinned = true")
|
|
.updateAll(
|
|
db,
|
|
SessionThread.Columns.pinnedPriority.set(to: 1)
|
|
)
|
|
|
|
// If we don't have an ed25519 key then no need to create cached dump data
|
|
let userPublicKey: String = getUserHexEncodedPublicKey(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 SessionThread
|
|
.filter(
|
|
SessionThread.Columns.shouldBeVisible == false &&
|
|
SessionThread.Columns.id != userPublicKey
|
|
)
|
|
.select(.id)
|
|
.asRequest(of: String.self)
|
|
.fetchAll(db)
|
|
try SessionThread
|
|
.deleteAll(db, ids: threadIdsToDelete)
|
|
try DisappearingMessagesConfiguration
|
|
.filter(threadIdsToDelete.contains(DisappearingMessagesConfiguration.Columns.threadId))
|
|
.deleteAll(db)
|
|
try ClosedGroup
|
|
.filter(threadIdsToDelete.contains(ClosedGroup.Columns.threadId))
|
|
.deleteAll(db)
|
|
try GroupMember
|
|
.filter(threadIdsToDelete.contains(GroupMember.Columns.groupId))
|
|
.deleteAll(db)
|
|
try Interaction
|
|
.filter(threadIdsToDelete.contains(Interaction.Columns.threadId))
|
|
.deleteAll(db)
|
|
try ThreadTypingIndicator
|
|
.filter(threadIdsToDelete.contains(ThreadTypingIndicator.Columns.threadId))
|
|
.deleteAll(db)
|
|
try PendingReadReceipt
|
|
.filter(threadIdsToDelete.contains(PendingReadReceipt.Columns.threadId))
|
|
.deleteAll(db)
|
|
|
|
/// 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 {
|
|
if (try SessionThread.exists(db, id: userPublicKey)) == false {
|
|
try SessionThread
|
|
.fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false)
|
|
}
|
|
}
|
|
|
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
|
}
|
|
}
|