Files

241 lines
10 KiB
Swift

// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
//
// stringlint:disable
import Foundation
import UIKit.UIImage
import UniformTypeIdentifiers
import GRDB
import SessionNetworkingKit
import SessionUtilitiesKit
enum _036_GroupsRebuildChanges: Migration {
static let identifier: String = "messagingKit.GroupsRebuildChanges"
static let minExpectedRunDuration: TimeInterval = 0.1
static var createdTables: [(FetchableRecord & TableRecord).Type] = []
static func migrate(_ db: ObservingDatabase, using dependencies: Dependencies) throws {
try db.alter(table: "thread") { t in
t.add(column: "isDraft", .boolean).defaults(to: false)
}
try db.alter(table: "closedGroup") { t in
t.add(column: "groupDescription", .text)
t.add(column: "displayPictureUrl", .text)
t.add(column: "displayPictureFilename", .text)
t.add(column: "displayPictureEncryptionKey", .blob)
t.add(column: "lastDisplayPictureUpdate", .integer).defaults(to: 0)
t.add(column: "shouldPoll", .boolean).defaults(to: false)
t.add(column: "groupIdentityPrivateKey", .blob)
t.add(column: "authData", .blob)
t.add(column: "invited", .boolean).defaults(to: false)
}
try db.alter(table: "openGroup") { t in
t.add(column: "displayPictureFilename", .text)
t.add(column: "lastDisplayPictureUpdate", .integer).defaults(to: 0)
}
try db.alter(table: "groupMember") { t in
t.add(column: "roleStatus", .integer)
.notNull()
.defaults(to: GroupMember.RoleStatus.accepted)
}
guard
MigrationHelper.userExists(db),
let userEd25519SecretKey: Data = MigrationHelper.fetchIdentityValue(db, key: "ed25519SecretKey")
else { return MigrationExecution.updateProgress(1) }
let userSessionId: SessionId = MigrationHelper.userSessionId(db)
// Update existing groups where the current user is a member to have `shouldPoll` as `true`
try db.execute(sql: """
UPDATE closedGroup
SET shouldPoll = true
WHERE EXISTS (
SELECT 1
FROM groupMember
WHERE groupMember.groupId = closedGroup.threadId
AND groupMember.profileId = '\(userSessionId.hexString)'
)
""")
// If a user had upgraded a different device their config could already contain V2 groups
// so we should check and, if so, create those
let cache: LibSession.Cache = LibSession.Cache(userSessionId: userSessionId, using: dependencies)
let userGroupsConfig: LibSession.Config = try cache.loadState(
for: .userGroups,
sessionId: userSessionId,
userEd25519SecretKey: Array(userEd25519SecretKey),
groupEd25519SecretKey: nil,
cachedData: MigrationHelper.configDump(db, for: ConfigDump.Variant.userGroups.rawValue)
)
let convoInfoVolatileConfig: LibSession.Config = try cache.loadState(
for: .convoInfoVolatile,
sessionId: userSessionId,
userEd25519SecretKey: Array(userEd25519SecretKey),
groupEd25519SecretKey: nil,
cachedData: MigrationHelper.configDump(db, for: ConfigDump.Variant.convoInfoVolatile.rawValue)
)
// Extract all of the user group info
if case .userGroups(let conf) = userGroupsConfig, case .convoInfoVolatile(let convoInfoVolatileConf) = convoInfoVolatileConfig {
let extractedUserGroups: LibSession.ExtractedUserGroups = try LibSession.extractUserGroups(
from: conf,
using: dependencies
)
let volatileThreadInfo: [String: LibSession.VolatileThreadInfo] = try LibSession
.extractConvoVolatileInfo(from: convoInfoVolatileConf)
.reduce(into: [:]) { result, next in result[next.threadId] = next }
try extractedUserGroups.groups.forEach { group in
let markedAsUnread: Bool = (volatileThreadInfo[group.groupSessionId]?.changes
.contains(where: { change in
switch change {
case .markedAsUnread(let value): return value
default: return false
}
}))
.defaulting(to: false)
try db.execute(sql: """
INSERT INTO thread (
id,
variant,
creationDateTimestamp,
shouldBeVisible,
isPinned,
markedAsUnread,
pinnedPriority
)
VALUES (
'\(group.groupSessionId)',
\(SessionThread.Variant.group.rawValue),
\(group.joinedAt),
true,
false,
\(markedAsUnread),
\(group.priority)
)
""")
try db.execute(
sql: """
INSERT INTO closedGroup (
threadId,
name,
formationTimestamp,
shouldPoll,
groupIdentityPrivateKey,
authData,
invited
)
VALUES (
'\(group.groupSessionId)',
'\(group.name)',
\(group.joinedAt),
\(group.invited == false),
?,
?,
\(group.invited)
)
""",
arguments: [group.groupIdentityPrivateKey, group.authData]
)
/// If the group isn't in the invited state then make sure to subscribe for PNs once the migrations are done
if !group.invited, let token: String = dependencies[defaults: .standard, key: .deviceToken] {
let maybeAuthMethod: AuthenticationMethod? = try? Authentication.with(
db,
swarmPublicKey: group.groupSessionId,
using: dependencies
)
if let authMethod: AuthenticationMethod = maybeAuthMethod {
db.afterCommit {
try? Network.PushNotification
.preparedSubscribe(
token: Data(hex: token),
swarms: [(SessionId(.group, hex: group.groupSessionId), authMethod)],
using: dependencies
)
.send(using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete()
}
}
}
}
}
// Move the `imageData` out of the `OpenGroup` table and on to disk to be consistent with
// the other display picture logic
let timestampMs: TimeInterval = TimeInterval(dependencies[cache: .snodeAPI].currentOffsetTimestampMs() / 1000)
let existingImageInfo: [Row] = try Row.fetchAll(db, sql: """
SELECT threadid, imageData
FROM openGroup
WHERE imageData IS NOT NULL
""")
existingImageInfo.forEach { imageInfo in
guard
let threadId: String = imageInfo["threadId"],
let imageData: Data = imageInfo["imageData"]
else {
Log.error("[GroupsRebuildChanges] Failed to extract imageData from community")
return
}
let filename: String = generateFilename(
utType: (UTType(imageData: imageData, using: dependencies) ?? .jpeg),
using: dependencies
)
let filePath: String = URL(fileURLWithPath: dependencies[singleton: .displayPictureManager].sharedDataDisplayPictureDirPath())
.appendingPathComponent(filename)
.path
// Save the decrypted display picture to disk
try? imageData.write(to: URL(fileURLWithPath: filePath), options: [.atomic])
// Verify the saved data is valid image data (don't do this when running unit tests because
// the data generally won't be valid and trying to mock the return for any possible test
// that may run this migration would be a nightmare)
guard SNUtilitiesKit.isRunningTests || UIImage(contentsOfFile: filePath) != nil else {
Log.error("[GroupsRebuildChanges] Failed to save Community imageData for \(threadId)")
return
}
// Update the database with the new info
try? db.execute(sql: """
UPDATE openGroup
SET
imageData = NULL,
displayPictureFilename = '\(filename)',
lastDisplayPictureUpdate = \(timestampMs)
WHERE threadId = '\(threadId)'
""")
}
MigrationExecution.updateProgress(1)
}
}
private extension _036_GroupsRebuildChanges {
static func generateFilename(utType: UTType, using dependencies: Dependencies) -> String {
return dependencies[singleton: .crypto]
.generate(.uuid())
.defaulting(to: UUID())
.uuidString
.appendingFileExtension(utType.sessionFileExtension(sourceFilename: nil) ?? "jpg")
}
}
private extension String {
func appendingFileExtension(_ fileExtension: String) -> String {
guard let result = (self as NSString).appendingPathExtension(fileExtension) else {
return self
}
return result
}
}