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.
1867 lines
95 KiB
Swift
1867 lines
95 KiB
Swift
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
|
|
import Foundation
|
|
import AVKit
|
|
import GRDB
|
|
import YapDatabase
|
|
import Curve25519Kit
|
|
import SessionUtilitiesKit
|
|
import SessionSnodeKit
|
|
|
|
// Note: Looks like the oldest iOS device we support (min iOS 13.0) has 2Gb of RAM, processing
|
|
// ~250k messages and ~1000 threads seems to take up
|
|
enum _003_YDBToGRDBMigration: Migration {
|
|
static let target: TargetMigrations.Identifier = .messagingKit
|
|
static let identifier: String = "YDBToGRDBMigration"
|
|
static let needsConfigSync: Bool = true
|
|
static let minExpectedRunDuration: TimeInterval = 20
|
|
|
|
static func migrate(_ db: Database) throws {
|
|
guard let dbConnection: YapDatabaseConnection = SUKLegacy.newDatabaseConnection() else {
|
|
// We want this setting to be on by default (even if there isn't a legacy database)
|
|
db[.trimOpenGroupMessagesOlderThanSixMonths] = true
|
|
|
|
SNLog("[Migration Warning] No legacy database, skipping \(target.key(with: self))")
|
|
return
|
|
}
|
|
|
|
// MARK: - Read from Legacy Database
|
|
|
|
let timestampNow: TimeInterval = Date().timeIntervalSince1970
|
|
var shouldFailMigration: Bool = false
|
|
var legacyMigrations: Set<SMKLegacy._DBMigration> = []
|
|
var contacts: Set<SMKLegacy._Contact> = []
|
|
var legacyBlockedSessionIds: Set<String> = []
|
|
var validProfileIds: Set<String> = []
|
|
var contactThreadIds: Set<String> = []
|
|
|
|
var legacyThreadIdToIdMap: [String: String] = [:]
|
|
var legacyThreads: Set<SMKLegacy._Thread> = []
|
|
var disappearingMessagesConfiguration: [String: SMKLegacy._DisappearingMessagesConfiguration] = [:]
|
|
|
|
var closedGroupKeys: [String: [TimeInterval: SUKLegacy.KeyPair]] = [:]
|
|
var closedGroupName: [String: String] = [:]
|
|
var closedGroupFormation: [String: UInt64] = [:]
|
|
var closedGroupModel: [String: SMKLegacy._GroupModel] = [:]
|
|
var closedGroupZombieMemberIds: [String: Set<String>] = [:]
|
|
|
|
var openGroupServer: [String: String] = [:]
|
|
var openGroupInfo: [String: SMKLegacy._OpenGroup] = [:]
|
|
var openGroupUserCount: [String: Int64] = [:]
|
|
var openGroupImage: [String: Data] = [:]
|
|
|
|
var interactions: [String: [SMKLegacy._DBInteraction]] = [:]
|
|
var attachments: [String: SMKLegacy._Attachment] = [:]
|
|
var processedAttachmentIds: Set<String> = []
|
|
var outgoingReadReceiptsTimestampsMs: [String: Set<Int64>] = [:]
|
|
var receivedMessageTimestamps: Set<UInt64> = []
|
|
var receivedCallUUIDs: [String: Set<String>] = [:]
|
|
|
|
var notifyPushServerJobs: Set<SMKLegacy._NotifyPNServerJob> = []
|
|
var messageReceiveJobs: Set<SMKLegacy._MessageReceiveJob> = []
|
|
var messageSendJobs: Set<SMKLegacy._MessageSendJob> = []
|
|
var attachmentUploadJobs: Set<SMKLegacy._AttachmentUploadJob> = []
|
|
var attachmentDownloadJobs: Set<SMKLegacy._AttachmentDownloadJob> = []
|
|
|
|
var legacyPreferences: [String: Any] = [:]
|
|
|
|
// Map the Legacy types for the NSKeyedUnarchivez
|
|
self.mapLegacyTypesForNSKeyedUnarchiver()
|
|
|
|
dbConnection.read { transaction in
|
|
// MARK: --Migrations
|
|
|
|
// Process the migrations (we don't want to bother running the old migrations as it would be
|
|
// a waste of time, rather we include the logic from the old migrations in here and make the
|
|
// same changes if the migration hasn't already run)
|
|
transaction.enumerateKeys(inCollection: SMKLegacy.databaseMigrationCollection) { key, _ in
|
|
guard let legacyMigration: SMKLegacy._DBMigration = SMKLegacy._DBMigration(rawValue: key) else {
|
|
SNLog("[Migration Error] Found unknown migration")
|
|
shouldFailMigration = true
|
|
return
|
|
}
|
|
|
|
legacyMigrations.insert(legacyMigration)
|
|
}
|
|
Storage.update(progress: 0.01, for: self, in: target)
|
|
|
|
// MARK: --Contacts
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Contacts")
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.contactCollection) { _, object, _, _ in
|
|
guard let contact = object as? SMKLegacy._Contact else { return }
|
|
|
|
contacts.insert(contact)
|
|
|
|
/// Store a record of the all valid profiles (so we can create dummy entries if we need to for closed group members)
|
|
validProfileIds.insert(contact.sessionID)
|
|
}
|
|
|
|
legacyBlockedSessionIds = Set(transaction.object(
|
|
forKey: SMKLegacy.blockedPhoneNumbersKey,
|
|
inCollection: SMKLegacy.blockListCollection
|
|
) as? [String] ?? [])
|
|
Storage.update(progress: 0.02, for: self, in: target)
|
|
|
|
// MARK: --Threads
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Threads")
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.threadCollection) { key, object, _ in
|
|
guard let thread: SMKLegacy._Thread = object as? SMKLegacy._Thread else { return }
|
|
|
|
legacyThreads.insert(thread)
|
|
|
|
// Want to exclude threads which aren't visible (ie. threads which we started
|
|
// but the user never ended up sending a message)
|
|
if key.starts(with: SMKLegacy.contactThreadPrefix) && thread.shouldBeVisible {
|
|
contactThreadIds.insert(key)
|
|
}
|
|
|
|
// Get the disappearing messages config
|
|
disappearingMessagesConfiguration[thread.uniqueId] = transaction
|
|
.object(forKey: thread.uniqueId, inCollection: SMKLegacy.disappearingMessagesCollection)
|
|
.asType(SMKLegacy._DisappearingMessagesConfiguration.self)
|
|
|
|
// Process group-specific info
|
|
guard let groupThread: SMKLegacy._GroupThread = thread as? SMKLegacy._GroupThread else {
|
|
legacyThreadIdToIdMap[thread.uniqueId] = thread.uniqueId.substring(
|
|
from: SMKLegacy.contactThreadPrefix.count
|
|
)
|
|
return
|
|
}
|
|
|
|
if groupThread.isClosedGroup {
|
|
// The old threadId for closed groups was in the below format, we don't
|
|
// really need the unnecessary complexity so process the key and extract
|
|
// the publicKey from it
|
|
// `g{base64String(Data(__textsecure_group__!{publicKey}))}
|
|
let base64GroupId: String = String(thread.uniqueId.suffix(from: thread.uniqueId.index(after: thread.uniqueId.startIndex)))
|
|
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) })
|
|
else {
|
|
SNLog("[Migration Error] Unable to decode Closed Group")
|
|
shouldFailMigration = true
|
|
return
|
|
}
|
|
|
|
legacyThreadIdToIdMap[thread.uniqueId] = publicKey
|
|
closedGroupName[thread.uniqueId] = groupThread.groupModel.groupName
|
|
closedGroupModel[thread.uniqueId] = groupThread.groupModel
|
|
closedGroupFormation[thread.uniqueId] = ((transaction.object(forKey: publicKey, inCollection: SMKLegacy.closedGroupFormationTimestampCollection) as? UInt64) ?? 0)
|
|
closedGroupZombieMemberIds[thread.uniqueId] = transaction.object(
|
|
forKey: publicKey,
|
|
inCollection: SMKLegacy.closedGroupZombieMembersCollection
|
|
) as? Set<String>
|
|
|
|
// Note: If the user is no longer in a closed group then the group will still exist but the user
|
|
// won't have the closed group public key anymore
|
|
let keyCollection: String = "\(SMKLegacy.closedGroupKeyPairPrefix)\(publicKey)"
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: keyCollection) { key, object, _ in
|
|
guard
|
|
let timestamp: TimeInterval = TimeInterval(key),
|
|
let keyPair: SUKLegacy.KeyPair = object as? SUKLegacy.KeyPair
|
|
else { return }
|
|
|
|
closedGroupKeys[thread.uniqueId] = (closedGroupKeys[thread.uniqueId] ?? [:])
|
|
.setting(timestamp, keyPair)
|
|
}
|
|
}
|
|
else if groupThread.isOpenGroup {
|
|
guard let openGroup: SMKLegacy._OpenGroup = transaction.object(forKey: thread.uniqueId, inCollection: SMKLegacy.openGroupCollection) as? SMKLegacy._OpenGroup else {
|
|
SNLog("[Migration Error] Unable to find open group info")
|
|
shouldFailMigration = true
|
|
return
|
|
}
|
|
|
|
// We want to migrate everyone over to using the domain name for open group
|
|
// servers rather than the IP, also best to use HTTPS over HTTP where possible
|
|
// so catch the case where we have the domain with HTTP (the 'defaultServer'
|
|
// value contains a HTTPS scheme so we get IP HTTP -> HTTPS for free as well)
|
|
let processedOpenGroupServer: String = {
|
|
// Check if the server is a Session-run one based on it's
|
|
guard OpenGroupManager.isSessionRunOpenGroup(server: openGroup.server) else {
|
|
return openGroup.server
|
|
}
|
|
|
|
return OpenGroupAPI.defaultServer
|
|
}()
|
|
legacyThreadIdToIdMap[thread.uniqueId] = OpenGroup.idFor(
|
|
roomToken: openGroup.room,
|
|
server: processedOpenGroupServer
|
|
)
|
|
openGroupServer[thread.uniqueId] = processedOpenGroupServer
|
|
openGroupInfo[thread.uniqueId] = openGroup
|
|
openGroupUserCount[thread.uniqueId] = ((transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupUserCountCollection) as? Int64) ?? 0)
|
|
openGroupImage[thread.uniqueId] = transaction.object(forKey: openGroup.id, inCollection: SMKLegacy.openGroupImageCollection) as? Data
|
|
}
|
|
}
|
|
Storage.update(progress: 0.04, for: self, in: target)
|
|
|
|
// MARK: --Interactions
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Interactions")
|
|
|
|
/// **Note:** There is no index on the collection column so unfortunately it takes the same amount of time to enumerate through all
|
|
/// collections as it does to just get the count of collections, due to this, if the database is very large, importing thecollections can be
|
|
/// very slow (~15s with 2,000,000 rows) - we want to show some kind of progress while enumerating so the below code creates a
|
|
/// very rought guess of the number of collections based on the file size of the database (this shouldn't affect most users at all)
|
|
let roughKbPerRow: CGFloat = 2.25
|
|
let oldDatabaseSizeBytes: CGFloat = (try? FileManager.default
|
|
.attributesOfItem(atPath: SUKLegacy.legacyDatabaseFilepath)[.size]
|
|
.asType(CGFloat.self))
|
|
.defaulting(to: 0)
|
|
let roughNumRows: CGFloat = ((oldDatabaseSizeBytes / 1024) / roughKbPerRow)
|
|
let startProgress: CGFloat = 0.04
|
|
let interactionsCompleteProgress: CGFloat = 0.19
|
|
var rowIndex: CGFloat = 0
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.interactionCollection) { _, object, _ in
|
|
guard let interaction: SMKLegacy._DBInteraction = object as? SMKLegacy._DBInteraction else {
|
|
SNLog("[Migration Error] Unable to process interaction")
|
|
shouldFailMigration = true
|
|
return
|
|
}
|
|
|
|
/// Prune interactions from OpenGroup thread interactions which are older than 6 months
|
|
///
|
|
/// The old structure for the open group id was `g{base64String(Data(__loki_public_chat_group__!{server.room}))}
|
|
/// so we process the uniqueThreadId to see if it matches that
|
|
if
|
|
interaction.uniqueThreadId.starts(with: SMKLegacy.groupThreadPrefix),
|
|
let base64Data: Data = Data(base64Encoded: interaction.uniqueThreadId.substring(from: SMKLegacy.groupThreadPrefix.count)),
|
|
let groupIdString: String = String(data: base64Data, encoding: .utf8),
|
|
(
|
|
groupIdString.starts(with: SMKLegacy.openGroupIdPrefix) ||
|
|
groupIdString.starts(with: "http")
|
|
),
|
|
interaction.timestamp < UInt64(floor((timestampNow - GarbageCollectionJob.approxSixMonthsInSeconds) * 1000))
|
|
{
|
|
return
|
|
}
|
|
|
|
interactions[interaction.uniqueThreadId] = (interactions[interaction.uniqueThreadId] ?? [])
|
|
.appending(interaction)
|
|
|
|
rowIndex += 1
|
|
|
|
Storage.update(
|
|
progress: min(
|
|
interactionsCompleteProgress,
|
|
((rowIndex / roughNumRows) * (interactionsCompleteProgress - startProgress))
|
|
),
|
|
for: self,
|
|
in: target
|
|
)
|
|
}
|
|
Storage.update(progress: interactionsCompleteProgress, for: self, in: target)
|
|
|
|
// MARK: --Attachments
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Attachments")
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.attachmentsCollection) { key, object, _ in
|
|
guard let attachment: SMKLegacy._Attachment = object as? SMKLegacy._Attachment else {
|
|
SNLog("[Migration Error] Unable to process attachment")
|
|
shouldFailMigration = true
|
|
return
|
|
}
|
|
|
|
attachments[key] = attachment
|
|
}
|
|
Storage.update(progress: 0.21, for: self, in: target)
|
|
|
|
// MARK: --Read Receipts
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.outgoingReadReceiptManagerCollection) { key, object, _ in
|
|
guard let timestampsMs: Set<Int64> = object as? Set<Int64> else { return }
|
|
|
|
outgoingReadReceiptsTimestampsMs[key] = (outgoingReadReceiptsTimestampsMs[key] ?? Set())
|
|
.union(timestampsMs)
|
|
}
|
|
|
|
// MARK: --De-duping
|
|
|
|
receivedMessageTimestamps = receivedMessageTimestamps.inserting(
|
|
contentsOf: transaction
|
|
.object(
|
|
forKey: SMKLegacy.receivedMessageTimestampsKey,
|
|
inCollection: SMKLegacy.receivedMessageTimestampsCollection
|
|
)
|
|
.asType([UInt64].self)
|
|
.defaulting(to: [])
|
|
.asSet()
|
|
)
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.receivedCallsCollection) { key, object, _ in
|
|
guard let uuids: Set<String> = object as? Set<String> else { return }
|
|
|
|
receivedCallUUIDs[key] = (receivedCallUUIDs[key] ?? Set())
|
|
.union(uuids)
|
|
}
|
|
|
|
// MARK: --Jobs
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Jobs")
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.notifyPushServerJobCollection) { _, object, _, _ in
|
|
guard let job = object as? SMKLegacy._NotifyPNServerJob else { return }
|
|
notifyPushServerJobs.insert(job)
|
|
}
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.messageReceiveJobCollection) { _, object, _, _ in
|
|
guard let job = object as? SMKLegacy._MessageReceiveJob else { return }
|
|
messageReceiveJobs.insert(job)
|
|
}
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.messageSendJobCollection) { _, object, _, _ in
|
|
guard let job = object as? SMKLegacy._MessageSendJob else { return }
|
|
messageSendJobs.insert(job)
|
|
}
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.attachmentUploadJobCollection) { _, object, _, _ in
|
|
guard let job = object as? SMKLegacy._AttachmentUploadJob else { return }
|
|
attachmentUploadJobs.insert(job)
|
|
}
|
|
|
|
transaction.enumerateRows(inCollection: SMKLegacy.attachmentDownloadJobCollection) { _, object, _, _ in
|
|
guard let job = object as? SMKLegacy._AttachmentDownloadJob else { return }
|
|
attachmentDownloadJobs.insert(job)
|
|
}
|
|
Storage.update(progress: 0.22, for: self, in: target)
|
|
|
|
// MARK: --Preferences
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Processing Preferences")
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.preferencesCollection) { key, object, _ in
|
|
legacyPreferences[key] = object
|
|
}
|
|
|
|
transaction.enumerateKeysAndObjects(inCollection: SMKLegacy.additionalPreferencesCollection) { key, object, _ in
|
|
legacyPreferences[key] = object
|
|
}
|
|
|
|
// Note: The 'int(forKey:inCollection:)' defaults to `0` which is an incorrect value
|
|
// for the notification sound so catch it and default
|
|
legacyPreferences[SMKLegacy.soundsGlobalNotificationKey] = (transaction
|
|
.object(
|
|
forKey: SMKLegacy.soundsGlobalNotificationKey,
|
|
inCollection: SMKLegacy.soundsStorageNotificationCollection
|
|
)
|
|
.asType(NSNumber.self)?
|
|
.intValue)
|
|
.defaulting(to: Preferences.Sound.defaultNotificationSound.rawValue)
|
|
|
|
legacyPreferences[SMKLegacy.readReceiptManagerAreReadReceiptsEnabled] = (transaction
|
|
.object(
|
|
forKey: SMKLegacy.readReceiptManagerAreReadReceiptsEnabled,
|
|
inCollection: SMKLegacy.readReceiptManagerCollection
|
|
)
|
|
.asType(NSNumber.self)?
|
|
.boolValue)
|
|
.defaulting(to: false)
|
|
|
|
legacyPreferences[SMKLegacy.typingIndicatorsEnabledKey] = (transaction
|
|
.object(
|
|
forKey: SMKLegacy.typingIndicatorsEnabledKey,
|
|
inCollection: SMKLegacy.typingIndicatorsCollection
|
|
)
|
|
.asType(NSNumber.self)?
|
|
.boolValue)
|
|
.defaulting(to: false)
|
|
|
|
legacyPreferences[SMKLegacy.screenLockIsScreenLockEnabledKey] = (transaction
|
|
.object(
|
|
forKey: SMKLegacy.screenLockIsScreenLockEnabledKey,
|
|
inCollection: SMKLegacy.screenLockCollection
|
|
)
|
|
.asType(NSNumber.self)?
|
|
.boolValue)
|
|
.defaulting(to: false)
|
|
|
|
legacyPreferences[SMKLegacy.screenLockScreenLockTimeoutSecondsKey] = (transaction
|
|
.object(
|
|
forKey: SMKLegacy.screenLockScreenLockTimeoutSecondsKey,
|
|
inCollection: SMKLegacy.screenLockCollection)
|
|
.asType(NSNumber.self)?
|
|
.doubleValue)
|
|
.defaulting(to: (15 * 60))
|
|
Storage.update(progress: 0.23, for: self, in: target)
|
|
}
|
|
|
|
// We can't properly throw within the 'enumerateKeysAndObjects' block so have to throw here
|
|
guard !shouldFailMigration else { throw StorageError.migrationFailed }
|
|
|
|
// Insert the data into GRDB
|
|
|
|
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
|
|
// MARK: - Insert Contacts
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Inserting Contacts")
|
|
|
|
try autoreleasepool {
|
|
// Values for contact progress
|
|
let contactStartProgress: CGFloat = 0.23
|
|
let progressPerContact: CGFloat = (0.05 / CGFloat(contacts.count))
|
|
|
|
try contacts.enumerated().forEach { index, legacyContact in
|
|
let isCurrentUser: Bool = (legacyContact.sessionID == currentUserPublicKey)
|
|
let contactThreadId: String = SMKLegacy._ContactThread.threadId(from: legacyContact.sessionID)
|
|
|
|
// Create the "Profile" for the legacy contact
|
|
try Profile(
|
|
id: legacyContact.sessionID,
|
|
name: (legacyContact.name ?? legacyContact.sessionID),
|
|
nickname: legacyContact.nickname,
|
|
profilePictureUrl: legacyContact.profilePictureURL,
|
|
profilePictureFileName: legacyContact.profilePictureFileName,
|
|
profileEncryptionKey: legacyContact.profileEncryptionKey
|
|
).migrationSafeInsert(db)
|
|
|
|
/// **Note:** The blow "shouldForce" flags are here to allow us to avoid having to run legacy migrations they
|
|
/// replicate the behaviour of a number of the migrations and perform the changes if the migrations had never run
|
|
|
|
/// `ContactsMigration` - Marked all existing contacts as trusted
|
|
let shouldForceTrustContact: Bool = (!legacyMigrations.contains(.contactsMigration))
|
|
|
|
/// `MessageRequestsMigration` - Marked all existing contacts as isApproved and didApproveMe
|
|
let shouldForceApproveContact: Bool = (!legacyMigrations.contains(.messageRequestsMigration))
|
|
|
|
/// `BlockingManagerRemovalMigration` - Removed the old blocking manager and updated contacts isBlocked flag accordingly
|
|
let shouldForceBlockContact: Bool = (
|
|
!legacyMigrations.contains(.messageRequestsMigration) &&
|
|
legacyBlockedSessionIds.contains(legacyContact.sessionID)
|
|
)
|
|
|
|
/// Looks like there are some cases where conversations would be visible in the old version but wouldn't in the new version
|
|
/// it seems to be related to the `isApproved` and `didApproveMe` not being set correctly somehow, this logic is to
|
|
/// ensure the flags are set correctly based on sent/received messages
|
|
let interactionsForContact: [SMKLegacy._DBInteraction] = (interactions["\(SMKLegacy.contactThreadPrefix)\(legacyContact.sessionID)"] ?? [])
|
|
let shouldForceIsApproved: Bool = interactionsForContact
|
|
.contains(where: { $0 is SMKLegacy._DBOutgoingMessage })
|
|
let shouldForceDidApproveMe: Bool = interactionsForContact
|
|
.contains(where: { $0 is SMKLegacy._DBIncomingMessage })
|
|
|
|
// 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) ||
|
|
legacyContact.isApproved ||
|
|
legacyContact.didApproveMe ||
|
|
legacyContact.isBlocked ||
|
|
legacyContact.hasBeenBlocked ||
|
|
shouldForceTrustContact ||
|
|
shouldForceApproveContact ||
|
|
shouldForceBlockContact ||
|
|
shouldForceIsApproved ||
|
|
shouldForceDidApproveMe
|
|
{
|
|
// Create the contact
|
|
try Contact(
|
|
id: legacyContact.sessionID,
|
|
isTrusted: (
|
|
isCurrentUser ||
|
|
legacyContact.isTrusted ||
|
|
shouldForceTrustContact
|
|
),
|
|
isApproved: (
|
|
isCurrentUser ||
|
|
legacyContact.isApproved ||
|
|
shouldForceApproveContact ||
|
|
shouldForceIsApproved
|
|
),
|
|
isBlocked: (
|
|
!isCurrentUser && (
|
|
legacyContact.isBlocked ||
|
|
shouldForceBlockContact
|
|
)
|
|
),
|
|
didApproveMe: (
|
|
isCurrentUser ||
|
|
legacyContact.didApproveMe ||
|
|
shouldForceApproveContact ||
|
|
shouldForceDidApproveMe
|
|
),
|
|
hasBeenBlocked: (!isCurrentUser && (legacyContact.hasBeenBlocked || legacyContact.isBlocked))
|
|
).migrationSafeInsert(db)
|
|
}
|
|
|
|
// Increment the progress for each contact
|
|
Storage.update(
|
|
progress: contactStartProgress + (progressPerContact * CGFloat(index + 1)),
|
|
for: self,
|
|
in: target
|
|
)
|
|
}
|
|
}
|
|
|
|
// Clear out processed data (give the memory a change to be freed)
|
|
contacts = []
|
|
legacyBlockedSessionIds = []
|
|
contactThreadIds = []
|
|
|
|
// MARK: - Insert Threads
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Inserting Threads & Interactions")
|
|
|
|
var legacyInteractionToIdMap: [String: Int64] = [:]
|
|
var legacyInteractionIdentifierToIdMap: [String: Int64] = [:]
|
|
var legacyInteractionIdentifierToIdFallbackMap: [String: Int64] = [:]
|
|
|
|
func identifier(
|
|
for threadId: String,
|
|
sentTimestamp: UInt64,
|
|
recipients: [String],
|
|
destination: Message.Destination?,
|
|
variant: Interaction.Variant?,
|
|
useFallback: Bool
|
|
) -> String {
|
|
let recipientString: String = {
|
|
if let destination: Message.Destination = destination {
|
|
switch destination {
|
|
case .contact(let publicKey): return publicKey
|
|
default: break
|
|
}
|
|
}
|
|
|
|
return (recipients.first ?? "0")
|
|
}()
|
|
|
|
return [
|
|
(useFallback ?
|
|
// Fallback to seconds-based accuracy (instead of milliseconds)
|
|
String("\(sentTimestamp)".prefix("\(Int(Date().timeIntervalSince1970))".count)) :
|
|
"\(sentTimestamp)"
|
|
),
|
|
(useFallback ? variant.map { "\($0)" } : nil),
|
|
recipientString,
|
|
threadId
|
|
]
|
|
.compactMap { $0 }
|
|
.joined(separator: "-")
|
|
}
|
|
|
|
// Values for thread progress
|
|
var interactionCounter: CGFloat = 0
|
|
let allInteractionsCount: Int = interactions.map { $0.value.count }.reduce(0, +)
|
|
let threadInteractionsStartProgress: CGFloat = 0.28
|
|
let progressPerInteraction: CGFloat = (0.70 / CGFloat(allInteractionsCount))
|
|
|
|
// Sort by id just so we can make the migration process more determinstic
|
|
try legacyThreads.sorted(by: { lhs, rhs in lhs.uniqueId < rhs.uniqueId }).forEach { legacyThread in
|
|
guard let threadId: String = legacyThreadIdToIdMap[legacyThread.uniqueId] else {
|
|
SNLog("[Migration Error] Unable to migrate thread with no id mapping")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
let threadVariant: SessionThread.Variant
|
|
let onlyNotifyForMentions: Bool
|
|
|
|
switch legacyThread {
|
|
case let groupThread as SMKLegacy._GroupThread:
|
|
threadVariant = (groupThread.isOpenGroup ? .openGroup : .closedGroup)
|
|
onlyNotifyForMentions = groupThread.isOnlyNotifyingForMentions
|
|
|
|
default:
|
|
threadVariant = .contact
|
|
onlyNotifyForMentions = false
|
|
}
|
|
|
|
try autoreleasepool {
|
|
try SessionThread(
|
|
id: threadId,
|
|
variant: threadVariant,
|
|
creationDateTimestamp: legacyThread.creationDate.timeIntervalSince1970,
|
|
shouldBeVisible: legacyThread.shouldBeVisible,
|
|
isPinned: legacyThread.isPinned,
|
|
messageDraft: ((legacyThread.messageDraft ?? "").isEmpty ?
|
|
nil :
|
|
legacyThread.messageDraft
|
|
),
|
|
mutedUntilTimestamp: legacyThread.mutedUntilDate?.timeIntervalSince1970,
|
|
onlyNotifyForMentions: onlyNotifyForMentions
|
|
).migrationSafeInsert(db)
|
|
|
|
// Disappearing Messages Configuration
|
|
if let config: SMKLegacy._DisappearingMessagesConfiguration = disappearingMessagesConfiguration[threadId] {
|
|
try DisappearingMessagesConfiguration(
|
|
threadId: threadId,
|
|
isEnabled: config.isEnabled,
|
|
durationSeconds: TimeInterval(config.durationSeconds)
|
|
).migrationSafeInsert(db)
|
|
}
|
|
else {
|
|
try DisappearingMessagesConfiguration
|
|
.defaultWith(threadId)
|
|
.migrationSafeInsert(db)
|
|
}
|
|
|
|
// Closed Groups
|
|
if legacyThread.isClosedGroup {
|
|
guard
|
|
let name: String = closedGroupName[legacyThread.uniqueId],
|
|
let groupModel: SMKLegacy._GroupModel = closedGroupModel[legacyThread.uniqueId],
|
|
let formationTimestamp: UInt64 = closedGroupFormation[legacyThread.uniqueId]
|
|
else {
|
|
SNLog("[Migration Error] Closed group missing required data")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
try ClosedGroup(
|
|
threadId: threadId,
|
|
name: name,
|
|
formationTimestamp: TimeInterval(formationTimestamp)
|
|
).migrationSafeInsert(db)
|
|
|
|
// Note: If a user has left a closed group then they won't actually have any keys
|
|
// but they should still be able to browse the old messages so we do want to allow
|
|
// this case and migrate the rest of the info
|
|
try closedGroupKeys[legacyThread.uniqueId]?.forEach { timestamp, legacyKeys in
|
|
try ClosedGroupKeyPair(
|
|
threadId: threadId,
|
|
publicKey: legacyKeys.publicKey,
|
|
secretKey: legacyKeys.privateKey,
|
|
receivedTimestamp: timestamp
|
|
).migrationSafeInsert(db)
|
|
}
|
|
|
|
// Create the 'GroupMember' models for the group (even if the current user is no longer
|
|
// a member as these objects are used to generate the group avatar icon)
|
|
func createDummyProfile(profileId: String) {
|
|
SNLog("[Migration Warning] Closed group member with unknown user found - Creating empty profile")
|
|
|
|
// Note: Need to upsert here because it's possible multiple quotes
|
|
// will use the same invalid 'authorId' value resulting in a unique
|
|
// constraint violation
|
|
try? Profile(
|
|
id: profileId,
|
|
name: profileId
|
|
).migrationSafeSave(db)
|
|
}
|
|
|
|
try groupModel.groupMemberIds.forEach { memberId in
|
|
try GroupMember(
|
|
groupId: threadId,
|
|
profileId: memberId,
|
|
role: .standard,
|
|
isHidden: false // Ignored: Didn't exist at time of migration
|
|
).migrationSafeInsert(db)
|
|
|
|
if !validProfileIds.contains(memberId) {
|
|
createDummyProfile(profileId: memberId)
|
|
}
|
|
}
|
|
|
|
try groupModel.groupAdminIds.forEach { adminId in
|
|
try GroupMember(
|
|
groupId: threadId,
|
|
profileId: adminId,
|
|
role: .admin,
|
|
isHidden: false // Ignored: Didn't exist at time of migration
|
|
).migrationSafeInsert(db)
|
|
|
|
if !validProfileIds.contains(adminId) {
|
|
createDummyProfile(profileId: adminId)
|
|
}
|
|
}
|
|
|
|
try (closedGroupZombieMemberIds[legacyThread.uniqueId] ?? []).forEach { zombieId in
|
|
try GroupMember(
|
|
groupId: threadId,
|
|
profileId: zombieId,
|
|
role: .zombie,
|
|
isHidden: false // Ignored: Didn't exist at time of migration
|
|
).migrationSafeInsert(db)
|
|
|
|
if !validProfileIds.contains(zombieId) {
|
|
createDummyProfile(profileId: zombieId)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Open Groups
|
|
if legacyThread.isOpenGroup {
|
|
guard
|
|
let openGroup: SMKLegacy._OpenGroup = openGroupInfo[legacyThread.uniqueId],
|
|
let targetOpenGroupServer: String = openGroupServer[legacyThread.uniqueId]
|
|
else {
|
|
SNLog("[Migration Error] Open group missing required data")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
try OpenGroup(
|
|
server: targetOpenGroupServer,
|
|
roomToken: openGroup.room,
|
|
publicKey: openGroup.publicKey,
|
|
isActive: true,
|
|
name: openGroup.name,
|
|
roomDescription: nil,
|
|
imageId: openGroup.imageID,
|
|
imageData: openGroupImage[legacyThread.uniqueId],
|
|
userCount: (openGroupUserCount[legacyThread.uniqueId] ?? 0), // Will be updated next poll
|
|
infoUpdates: 0,
|
|
sequenceNumber: 0,
|
|
inboxLatestMessageId: 0,
|
|
outboxLatestMessageId: 0
|
|
)
|
|
.migrationSafeInsert(db)
|
|
}
|
|
}
|
|
|
|
try autoreleasepool {
|
|
try interactions[legacyThread.uniqueId]?
|
|
.sorted(by: { lhs, rhs in lhs.timestamp < rhs.timestamp }) // Maintain sort order
|
|
.forEach { legacyInteraction in
|
|
let serverHash: String?
|
|
let variant: Interaction.Variant
|
|
let authorId: String
|
|
let body: String?
|
|
let wasRead: Bool
|
|
let expiresInSeconds: UInt32?
|
|
let expiresStartedAtMs: UInt64?
|
|
let openGroupServerMessageId: Int64?
|
|
let recipientStateMap: [String: SMKLegacy._DBOutgoingMessageRecipientState]?
|
|
let mostRecentFailureText: String?
|
|
let quotedMessage: SMKLegacy._DBQuotedMessage?
|
|
let linkPreview: SMKLegacy._DBLinkPreview?
|
|
let linkPreviewVariant: LinkPreview.Variant
|
|
var attachmentIds: [String]
|
|
|
|
// Handle the common 'SMKLegacy._DBMessage' values first
|
|
if let legacyMessage: SMKLegacy._DBMessage = legacyInteraction as? SMKLegacy._DBMessage {
|
|
serverHash = legacyMessage.serverHash
|
|
|
|
// 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)
|
|
//
|
|
// Note: Looks like it was also possible for this to be set to the max
|
|
// value which overflows when trying to convert to a signed version so
|
|
// we essentially discard the information in those cases)
|
|
openGroupServerMessageId = (Int64.zeroingOverflow(legacyMessage.openGroupServerMessageID) == 0 ?
|
|
nil :
|
|
Int64.zeroingOverflow(legacyMessage.openGroupServerMessageID)
|
|
)
|
|
quotedMessage = legacyMessage.quotedMessage
|
|
|
|
// Convert the 'OpenGroupInvitation' into a LinkPreview
|
|
if let openGroupInvitationName: String = legacyMessage.openGroupInvitationName, let openGroupInvitationUrl: String = legacyMessage.openGroupInvitationURL {
|
|
linkPreviewVariant = .openGroupInvitation
|
|
linkPreview = SMKLegacy._DBLinkPreview(
|
|
urlString: openGroupInvitationUrl,
|
|
title: openGroupInvitationName,
|
|
imageAttachmentId: nil
|
|
)
|
|
}
|
|
else {
|
|
linkPreviewVariant = .standard
|
|
linkPreview = legacyMessage.linkPreview
|
|
}
|
|
|
|
// Attachments for deleted messages won't exist
|
|
attachmentIds = (legacyMessage.isDeleted ?
|
|
[] :
|
|
legacyMessage.attachmentIds
|
|
)
|
|
}
|
|
else {
|
|
serverHash = nil
|
|
openGroupServerMessageId = nil
|
|
quotedMessage = nil
|
|
linkPreviewVariant = .standard
|
|
linkPreview = nil
|
|
attachmentIds = []
|
|
}
|
|
|
|
// Then handle the behaviours for each message type
|
|
switch legacyInteraction {
|
|
case let incomingMessage as SMKLegacy._DBIncomingMessage:
|
|
// Note: We want to distinguish deleted messages from normal ones
|
|
variant = (incomingMessage.isDeleted ?
|
|
.standardIncomingDeleted :
|
|
.standardIncoming
|
|
)
|
|
authorId = incomingMessage.authorId
|
|
body = incomingMessage.body
|
|
wasRead = incomingMessage.wasRead
|
|
expiresInSeconds = incomingMessage.expiresInSeconds
|
|
expiresStartedAtMs = incomingMessage.expireStartedAt
|
|
recipientStateMap = [:]
|
|
mostRecentFailureText = nil
|
|
|
|
case let outgoingMessage as SMKLegacy._DBOutgoingMessage:
|
|
variant = .standardOutgoing
|
|
authorId = currentUserPublicKey
|
|
body = outgoingMessage.body
|
|
wasRead = true // Outgoing messages are read by default
|
|
expiresInSeconds = outgoingMessage.expiresInSeconds
|
|
expiresStartedAtMs = outgoingMessage.expireStartedAt
|
|
recipientStateMap = outgoingMessage.recipientStateMap
|
|
mostRecentFailureText = outgoingMessage.mostRecentFailureText
|
|
|
|
case let infoMessage as SMKLegacy._DBInfoMessage:
|
|
// Note: The legacy 'TSInfoMessage' didn't store the author id so there is no
|
|
// way to determine who actually triggered the info message
|
|
authorId = currentUserPublicKey
|
|
body = {
|
|
// Note: Some message types stored additional info and constructed a string
|
|
// at display time, instead we encode the data into the body of the message
|
|
// as JSON so we want to continue that behaviour but not change the database
|
|
// structure for some edge cases
|
|
switch infoMessage.messageType {
|
|
case .disappearingMessagesUpdate:
|
|
guard
|
|
let updateMessage: SMKLegacy._DisappearingConfigurationUpdateInfoMessage = infoMessage as? SMKLegacy._DisappearingConfigurationUpdateInfoMessage,
|
|
let infoMessageData: Data = try? JSONEncoder().encode(
|
|
DisappearingMessagesConfiguration.MessageInfo(
|
|
senderName: updateMessage.createdByRemoteName,
|
|
isEnabled: updateMessage.configurationIsEnabled,
|
|
durationSeconds: TimeInterval(updateMessage.configurationDurationSeconds)
|
|
)
|
|
),
|
|
let infoMessageString: String = String(data: infoMessageData, encoding: .utf8)
|
|
else { break }
|
|
|
|
return infoMessageString
|
|
|
|
case .call:
|
|
let messageInfo: CallMessage.MessageInfo = CallMessage.MessageInfo(
|
|
state: {
|
|
switch infoMessage.callState {
|
|
case .incoming: return .incoming
|
|
case .outgoing: return .outgoing
|
|
case .missed: return .missed
|
|
case .permissionDenied: return .permissionDenied
|
|
case .unknown: return .unknown
|
|
}
|
|
}()
|
|
)
|
|
|
|
guard
|
|
let messageInfoData: Data = try? JSONEncoder().encode(messageInfo),
|
|
let messageInfoDataString: String = String(data: messageInfoData, encoding: .utf8)
|
|
else { break }
|
|
|
|
return messageInfoDataString
|
|
|
|
default: break
|
|
}
|
|
|
|
return ((infoMessage.body ?? "").isEmpty ?
|
|
infoMessage.customMessage :
|
|
infoMessage.body
|
|
)
|
|
}()
|
|
wasRead = infoMessage.wasRead
|
|
expiresInSeconds = nil // Info messages don't expire
|
|
expiresStartedAtMs = nil // Info messages don't expire
|
|
recipientStateMap = [:]
|
|
mostRecentFailureText = nil
|
|
|
|
switch infoMessage.messageType {
|
|
case .groupCreated: variant = .infoClosedGroupCreated
|
|
case .groupUpdated: variant = .infoClosedGroupUpdated
|
|
case .groupCurrentUserLeft: variant = .infoClosedGroupCurrentUserLeft
|
|
case .disappearingMessagesUpdate: variant = .infoDisappearingMessagesUpdate
|
|
case .screenshotNotification: variant = .infoScreenshotNotification
|
|
case .mediaSavedNotification: variant = .infoMediaSavedNotification
|
|
case .call: variant = .infoCall
|
|
case .messageRequestAccepted: variant = .infoMessageRequestAccepted
|
|
}
|
|
|
|
default:
|
|
SNLog("[Migration Error] Unsupported interaction type")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
// Insert the data
|
|
let interaction: Interaction
|
|
|
|
do {
|
|
interaction = try Interaction(
|
|
serverHash: {
|
|
switch variant {
|
|
// Don't store the 'serverHash' for these so sync messages
|
|
// are seen as duplicates
|
|
case .infoDisappearingMessagesUpdate: return nil
|
|
|
|
default: return serverHash
|
|
}
|
|
}(),
|
|
messageUuid: {
|
|
guard variant == .infoCall else { return nil }
|
|
|
|
/// **Note:** Unfortunately there is no good way to properly match this UUID up with the correct
|
|
/// interaction (and it was previously stored as a Set so the values will be unsorted anyway); luckily
|
|
/// we are only using this value for updating and de-duping purposes at this stage so it _shouldn't_
|
|
/// matter if the values end up being assigned to the wrong interactions, we do still want to try and
|
|
/// store each value through so mutate the list as we process each UUID
|
|
///
|
|
/// **Note:** It looks like these values were stored against the sessionId rather than the legacy
|
|
/// thread unique id
|
|
return receivedCallUUIDs[threadId]?.popFirst()
|
|
}(),
|
|
threadId: threadId,
|
|
authorId: authorId,
|
|
variant: variant,
|
|
body: body,
|
|
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp),
|
|
receivedAtTimestampMs: Int64.zeroingOverflow(legacyInteraction.receivedAtTimestamp),
|
|
wasRead: wasRead,
|
|
hasMention: Interaction.isUserMentioned(
|
|
db,
|
|
threadId: threadId,
|
|
body: body,
|
|
quoteAuthorId: quotedMessage?.authorId
|
|
),
|
|
// For both of these '0' used to be equivalent to null
|
|
expiresInSeconds: ((expiresInSeconds ?? 0) > 0 ?
|
|
expiresInSeconds.map { TimeInterval($0) } :
|
|
nil
|
|
),
|
|
expiresStartedAtMs: ((expiresStartedAtMs ?? 0) > 0 ?
|
|
expiresStartedAtMs.map { Double($0) } :
|
|
nil
|
|
),
|
|
linkPreviewUrl: linkPreview?.urlString, // Only a soft link so save to set
|
|
openGroupServerMessageId: openGroupServerMessageId,
|
|
openGroupWhisperMods: false,
|
|
openGroupWhisperTo: nil
|
|
).migrationSafeInserted(db)
|
|
}
|
|
catch {
|
|
switch error {
|
|
// Ignore duplicate interactions
|
|
case DatabaseError.SQLITE_CONSTRAINT_UNIQUE:
|
|
SNLog("[Migration Warning] Found duplicate message of variant: \(variant); skipping")
|
|
return
|
|
|
|
default:
|
|
SNLog("[Migration Error] Failed to insert interaction")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
}
|
|
|
|
// Insert a 'ControlMessageProcessRecord' if needed (for duplication prevention)
|
|
try ControlMessageProcessRecord(
|
|
threadId: threadId,
|
|
variant: variant,
|
|
timestampMs: Int64.zeroingOverflow(legacyInteraction.timestamp)
|
|
)?.migrationSafeInsert(db)
|
|
|
|
// Remove timestamps we created records for (they will be protected by unique
|
|
// constraints so don't need legacy process records)
|
|
receivedMessageTimestamps.remove(legacyInteraction.timestamp)
|
|
|
|
guard let interactionId: Int64 = interaction.id else {
|
|
SNLog("[Migration Error] Failed to insert interaction")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
// Store the interactionId in the lookup map to simplify job creation later
|
|
let legacyIdentifier: String = identifier(
|
|
for: threadId,
|
|
sentTimestamp: legacyInteraction.timestamp,
|
|
recipients: ((legacyInteraction as? SMKLegacy._DBOutgoingMessage)?
|
|
.recipientStateMap?
|
|
.keys
|
|
.map { $0 })
|
|
.defaulting(to: []),
|
|
destination: (threadVariant == .contact ? .contact(publicKey: threadId) : nil),
|
|
variant: variant,
|
|
useFallback: false
|
|
)
|
|
let legacyIdentifierFallback: String = identifier(
|
|
for: threadId,
|
|
sentTimestamp: legacyInteraction.timestamp,
|
|
recipients: ((legacyInteraction as? SMKLegacy._DBOutgoingMessage)?
|
|
.recipientStateMap?
|
|
.keys
|
|
.map { $0 })
|
|
.defaulting(to: []),
|
|
destination: (threadVariant == .contact ? .contact(publicKey: threadId) : nil),
|
|
variant: variant,
|
|
useFallback: true
|
|
)
|
|
|
|
legacyInteractionToIdMap[legacyInteraction.uniqueId] = interactionId
|
|
legacyInteractionIdentifierToIdMap[legacyIdentifier] = interactionId
|
|
legacyInteractionIdentifierToIdFallbackMap[legacyIdentifierFallback] = interactionId
|
|
|
|
// Handle the recipient states
|
|
|
|
// Note: Inserting an Interaction into the database will automatically create a 'RecipientState'
|
|
// for outgoing messages
|
|
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
|
|
}
|
|
}(),
|
|
readTimestampMs: legacyState.readTimestamp,
|
|
mostRecentFailureText: (legacyState.state == .failed ?
|
|
mostRecentFailureText :
|
|
nil
|
|
)
|
|
).migrationSafeSave(db)
|
|
}
|
|
|
|
// Handle any quote
|
|
|
|
if let quotedMessage: SMKLegacy._DBQuotedMessage = quotedMessage {
|
|
var quoteAttachmentId: String? = quotedMessage.quotedAttachments
|
|
.flatMap { attachmentInfo in
|
|
return [
|
|
// Prioritise the thumbnail as it means we won't
|
|
// need to generate a new one
|
|
attachmentInfo.thumbnailAttachmentStreamId,
|
|
attachmentInfo.thumbnailAttachmentPointerId,
|
|
attachmentInfo.attachmentId
|
|
]
|
|
.compactMap { $0 }
|
|
}
|
|
.first { attachmentId -> Bool in attachments[attachmentId] != nil }
|
|
|
|
// It looks like there can be cases where a quote can be quoting an
|
|
// interaction that isn't associated with a profile we know about (eg.
|
|
// if you join an open group and one of the first messages is a quote of
|
|
// an older message not cached to the device) - this will cause a foreign
|
|
// key constraint violation so in these cases just create an empty profile
|
|
if !validProfileIds.contains(quotedMessage.authorId) {
|
|
SNLog("[Migration Warning] Quote with unknown author found - Creating empty profile")
|
|
|
|
// Note: Need to upsert here because it's possible multiple quotes
|
|
// will use the same invalid 'authorId' value resulting in a unique
|
|
// constraint violation
|
|
try Profile(
|
|
id: quotedMessage.authorId,
|
|
name: quotedMessage.authorId
|
|
).migrationSafeSave(db)
|
|
}
|
|
|
|
// Note: It looks like there is a way for a quote to not have it's
|
|
// associated attachmentId so let's try our best to track down the
|
|
// original interaction and re-create the attachment link before
|
|
// falling back to having no attachment in the quote
|
|
if quoteAttachmentId == nil && !quotedMessage.quotedAttachments.isEmpty {
|
|
quoteAttachmentId = interactions[legacyThread.uniqueId]?
|
|
.first(where: {
|
|
$0.timestamp == quotedMessage.timestamp &&
|
|
(
|
|
// Outgoing messages don't store the 'authorId' so we
|
|
// need to compare against the 'currentUserPublicKey'
|
|
// for those or cast to a TSIncomingMessage otherwise
|
|
quotedMessage.authorId == currentUserPublicKey ||
|
|
quotedMessage.authorId == ($0 as? SMKLegacy._DBIncomingMessage)?.authorId
|
|
)
|
|
})
|
|
.asType(SMKLegacy._DBMessage.self)?
|
|
.attachmentIds
|
|
.first
|
|
|
|
SNLog([
|
|
"[Migration Warning] Quote with invalid attachmentId found",
|
|
(quoteAttachmentId == nil ?
|
|
"Unable to reconcile, leaving attachment blank" :
|
|
"Original interaction found, using source attachment"
|
|
)
|
|
].joined(separator: " - "))
|
|
}
|
|
|
|
// Setup the attachment and add it to the lookup (if it exists)
|
|
let attachmentId: String? = try attachmentId(
|
|
db,
|
|
for: quoteAttachmentId,
|
|
isQuotedMessage: true,
|
|
attachments: attachments,
|
|
processedAttachmentIds: &processedAttachmentIds
|
|
)
|
|
|
|
// Create the quote
|
|
try Quote(
|
|
interactionId: interactionId,
|
|
authorId: quotedMessage.authorId,
|
|
timestampMs: Int64.zeroingOverflow(quotedMessage.timestamp),
|
|
body: quotedMessage.body,
|
|
attachmentId: attachmentId
|
|
).migrationSafeInsert(db)
|
|
}
|
|
|
|
// Handle any LinkPreview
|
|
|
|
if let linkPreview: SMKLegacy._DBLinkPreview = linkPreview, let urlString: String = linkPreview.urlString {
|
|
// Note: The `legacyInteraction.timestamp` value is in milliseconds
|
|
let timestamp: TimeInterval = LinkPreview.timestampFor(sentTimestampMs: Double(legacyInteraction.timestamp))
|
|
|
|
// Setup the attachment and add it to the lookup (if it exists - we do actually
|
|
// support link previews with no image attachments so no need to throw migration
|
|
// errors in those cases)
|
|
let attachmentId: String? = try attachmentId(
|
|
db,
|
|
for: linkPreview.imageAttachmentId,
|
|
attachments: attachments,
|
|
processedAttachmentIds: &processedAttachmentIds
|
|
)
|
|
|
|
// Note: It's possible for there to be duplicate values here so we use 'save'
|
|
// instead of insert (ie. upsert)
|
|
try LinkPreview(
|
|
url: urlString,
|
|
timestamp: timestamp,
|
|
variant: linkPreviewVariant,
|
|
title: linkPreview.title,
|
|
attachmentId: attachmentId
|
|
).migrationSafeSave(db)
|
|
}
|
|
|
|
// Handle any attachments
|
|
|
|
try attachmentIds.enumerated().forEach { index, legacyAttachmentId in
|
|
let maybeAttachmentId: String? = (try attachmentId(
|
|
db,
|
|
for: legacyAttachmentId,
|
|
interactionVariant: variant,
|
|
attachments: attachments,
|
|
processedAttachmentIds: &processedAttachmentIds
|
|
))
|
|
.defaulting(
|
|
// It looks like somehow messages could exist in the old database which
|
|
// referenced attachments but had no attachments in the database; doing
|
|
// nothing here results in these messages appearing as empty message
|
|
// bubbles so instead we want to insert invalid attachments instead
|
|
to: try invalidAttachmentId(
|
|
db,
|
|
for: legacyAttachmentId,
|
|
attachments: attachments,
|
|
processedAttachmentIds: &processedAttachmentIds
|
|
)
|
|
)
|
|
|
|
guard let attachmentId: String = maybeAttachmentId else {
|
|
SNLog("[Migration Warning] Failed to create invalid attachment for missing attachment")
|
|
return
|
|
}
|
|
|
|
// Link the attachment to the interaction and add to the id lookup
|
|
try InteractionAttachment(
|
|
albumIndex: index,
|
|
interactionId: interactionId,
|
|
attachmentId: attachmentId
|
|
).migrationSafeInsert(db)
|
|
}
|
|
|
|
// Increment the progress for each contact
|
|
Storage.update(
|
|
progress: (
|
|
threadInteractionsStartProgress +
|
|
(progressPerInteraction * (interactionCounter + 1))
|
|
),
|
|
for: self,
|
|
in: target
|
|
)
|
|
interactionCounter += 1
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear out processed data (give the memory a change to be freed)
|
|
legacyThreads = []
|
|
disappearingMessagesConfiguration = [:]
|
|
|
|
closedGroupKeys = [:]
|
|
closedGroupName = [:]
|
|
closedGroupFormation = [:]
|
|
closedGroupModel = [:]
|
|
closedGroupZombieMemberIds = [:]
|
|
|
|
openGroupInfo = [:]
|
|
openGroupUserCount = [:]
|
|
openGroupImage = [:]
|
|
|
|
interactions = [:]
|
|
attachments = [:]
|
|
|
|
// MARK: --Received Message Timestamps
|
|
|
|
// Insert a 'ControlMessageProcessRecord' for any remaining 'receivedMessageTimestamp'
|
|
// entries as "legacy"
|
|
try ControlMessageProcessRecord.generateLegacyProcessRecords(
|
|
db,
|
|
receivedMessageTimestamps: receivedMessageTimestamps.map { Int64.zeroingOverflow($0) }
|
|
)
|
|
|
|
// Clear out processed data (give the memory a change to be freed)
|
|
receivedMessageTimestamps = []
|
|
|
|
// MARK: - Insert Jobs
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Inserting Jobs")
|
|
|
|
// MARK: --notifyPushServer
|
|
|
|
try autoreleasepool {
|
|
try notifyPushServerJobs.forEach { legacyJob in
|
|
_ = try Job(
|
|
failureCount: legacyJob.failureCount,
|
|
variant: .notifyPushServer,
|
|
behaviour: .runOnce,
|
|
nextRunTimestamp: 0,
|
|
details: NotifyPushServerJob.Details(
|
|
message: SnodeMessage(
|
|
recipient: legacyJob.message.recipient,
|
|
// Note: The legacy type had 'LosslessStringConvertible' so we need
|
|
// to use '.description' to get it as a basic string
|
|
data: legacyJob.message.data.description,
|
|
ttl: legacyJob.message.ttl,
|
|
timestampMs: legacyJob.message.timestamp
|
|
)
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
}
|
|
}
|
|
|
|
// MARK: --messageReceive
|
|
|
|
try autoreleasepool {
|
|
try messageReceiveJobs.forEach { legacyJob in
|
|
// We haven't supported OpenGroup messageReceive jobs for a long time so if
|
|
// we see any then just ignore them
|
|
if legacyJob.openGroupID != nil && legacyJob.openGroupMessageServerID != nil {
|
|
return
|
|
}
|
|
|
|
// We have changed how messageReceive jobs work - we now parse the message upon receipt and
|
|
// the MessageReceiveJob only does the handling - as a result we need to do the same behaviour
|
|
// here so we don't need to support the legacy behaviour
|
|
guard let processedMessage: ProcessedMessage = try? Message.processRawReceivedMessage(db, serializedData: legacyJob.data, serverHash: legacyJob.serverHash) else {
|
|
return
|
|
}
|
|
|
|
_ = try Job(
|
|
failureCount: legacyJob.failureCount,
|
|
variant: .messageReceive,
|
|
behaviour: .runOnce,
|
|
nextRunTimestamp: 0,
|
|
threadId: processedMessage.threadId,
|
|
details: MessageReceiveJob.Details(
|
|
messages: [processedMessage.messageInfo],
|
|
calledFromBackgroundPoller: legacyJob.isBackgroundPoll
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
}
|
|
}
|
|
|
|
// MARK: --messageSend
|
|
|
|
var messageSendJobLegacyMap: [String: Job] = [:]
|
|
|
|
try autoreleasepool {
|
|
try messageSendJobs.forEach { legacyJob in
|
|
// Fetch the threadId and interactionId this job should be associated with
|
|
let threadId: String = {
|
|
switch legacyJob.destination {
|
|
case .contact(let publicKey): return publicKey
|
|
case .closedGroup(let groupPublicKey): return groupPublicKey
|
|
case .openGroup(let roomToken, let server, _, _, _):
|
|
return OpenGroup.idFor(roomToken: roomToken, server: server)
|
|
|
|
case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey
|
|
}
|
|
}()
|
|
let interactionId: Int64? = {
|
|
// The 'Legacy.Job' 'id' value was "(timestamp)(num jobs for this timestamp)"
|
|
// so we can reverse-engineer an approximate timestamp by extracting it from
|
|
// the id (this value is unlikely to match exactly though)
|
|
let fallbackTimestamp: UInt64 = legacyJob.id
|
|
.map { UInt64($0.prefix("\(SnodeAPI.currentOffsetTimestampMs())".count)) }
|
|
.defaulting(to: 0)
|
|
let legacyIdentifier: String = identifier(
|
|
for: threadId,
|
|
sentTimestamp: (legacyJob.message.sentTimestamp ?? fallbackTimestamp),
|
|
recipients: (legacyJob.message.recipient.map { [$0] } ?? []),
|
|
destination: legacyJob.destination,
|
|
variant: nil,
|
|
useFallback: false
|
|
)
|
|
|
|
if let matchingId: Int64 = legacyInteractionIdentifierToIdMap[legacyIdentifier] {
|
|
return matchingId
|
|
}
|
|
|
|
// If we didn't find the correct interaction then we need to try the "fallback"
|
|
// identifier which is less accurate (during testing this only happened for
|
|
// 'ExpirationTimerUpdate' send jobs)
|
|
let fallbackIdentifier: String = identifier(
|
|
for: threadId,
|
|
sentTimestamp: (legacyJob.message.sentTimestamp ?? fallbackTimestamp),
|
|
recipients: (legacyJob.message.recipient.map { [$0] } ?? []),
|
|
destination: legacyJob.destination,
|
|
variant: {
|
|
switch legacyJob.message {
|
|
case is SMKLegacy._ExpirationTimerUpdate:
|
|
return .infoDisappearingMessagesUpdate
|
|
default: return nil
|
|
}
|
|
}(),
|
|
useFallback: true
|
|
)
|
|
|
|
return legacyInteractionIdentifierToIdFallbackMap[fallbackIdentifier]
|
|
}()
|
|
|
|
// Don't botther adding any 'MessageSend' jobs VisibleMessages which don't have associated
|
|
// interactions
|
|
switch legacyJob.message {
|
|
case is SMKLegacy._VisibleMessage:
|
|
guard interactionId != nil else {
|
|
SNLog("[Migration Warning] Unable to find associated interaction to messageSend job, ignoring.")
|
|
return
|
|
}
|
|
|
|
break
|
|
|
|
default: break
|
|
}
|
|
|
|
let job: Job? = try Job(
|
|
failureCount: legacyJob.failureCount,
|
|
variant: .messageSend,
|
|
behaviour: .runOnce,
|
|
nextRunTimestamp: 0,
|
|
threadId: threadId,
|
|
// Note: There are some cases where there isn't a link between a
|
|
// 'MessageSendJob' and an interaction (eg. ConfigurationMessage),
|
|
// in these cases the 'interactionId' value will be nil
|
|
interactionId: interactionId,
|
|
details: MessageSendJob.Details(
|
|
destination: legacyJob.destination,
|
|
message: legacyJob.message.toNonLegacy()
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
|
|
if let oldId: String = legacyJob.id {
|
|
messageSendJobLegacyMap[oldId] = job
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: --attachmentUpload
|
|
|
|
try autoreleasepool {
|
|
try attachmentUploadJobs.forEach { legacyJob in
|
|
guard let sendJob: Job = messageSendJobLegacyMap[legacyJob.messageSendJobID], let sendJobId: Int64 = sendJob.id else {
|
|
SNLog("[Migration Error] attachmentUpload job missing associated MessageSendJob")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
let uploadJob: Job? = try Job(
|
|
failureCount: legacyJob.failureCount,
|
|
variant: .attachmentUpload,
|
|
behaviour: .runOnce,
|
|
threadId: sendJob.threadId,
|
|
interactionId: sendJob.interactionId,
|
|
details: AttachmentUploadJob.Details(
|
|
messageSendJobId: sendJobId,
|
|
attachmentId: legacyJob.attachmentID
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
|
|
// Add the dependency to the relevant MessageSendJob
|
|
guard let uploadJobId: Int64 = uploadJob?.id else {
|
|
SNLog("[Migration Error] attachmentUpload job was not created")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
try JobDependencies(
|
|
jobId: sendJobId,
|
|
dependantId: uploadJobId
|
|
).migrationSafeInsert(db)
|
|
}
|
|
}
|
|
|
|
// MARK: --attachmentDownload
|
|
|
|
try autoreleasepool {
|
|
try attachmentDownloadJobs.forEach { legacyJob in
|
|
guard let interactionId: Int64 = legacyInteractionToIdMap[legacyJob.tsMessageID] else {
|
|
// This can happen if an UnsendRequest came before an AttachmentDownloadJob completed
|
|
SNLog("[Migration Warning] attachmentDownload job with no interaction found - ignoring")
|
|
return
|
|
}
|
|
guard processedAttachmentIds.contains(legacyJob.attachmentID) else {
|
|
// Unsure how this case can occur but it seemed to happen when testing internally
|
|
SNLog("[Migration Warning] attachmentDownload job unable to find attachment - ignoring")
|
|
return
|
|
}
|
|
|
|
_ = try Job(
|
|
failureCount: legacyJob.failureCount,
|
|
variant: .attachmentDownload,
|
|
behaviour: .runOnce,
|
|
nextRunTimestamp: 0,
|
|
threadId: legacyThreadIdToIdMap[legacyJob.threadID],
|
|
interactionId: interactionId,
|
|
details: AttachmentDownloadJob.Details(
|
|
attachmentId: legacyJob.attachmentID
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
}
|
|
}
|
|
|
|
// MARK: --sendReadReceipts
|
|
|
|
try autoreleasepool {
|
|
try outgoingReadReceiptsTimestampsMs.forEach { threadId, timestampsMs in
|
|
_ = try Job(
|
|
variant: .sendReadReceipts,
|
|
behaviour: .recurring,
|
|
threadId: threadId,
|
|
details: SendReadReceiptsJob.Details(
|
|
destination: .contact(publicKey: threadId),
|
|
timestampMsValues: timestampsMs
|
|
)
|
|
)?.migrationSafeInserted(db)
|
|
}
|
|
}
|
|
Storage.update(progress: 0.99, for: self, in: target)
|
|
|
|
// MARK: - Preferences
|
|
|
|
SNLog("[Migration Info] \(target.key(with: self)) - Inserting Preferences")
|
|
|
|
db[.defaultNotificationSound] = Preferences.Sound(rawValue: legacyPreferences[SMKLegacy.soundsGlobalNotificationKey] as? Int ?? -1)
|
|
.defaulting(to: Preferences.Sound.defaultNotificationSound)
|
|
db[.playNotificationSoundInForeground] = (legacyPreferences[SMKLegacy.preferencesKeyNotificationSoundInForeground] as? Bool == true)
|
|
db[.preferencesNotificationPreviewType] = Preferences.NotificationPreviewType(rawValue: legacyPreferences[SMKLegacy.preferencesKeyNotificationPreviewType] as? Int ?? -1)
|
|
.defaulting(to: .defaultPreviewType)
|
|
|
|
if let lastPushToken: String = legacyPreferences[SMKLegacy.preferencesKeyLastRecordedPushToken] as? String {
|
|
db[.lastRecordedPushToken] = lastPushToken
|
|
}
|
|
|
|
if let lastVoipToken: String = legacyPreferences[SMKLegacy.preferencesKeyLastRecordedVoipToken] as? String {
|
|
db[.lastRecordedVoipToken] = lastVoipToken
|
|
}
|
|
|
|
db[.areReadReceiptsEnabled] = (legacyPreferences[SMKLegacy.readReceiptManagerAreReadReceiptsEnabled] as? Bool == true)
|
|
db[.typingIndicatorsEnabled] = (legacyPreferences[SMKLegacy.typingIndicatorsEnabledKey] as? Bool == true)
|
|
db[.isScreenLockEnabled] = (legacyPreferences[SMKLegacy.screenLockIsScreenLockEnabledKey] as? Bool == true)
|
|
// Note: 'screenLockTimeoutSeconds' has been removed, but we want to avoid changing the behaviour
|
|
// of old migrations when possible
|
|
db.unsafeSet(
|
|
key: "screenLockTimeoutSeconds",
|
|
value: (legacyPreferences[SMKLegacy.screenLockScreenLockTimeoutSecondsKey] as? Double)
|
|
.defaulting(to: (15 * 60))
|
|
)
|
|
db[.areLinkPreviewsEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyAreLinkPreviewsEnabled] as? Bool == true)
|
|
db[.areCallsEnabled] = (legacyPreferences[SMKLegacy.preferencesKeyAreCallsEnabled] as? Bool == true)
|
|
db[.hasHiddenMessageRequests] = CurrentAppContext().appUserDefaults()
|
|
.bool(forKey: SMKLegacy.userDefaultsHasHiddenMessageRequests)
|
|
|
|
// Note: The 'hasViewedSeed' was originally stored on standard user defaults
|
|
db[.hasViewedSeed] = UserDefaults.standard.bool(forKey: SMKLegacy.userDefaultsHasViewedSeedKey)
|
|
db[.hasSavedThread] = (legacyPreferences[SMKLegacy.preferencesKeyHasSavedThreadKey] as? Bool == true)
|
|
db[.hasSentAMessage] = (legacyPreferences[SMKLegacy.preferencesKeyHasSentAMessageKey] as? Bool == true)
|
|
db[.isReadyForAppExtensions] = CurrentAppContext().appUserDefaults().bool(forKey: SMKLegacy.preferencesKeyIsReadyForAppExtensions)
|
|
|
|
// We want this setting to be on by default
|
|
db[.trimOpenGroupMessagesOlderThanSixMonths] = true
|
|
|
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
|
}
|
|
|
|
// MARK: - Convenience
|
|
|
|
private static func attachmentId(
|
|
_ db: Database,
|
|
for legacyAttachmentId: String?,
|
|
interactionVariant: Interaction.Variant? = nil,
|
|
isQuotedMessage: Bool = false,
|
|
attachments: [String: SMKLegacy._Attachment],
|
|
processedAttachmentIds: inout Set<String>
|
|
) throws -> String? {
|
|
guard let legacyAttachmentId: String = legacyAttachmentId else { return nil }
|
|
guard !processedAttachmentIds.contains(legacyAttachmentId) else {
|
|
guard isQuotedMessage else {
|
|
SNLog("[Migration Error] Attempted to process duplicate attachment")
|
|
throw StorageError.migrationFailed
|
|
}
|
|
|
|
return legacyAttachmentId
|
|
}
|
|
|
|
guard let legacyAttachment: SMKLegacy._Attachment = attachments[legacyAttachmentId] else {
|
|
SNLog("[Migration Warning] Missing attachment - interaction will show a \"failed\" attachment")
|
|
return nil
|
|
}
|
|
|
|
let processedLocalRelativeFilePath: String? = (legacyAttachment as? SMKLegacy._AttachmentStream)?
|
|
.localRelativeFilePath
|
|
.map { filePath -> String in
|
|
// The old 'localRelativeFilePath' seemed to have a leading forward slash (want
|
|
// to get rid of it so we can correctly use 'appendingPathComponent')
|
|
guard filePath.starts(with: "/") else { return filePath }
|
|
|
|
return String(filePath.suffix(from: filePath.index(after: filePath.startIndex)))
|
|
}
|
|
let state: Attachment.State = {
|
|
switch legacyAttachment {
|
|
case let stream as SMKLegacy._AttachmentStream: // Outgoing or already downloaded
|
|
switch interactionVariant {
|
|
case .standardOutgoing: return (stream.isUploaded ? .uploaded : .uploading)
|
|
default: return .downloaded
|
|
}
|
|
|
|
// All other cases can just be set to 'pendingDownload'
|
|
default: return .pendingDownload
|
|
}
|
|
}()
|
|
let size: CGSize = {
|
|
switch legacyAttachment {
|
|
case let stream as SMKLegacy._AttachmentStream:
|
|
// First try to get an image size using the 'localRelativeFilePath' value
|
|
if
|
|
let localRelativeFilePath: String = processedLocalRelativeFilePath,
|
|
let specificImageSize: CGSize = Attachment.imageSize(
|
|
contentType: stream.contentType,
|
|
originalFilePath: URL(fileURLWithPath: Attachment.attachmentsFolder)
|
|
.appendingPathComponent(localRelativeFilePath)
|
|
.path
|
|
),
|
|
specificImageSize != .zero
|
|
{
|
|
return specificImageSize
|
|
}
|
|
|
|
// Then fallback to trying to get the size from the 'originalFilePath'
|
|
guard let originalFilePath: String = Attachment.originalFilePath(id: legacyAttachmentId, mimeType: stream.contentType, sourceFilename: stream.sourceFilename) else {
|
|
return .zero
|
|
}
|
|
|
|
return Attachment
|
|
.imageSize(
|
|
contentType: stream.contentType,
|
|
originalFilePath: originalFilePath
|
|
)
|
|
.defaulting(to: .zero)
|
|
|
|
case let pointer as SMKLegacy._AttachmentPointer: return pointer.mediaSize
|
|
default: return CGSize.zero
|
|
}
|
|
}()
|
|
let (isValid, duration): (Bool, TimeInterval?) = {
|
|
guard
|
|
let stream: SMKLegacy._AttachmentStream = legacyAttachment as? SMKLegacy._AttachmentStream,
|
|
let originalFilePath: String = Attachment.originalFilePath(
|
|
id: legacyAttachmentId,
|
|
mimeType: stream.contentType,
|
|
sourceFilename: stream.sourceFilename
|
|
)
|
|
else {
|
|
return (false, nil)
|
|
}
|
|
|
|
if stream.isAudio {
|
|
if let cachedDuration: TimeInterval = stream.cachedAudioDurationSeconds?.doubleValue, cachedDuration > 0 {
|
|
return (true, cachedDuration)
|
|
}
|
|
|
|
let attachmentVailidityInfo = Attachment.determineValidityAndDuration(
|
|
contentType: stream.contentType,
|
|
localRelativeFilePath: processedLocalRelativeFilePath,
|
|
originalFilePath: originalFilePath
|
|
)
|
|
|
|
return (attachmentVailidityInfo.isValid, attachmentVailidityInfo.duration)
|
|
}
|
|
|
|
if stream.isVisualMedia {
|
|
let attachmentVailidityInfo = Attachment.determineValidityAndDuration(
|
|
contentType: stream.contentType,
|
|
localRelativeFilePath: processedLocalRelativeFilePath,
|
|
originalFilePath: originalFilePath
|
|
)
|
|
|
|
return (attachmentVailidityInfo.isValid, attachmentVailidityInfo.duration)
|
|
}
|
|
|
|
return (true, nil)
|
|
}()
|
|
|
|
_ = try Attachment(
|
|
// Note: The legacy attachment object used a UUID string for it's id as well
|
|
// and saved files using these id's so just used the existing id so we don't
|
|
// need to bother renaming files as part of the migration
|
|
id: legacyAttachmentId,
|
|
serverId: "\(legacyAttachment.serverId)",
|
|
variant: (legacyAttachment.attachmentType == .voiceMessage ? .voiceMessage : .standard),
|
|
state: state,
|
|
contentType: legacyAttachment.contentType,
|
|
byteCount: UInt(legacyAttachment.byteCount),
|
|
creationTimestamp: (legacyAttachment as? SMKLegacy._AttachmentStream)?
|
|
.creationTimestamp.timeIntervalSince1970,
|
|
sourceFilename: legacyAttachment.sourceFilename,
|
|
downloadUrl: legacyAttachment.downloadURL,
|
|
localRelativeFilePath: processedLocalRelativeFilePath,
|
|
width: (size == .zero ? nil : UInt(size.width)),
|
|
height: (size == .zero ? nil : UInt(size.height)),
|
|
duration: duration,
|
|
isValid: isValid,
|
|
encryptionKey: legacyAttachment.encryptionKey,
|
|
digest: {
|
|
switch legacyAttachment {
|
|
case let stream as SMKLegacy._AttachmentStream: return stream.digest
|
|
case let pointer as SMKLegacy._AttachmentPointer: return pointer.digest
|
|
default: return nil
|
|
}
|
|
}(),
|
|
caption: legacyAttachment.caption
|
|
).migrationSafeInserted(db)
|
|
|
|
processedAttachmentIds.insert(legacyAttachmentId)
|
|
|
|
return legacyAttachmentId
|
|
}
|
|
|
|
private static func invalidAttachmentId(
|
|
_ db: Database,
|
|
for legacyAttachmentId: String,
|
|
interactionVariant: Interaction.Variant? = nil,
|
|
attachments: [String: SMKLegacy._Attachment],
|
|
processedAttachmentIds: inout Set<String>
|
|
) throws -> String {
|
|
guard !processedAttachmentIds.contains(legacyAttachmentId) else {
|
|
return legacyAttachmentId
|
|
}
|
|
|
|
_ = try Attachment(
|
|
// Note: The legacy attachment object used a UUID string for it's id as well
|
|
// and saved files using these id's so just used the existing id so we don't
|
|
// need to bother renaming files as part of the migration
|
|
id: legacyAttachmentId,
|
|
serverId: nil,
|
|
variant: .standard,
|
|
state: .invalid,
|
|
contentType: "",
|
|
byteCount: 0,
|
|
creationTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000),
|
|
sourceFilename: nil,
|
|
downloadUrl: nil,
|
|
localRelativeFilePath: nil,
|
|
width: nil,
|
|
height: nil,
|
|
duration: nil,
|
|
isValid: false,
|
|
encryptionKey: nil,
|
|
digest: nil,
|
|
caption: nil
|
|
).migrationSafeInserted(db)
|
|
|
|
processedAttachmentIds.insert(legacyAttachmentId)
|
|
|
|
return legacyAttachmentId
|
|
}
|
|
|
|
private static func mapLegacyTypesForNSKeyedUnarchiver() {
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Thread.self,
|
|
forClassName: "TSThread"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ContactThread.self,
|
|
forClassName: "TSContactThread"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._GroupThread.self,
|
|
forClassName: "TSGroupThread"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._GroupModel.self,
|
|
forClassName: "TSGroupModel"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._OpenGroup.self,
|
|
forClassName: "SNOpenGroupV2"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Contact.self,
|
|
forClassName: "SNContact"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBInteraction.self,
|
|
forClassName: "TSInteraction"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBMessage.self,
|
|
forClassName: "TSMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBQuotedMessage.self,
|
|
forClassName: "TSQuotedMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBQuotedMessage._DBAttachmentInfo.self,
|
|
forClassName: "OWSAttachmentInfo"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBLinkPreview.self,
|
|
forClassName: "SessionServiceKit.OWSLinkPreview" // Very old legacy name
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBLinkPreview.self,
|
|
forClassName: "SessionMessagingKit.OWSLinkPreview"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBIncomingMessage.self,
|
|
forClassName: "TSIncomingMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBOutgoingMessage.self,
|
|
forClassName: "TSOutgoingMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBOutgoingMessageRecipientState.self,
|
|
forClassName: "TSOutgoingMessageRecipientState"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DBInfoMessage.self,
|
|
forClassName: "TSInfoMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DisappearingConfigurationUpdateInfoMessage.self,
|
|
forClassName: "OWSDisappearingConfigurationUpdateInfoMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DataExtractionNotificationInfoMessage.self,
|
|
forClassName: "SNDataExtractionNotificationInfoMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Attachment.self,
|
|
forClassName: "TSAttachment"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._AttachmentStream.self,
|
|
forClassName: "TSAttachmentStream"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._AttachmentPointer.self,
|
|
forClassName: "TSAttachmentPointer"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._NotifyPNServerJob.self,
|
|
forClassName: "SessionMessagingKit.NotifyPNServerJob"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._NotifyPNServerJob._SnodeMessage.self,
|
|
forClassName: "SessionSnodeKit.SnodeMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._MessageSendJob.self,
|
|
forClassName: "SessionMessagingKit.SNMessageSendJob"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._MessageReceiveJob.self,
|
|
forClassName: "SessionMessagingKit.MessageReceiveJob"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._AttachmentUploadJob.self,
|
|
forClassName: "SessionMessagingKit.AttachmentUploadJob"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._AttachmentDownloadJob.self,
|
|
forClassName: "SessionMessagingKit.AttachmentDownloadJob"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Message.self,
|
|
forClassName: "SNMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._VisibleMessage.self,
|
|
forClassName: "SNVisibleMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Quote.self,
|
|
forClassName: "SNQuote"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._LinkPreview.self,
|
|
forClassName: "SNLinkPreview"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._Profile.self,
|
|
forClassName: "SNProfile"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._OpenGroupInvitation.self,
|
|
forClassName: "SNOpenGroupInvitation"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ControlMessage.self,
|
|
forClassName: "SNControlMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ReadReceipt.self,
|
|
forClassName: "SNReadReceipt"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._TypingIndicator.self,
|
|
forClassName: "SNTypingIndicator"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ClosedGroupControlMessage.self,
|
|
forClassName: "SessionMessagingKit.ClosedGroupControlMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ClosedGroupControlMessage._KeyPairWrapper.self,
|
|
forClassName: "ClosedGroupControlMessage.SNKeyPairWrapper"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._DataExtractionNotification.self,
|
|
forClassName: "SessionMessagingKit.DataExtractionNotification"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ExpirationTimerUpdate.self,
|
|
forClassName: "SNExpirationTimerUpdate"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._ConfigurationMessage.self,
|
|
forClassName: "SNConfigurationMessage"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._CMClosedGroup.self,
|
|
forClassName: "SNClosedGroup"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._CMContact.self,
|
|
forClassName: "SNConfigurationMessage.SNConfigurationMessageContact"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._UnsendRequest.self,
|
|
forClassName: "SNUnsendRequest"
|
|
)
|
|
NSKeyedUnarchiver.setClass(
|
|
SMKLegacy._MessageRequestResponse.self,
|
|
forClassName: "SNMessageRequestResponse"
|
|
)
|
|
}
|
|
}
|
|
|
|
fileprivate extension Int64 {
|
|
static func zeroingOverflow(_ value: UInt64) -> Int64 {
|
|
return (value > UInt64(Int64.max) ? 0 : Int64(value))
|
|
}
|
|
}
|