Fixed a number of bugs with the config handling
Added a number of feature flag checks to config updates Added legacy group disappearing message timer handling Updated the string linter to clean up the build logs a little Split the initial config dump generation into it's own migration so it can run the launch after the feature flag is toggled Fixed a few issues with the initial config dump creation Fixed an issue where "shadow" conversations would be left in the database by opening a thread and never sending a message Fixed a bug where duplicate members could be added to legacy groups Fixed a bug with using animated images for the avatar Fixed a bug where avatar images which were already on disk could be re-downloaded
This commit is contained in:
parent
7ee84fe0d3
commit
e28b4b4531
|
@ -1,11 +1,6 @@
|
||||||
#!/usr/bin/xcrun --sdk macosx swift
|
#!/usr/bin/xcrun --sdk macosx swift
|
||||||
|
|
||||||
//
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
// ListLocalizableStrings.swift
|
|
||||||
// Archa
|
|
||||||
//
|
|
||||||
// Created by Morgan Pretty on 18/5/20.
|
|
||||||
// Copyright © 2020 Archa. All rights reserved.
|
|
||||||
//
|
//
|
||||||
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
|
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
|
||||||
// is canges to the localized usage regex
|
// is canges to the localized usage regex
|
||||||
|
@ -56,7 +51,6 @@ var executableFiles: [String] = {
|
||||||
/// - Parameter path: path of file
|
/// - Parameter path: path of file
|
||||||
/// - Returns: content in file
|
/// - Returns: content in file
|
||||||
func contents(atPath path: String) -> String {
|
func contents(atPath path: String) -> String {
|
||||||
print("Path: \(path)")
|
|
||||||
guard let data = fileManager.contents(atPath: path), let content = String(data: data, encoding: .utf8) else {
|
guard let data = fileManager.contents(atPath: path), let content = String(data: data, encoding: .utf8) else {
|
||||||
fatalError("Could not read from path: \(path)")
|
fatalError("Could not read from path: \(path)")
|
||||||
}
|
}
|
||||||
|
@ -109,8 +103,6 @@ func localizedStringsInCode() -> [LocalizationCodeFile] {
|
||||||
///
|
///
|
||||||
/// - Parameter files: list of localizable files to validate
|
/// - Parameter files: list of localizable files to validate
|
||||||
func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
||||||
print("------------ Validating keys match in all localizable files ------------")
|
|
||||||
|
|
||||||
guard let base = files.first, files.count > 1 else { return }
|
guard let base = files.first, files.count > 1 else { return }
|
||||||
|
|
||||||
let files = Array(files.dropFirst())
|
let files = Array(files.dropFirst())
|
||||||
|
@ -128,8 +120,6 @@ func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
||||||
/// - codeFiles: Array of LocalizationCodeFile
|
/// - codeFiles: Array of LocalizationCodeFile
|
||||||
/// - localizationFiles: Array of LocalizableStringFiles
|
/// - localizationFiles: Array of LocalizableStringFiles
|
||||||
func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||||||
print("------------ Checking for missing keys -----------")
|
|
||||||
|
|
||||||
guard let baseFile = localizationFiles.first else {
|
guard let baseFile = localizationFiles.first else {
|
||||||
fatalError("Could not locate base localization file")
|
fatalError("Could not locate base localization file")
|
||||||
}
|
}
|
||||||
|
@ -150,8 +140,6 @@ func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles:
|
||||||
/// - codeFiles: Array of LocalizationCodeFile
|
/// - codeFiles: Array of LocalizationCodeFile
|
||||||
/// - localizationFiles: Array of LocalizableStringFiles
|
/// - localizationFiles: Array of LocalizableStringFiles
|
||||||
func validateDeadKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
func validateDeadKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||||||
print("------------ Checking for any dead keys in localizable file -----------")
|
|
||||||
|
|
||||||
guard let baseFile = localizationFiles.first else {
|
guard let baseFile = localizationFiles.first else {
|
||||||
fatalError("Could not locate base localization file")
|
fatalError("Could not locate base localization file")
|
||||||
}
|
}
|
||||||
|
@ -174,14 +162,18 @@ protocol Pathable {
|
||||||
struct LocalizationStringsFile: Pathable {
|
struct LocalizationStringsFile: Pathable {
|
||||||
let path: String
|
let path: String
|
||||||
let kv: [String: String]
|
let kv: [String: String]
|
||||||
|
let duplicates: [(key: String, path: String)]
|
||||||
|
|
||||||
var keys: [String] {
|
var keys: [String] {
|
||||||
return Array(kv.keys)
|
return Array(kv.keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
init(path: String) {
|
init(path: String) {
|
||||||
|
let result = ContentParser.parse(path)
|
||||||
|
|
||||||
self.path = path
|
self.path = path
|
||||||
self.kv = ContentParser.parse(path)
|
self.kv = result.kv
|
||||||
|
self.duplicates = result.duplicates
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Writes back to localizable file with sorted keys and removed whitespaces and new lines
|
/// Writes back to localizable file with sorted keys and removed whitespaces and new lines
|
||||||
|
@ -204,9 +196,7 @@ struct ContentParser {
|
||||||
///
|
///
|
||||||
/// - Parameter path: Localizable file paths
|
/// - Parameter path: Localizable file paths
|
||||||
/// - Returns: localizable key and value for content at path
|
/// - Returns: localizable key and value for content at path
|
||||||
static func parse(_ path: String) -> [String: String] {
|
static func parse(_ path: String) -> (kv: [String: String], duplicates: [(key: String, path: String)]) {
|
||||||
print("------------ Checking for duplicate keys: \(path) ------------")
|
|
||||||
|
|
||||||
let content = contents(atPath: path)
|
let content = contents(atPath: path)
|
||||||
let trimmed = content
|
let trimmed = content
|
||||||
.replacingOccurrences(of: "\n+", with: "", options: .regularExpression, range: nil)
|
.replacingOccurrences(of: "\n+", with: "", options: .regularExpression, range: nil)
|
||||||
|
@ -218,13 +208,18 @@ struct ContentParser {
|
||||||
fatalError("Error parsing contents: Make sure all keys and values are in correct format (this could be due to extra spaces between keys and values)")
|
fatalError("Error parsing contents: Make sure all keys and values are in correct format (this could be due to extra spaces between keys and values)")
|
||||||
}
|
}
|
||||||
|
|
||||||
return zip(keys, values).reduce(into: [String: String]()) { results, keyValue in
|
var duplicates: [(key: String, path: String)] = []
|
||||||
if results[keyValue.0] != nil {
|
let kv: [String: String] = zip(keys, values)
|
||||||
printPretty("error: Found duplicate key: \(keyValue.0) in file: \(path)")
|
.reduce(into: [:]) { results, keyValue in
|
||||||
abort()
|
guard results[keyValue.0] == nil else {
|
||||||
|
duplicates.append((keyValue.0, path))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
results[keyValue.0] = keyValue.1
|
results[keyValue.0] = keyValue.1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (kv, duplicates)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,20 +227,27 @@ func printPretty(_ string: String) {
|
||||||
print(string.replacingOccurrences(of: "\\", with: ""))
|
print(string.replacingOccurrences(of: "\\", with: ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
let stringFiles = create()
|
// MARK: - Processing
|
||||||
|
|
||||||
|
let stringFiles: [LocalizationStringsFile] = create()
|
||||||
|
|
||||||
if !stringFiles.isEmpty {
|
if !stringFiles.isEmpty {
|
||||||
print("------------ Found \(stringFiles.count) file(s) ------------")
|
print("------------ Found \(stringFiles.count) file(s) - checking for duplicate, extra, missing and dead keys ------------")
|
||||||
|
|
||||||
|
stringFiles.forEach { file in
|
||||||
|
file.duplicates.forEach { key, path in
|
||||||
|
printPretty("error: Found duplicate key: \(key) in file: \(path)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stringFiles.forEach { print($0.path) }
|
|
||||||
validateMatchKeys(stringFiles)
|
validateMatchKeys(stringFiles)
|
||||||
|
|
||||||
// Note: Uncomment the below file to clean out all comments from the localizable file (we don't want this because comments make it readable...)
|
// Note: Uncomment the below file to clean out all comments from the localizable file (we don't want this because comments make it readable...)
|
||||||
// stringFiles.forEach { $0.cleanWrite() }
|
// stringFiles.forEach { $0.cleanWrite() }
|
||||||
|
|
||||||
let codeFiles = localizedStringsInCode()
|
let codeFiles: [LocalizationCodeFile] = localizedStringsInCode()
|
||||||
validateMissingKeys(codeFiles, localizationFiles: stringFiles)
|
validateMissingKeys(codeFiles, localizationFiles: stringFiles)
|
||||||
validateDeadKeys(codeFiles, localizationFiles: stringFiles)
|
validateDeadKeys(codeFiles, localizationFiles: stringFiles)
|
||||||
}
|
}
|
||||||
|
|
||||||
print("------------ SUCCESS ------------")
|
print("------------ Complete ------------")
|
||||||
|
|
|
@ -708,6 +708,7 @@
|
||||||
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; };
|
FD7728982849E8110018502F /* UITableView+ReusableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD7728972849E8110018502F /* UITableView+ReusableView.swift */; };
|
||||||
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD772899284AF1BD0018502F /* Sodium+Utilities.swift */; };
|
FD77289A284AF1BD0018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD772899284AF1BD0018502F /* Sodium+Utilities.swift */; };
|
||||||
FD77289E284EF1C50018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289D284EF1C50018502F /* Sodium+Utilities.swift */; };
|
FD77289E284EF1C50018502F /* Sodium+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD77289D284EF1C50018502F /* Sodium+Utilities.swift */; };
|
||||||
|
FD778B6429B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD778B6329B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift */; };
|
||||||
FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
|
FD83B9B327CF200A005E1583 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; };
|
||||||
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */; };
|
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */; };
|
||||||
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
|
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BD27CF2243005E1583 /* TestConstants.swift */; };
|
||||||
|
@ -1835,6 +1836,7 @@
|
||||||
FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
FD7728972849E8110018502F /* UITableView+ReusableView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UITableView+ReusableView.swift"; sourceTree = "<group>"; };
|
||||||
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
|
FD772899284AF1BD0018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
FD77289D284EF1C50018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
|
FD77289D284EF1C50018502F /* Sodium+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Sodium+Utilities.swift"; sourceTree = "<group>"; };
|
||||||
|
FD778B6329B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_GenerateInitialUserConfigDumps.swift; sourceTree = "<group>"; };
|
||||||
FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionUtilitiesKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
FD83B9AF27CF200A005E1583 /* SessionUtilitiesKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionUtilitiesKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = "<group>"; };
|
FD83B9BA27CF20AF005E1583 /* SessionIdSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionIdSpec.swift; sourceTree = "<group>"; };
|
||||||
FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
FD83B9BD27CF2243005E1583 /* TestConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestConstants.swift; sourceTree = "<group>"; };
|
||||||
|
@ -3629,6 +3631,7 @@
|
||||||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
||||||
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
|
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
|
||||||
FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */,
|
FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */,
|
||||||
|
FD778B6329B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift */,
|
||||||
);
|
);
|
||||||
path = Migrations;
|
path = Migrations;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -5791,6 +5794,7 @@
|
||||||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
||||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
|
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
|
||||||
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */,
|
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */,
|
||||||
|
FD778B6429B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift in Sources */,
|
||||||
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
|
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
|
||||||
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
|
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
|
||||||
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */,
|
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */,
|
||||||
|
|
|
@ -454,10 +454,12 @@ extension ConversationVC:
|
||||||
// Let the viewModel know we are about to send a message
|
// Let the viewModel know we are about to send a message
|
||||||
self?.viewModel.sentMessageBeforeUpdate = true
|
self?.viewModel.sentMessageBeforeUpdate = true
|
||||||
|
|
||||||
// Update the thread to be visible
|
// Update the thread to be visible (if it isn't already)
|
||||||
|
if !thread.shouldBeVisible {
|
||||||
_ = try SessionThread
|
_ = try SessionThread
|
||||||
.filter(id: threadId)
|
.filter(id: threadId)
|
||||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||||
|
}
|
||||||
|
|
||||||
let authorId: String = {
|
let authorId: String = {
|
||||||
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
|
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
|
||||||
|
@ -585,10 +587,12 @@ extension ConversationVC:
|
||||||
// Let the viewModel know we are about to send a message
|
// Let the viewModel know we are about to send a message
|
||||||
self?.viewModel.sentMessageBeforeUpdate = true
|
self?.viewModel.sentMessageBeforeUpdate = true
|
||||||
|
|
||||||
// Update the thread to be visible
|
// Update the thread to be visible (if it isn't already)
|
||||||
|
if !thread.shouldBeVisible {
|
||||||
_ = try SessionThread
|
_ = try SessionThread
|
||||||
.filter(id: threadId)
|
.filter(id: threadId)
|
||||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||||
|
}
|
||||||
|
|
||||||
// Create the interaction
|
// Create the interaction
|
||||||
let interaction: Interaction = try Interaction(
|
let interaction: Interaction = try Interaction(
|
||||||
|
@ -1301,7 +1305,7 @@ extension ConversationVC:
|
||||||
.suffix(19))
|
.suffix(19))
|
||||||
.appending(sentTimestamp)
|
.appending(sentTimestamp)
|
||||||
}
|
}
|
||||||
// TODO: Need to test emoji reacts for both open groups and one-to-one to make sure this isn't broken
|
|
||||||
// Perform the sending logic
|
// Perform the sending logic
|
||||||
Storage.shared
|
Storage.shared
|
||||||
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
||||||
|
@ -1311,10 +1315,12 @@ extension ConversationVC:
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the thread to be visible
|
// Update the thread to be visible (if it isn't already)
|
||||||
|
if !thread.shouldBeVisible {
|
||||||
_ = try SessionThread
|
_ = try SessionThread
|
||||||
.filter(id: thread.id)
|
.filter(id: thread.id)
|
||||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||||
|
}
|
||||||
|
|
||||||
let pendingReaction: Reaction? = {
|
let pendingReaction: Reaction? = {
|
||||||
if remove {
|
if remove {
|
||||||
|
|
|
@ -528,6 +528,25 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
||||||
mediaCache.removeAllObjects()
|
mediaCache.removeAllObjects()
|
||||||
hasReloadedThreadDataAfterDisappearance = false
|
hasReloadedThreadDataAfterDisappearance = false
|
||||||
viewIsDisappearing = false
|
viewIsDisappearing = false
|
||||||
|
|
||||||
|
// If the user just created this thread but didn't send a message then we want to delete the
|
||||||
|
// "shadow" thread since it's not actually in use (this is to prevent it from taking up database
|
||||||
|
// space or unintentionally getting synced via libSession in the future)
|
||||||
|
let threadId: String = viewModel.threadData.threadId
|
||||||
|
|
||||||
|
if
|
||||||
|
viewModel.threadData.threadShouldBeVisible == false &&
|
||||||
|
!SessionUtil.conversationExistsInConfig(
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: viewModel.threadData.threadVariant
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Storage.shared.writeAsync { db in
|
||||||
|
_ = try SessionThread
|
||||||
|
.filter(id: threadId)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
@objc func applicationDidBecomeActive(_ notification: Notification) {
|
||||||
|
|
|
@ -30,6 +30,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
||||||
|
|
||||||
private let dependencies: Dependencies
|
private let dependencies: Dependencies
|
||||||
private let threadId: String
|
private let threadId: String
|
||||||
|
private let threadVariant: SessionThread.Variant
|
||||||
private let config: DisappearingMessagesConfiguration
|
private let config: DisappearingMessagesConfiguration
|
||||||
private var storedSelection: TimeInterval
|
private var storedSelection: TimeInterval
|
||||||
private var currentSelection: CurrentValueSubject<TimeInterval, Never>
|
private var currentSelection: CurrentValueSubject<TimeInterval, Never>
|
||||||
|
@ -39,10 +40,12 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
||||||
init(
|
init(
|
||||||
dependencies: Dependencies = Dependencies(),
|
dependencies: Dependencies = Dependencies(),
|
||||||
threadId: String,
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
config: DisappearingMessagesConfiguration
|
config: DisappearingMessagesConfiguration
|
||||||
) {
|
) {
|
||||||
self.dependencies = dependencies
|
self.dependencies = dependencies
|
||||||
self.threadId = threadId
|
self.threadId = threadId
|
||||||
|
self.threadVariant = threadVariant
|
||||||
self.config = config
|
self.config = config
|
||||||
self.storedSelection = (config.isEnabled ? config.durationSeconds : 0)
|
self.storedSelection = (config.isEnabled ? config.durationSeconds : 0)
|
||||||
self.currentSelection = CurrentValueSubject(self.storedSelection)
|
self.currentSelection = CurrentValueSubject(self.storedSelection)
|
||||||
|
@ -134,6 +137,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
||||||
|
|
||||||
private func saveChanges() {
|
private func saveChanges() {
|
||||||
let threadId: String = self.threadId
|
let threadId: String = self.threadId
|
||||||
|
let threadVariant: SessionThread.Variant = self.threadVariant
|
||||||
let currentSelection: TimeInterval = self.currentSelection.value
|
let currentSelection: TimeInterval = self.currentSelection.value
|
||||||
let updatedConfig: DisappearingMessagesConfiguration = self.config
|
let updatedConfig: DisappearingMessagesConfiguration = self.config
|
||||||
.with(
|
.with(
|
||||||
|
@ -175,6 +179,19 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
||||||
interactionId: interaction.id,
|
interactionId: interaction.id,
|
||||||
in: thread
|
in: thread
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Legacy closed groups
|
||||||
|
switch threadVariant {
|
||||||
|
case .legacyGroup:
|
||||||
|
try SessionUtil
|
||||||
|
.update(
|
||||||
|
db,
|
||||||
|
groupPublicKey: threadId,
|
||||||
|
disappearingConfig: updatedConfig
|
||||||
|
)
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -445,6 +445,7 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
|
||||||
SessionTableViewController(
|
SessionTableViewController(
|
||||||
viewModel: ThreadDisappearingMessagesSettingsViewModel(
|
viewModel: ThreadDisappearingMessagesSettingsViewModel(
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
config: disappearingMessagesConfig
|
config: disappearingMessagesConfig
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -731,7 +731,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
||||||
cancelStyle: .alert_text,
|
cancelStyle: .alert_text,
|
||||||
dismissOnConfirm: true,
|
dismissOnConfirm: true,
|
||||||
onConfirm: { [weak self] _ in
|
onConfirm: { [weak self] _ in
|
||||||
self?.viewModel.delete(
|
self?.viewModel.deleteOrLeave(
|
||||||
threadId: threadViewModel.threadId,
|
threadId: threadViewModel.threadId,
|
||||||
threadVariant: threadViewModel.threadVariant
|
threadVariant: threadViewModel.threadVariant
|
||||||
)
|
)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
import DifferenceKit
|
import DifferenceKit
|
||||||
import SignalUtilitiesKit
|
import SignalUtilitiesKit
|
||||||
|
import SessionMessagingKit
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
public class HomeViewModel {
|
public class HomeViewModel {
|
||||||
|
@ -365,10 +366,27 @@ public class HomeViewModel {
|
||||||
threadViewModel.markAsUnread()
|
threadViewModel.markAsUnread()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
|
public func deleteOrLeave(threadId: String, threadVariant: SessionThread.Variant) {
|
||||||
Storage.shared.writeAsync { db in
|
Storage.shared.writeAsync { db in
|
||||||
switch threadVariant {
|
switch threadVariant {
|
||||||
case .contact:
|
case .contact:
|
||||||
|
// We need to custom handle the 'Note to Self' conversation (it should just be
|
||||||
|
// hidden rather than deleted
|
||||||
|
guard threadId != getUserHexEncodedPublicKey(db) else {
|
||||||
|
_ = try Interaction
|
||||||
|
.filter(Interaction.Columns.threadId == threadId)
|
||||||
|
.deleteAll(db)
|
||||||
|
|
||||||
|
_ = try SessionThread
|
||||||
|
.filter(id: threadId)
|
||||||
|
.updateAllAndConfig(
|
||||||
|
db,
|
||||||
|
SessionThread.Columns.shouldBeVisible.set(to: false)
|
||||||
|
)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try SessionUtil
|
try SessionUtil
|
||||||
.hide(db, contactIds: [threadId])
|
.hide(db, contactIds: [threadId])
|
||||||
|
|
||||||
|
|
|
@ -38,10 +38,8 @@ class ImagePickerHandler: NSObject, UIImagePickerControllerDelegate & UINavigati
|
||||||
// Check if the user selected an animated image (if so then don't crop, just
|
// Check if the user selected an animated image (if so then don't crop, just
|
||||||
// set the avatar directly
|
// set the avatar directly
|
||||||
guard
|
guard
|
||||||
let type: Any = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey])
|
let type: URLResourceValues = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey]),
|
||||||
.allValues
|
let typeString: String = type.typeIdentifier,
|
||||||
.first,
|
|
||||||
let typeString: String = type as? String,
|
|
||||||
MIMETypeUtil.supportedAnimatedImageUTITypes().contains(typeString)
|
MIMETypeUtil.supportedAnimatedImageUTITypes().contains(typeString)
|
||||||
else {
|
else {
|
||||||
let viewController: CropScaleImageViewController = CropScaleImageViewController(
|
let viewController: CropScaleImageViewController = CropScaleImageViewController(
|
||||||
|
|
|
@ -610,8 +610,6 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
button.isUserInteractionEnabled = false
|
button.isUserInteractionEnabled = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
UIView.transition(
|
UIView.transition(
|
||||||
with: button,
|
with: button,
|
||||||
duration: 0.25,
|
duration: 0.25,
|
||||||
|
|
|
@ -259,33 +259,33 @@ enum MockDataGenerator {
|
||||||
.saved(db)
|
.saved(db)
|
||||||
|
|
||||||
members.forEach { memberId in
|
members.forEach { memberId in
|
||||||
_ = try! GroupMember(
|
try! GroupMember(
|
||||||
groupId: randomGroupPublicKey,
|
groupId: randomGroupPublicKey,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
role: .standard,
|
role: .standard,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
)
|
)
|
||||||
.saved(db)
|
.save(db)
|
||||||
}
|
}
|
||||||
[members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId].forEach { adminId in
|
[members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId].forEach { adminId in
|
||||||
_ = try! GroupMember(
|
try! GroupMember(
|
||||||
groupId: randomGroupPublicKey,
|
groupId: randomGroupPublicKey,
|
||||||
profileId: adminId,
|
profileId: adminId,
|
||||||
role: .admin,
|
role: .admin,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
)
|
)
|
||||||
.saved(db)
|
.save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add the group to the user's set of public keys to poll for and store the key pair
|
// Add the group to the user's set of public keys to poll for and store the key pair
|
||||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||||
_ = try! ClosedGroupKeyPair(
|
try! ClosedGroupKeyPair(
|
||||||
threadId: randomGroupPublicKey,
|
threadId: randomGroupPublicKey,
|
||||||
publicKey: encryptionKeyPair.publicKey,
|
publicKey: encryptionKeyPair.publicKey,
|
||||||
secretKey: encryptionKeyPair.privateKey,
|
secretKey: encryptionKeyPair.privateKey,
|
||||||
receivedTimestamp: timestampNow
|
receivedTimestamp: timestampNow
|
||||||
)
|
)
|
||||||
.saved(db)
|
.save(db)
|
||||||
|
|
||||||
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
// Generate the message history (Note: Unapproved message requests will only include incoming messages)
|
||||||
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
logProgress("Closed Group Thread \(threadIndex)", "Generate \(numMessages) Messages")
|
||||||
|
|
|
@ -28,8 +28,14 @@ public enum SNMessagingKit { // Just to make the external API nice
|
||||||
], // Add job priorities
|
], // Add job priorities
|
||||||
[
|
[
|
||||||
_011_AddPendingReadReceipts.self,
|
_011_AddPendingReadReceipts.self,
|
||||||
_012_SharedUtilChanges.self
|
_012_SharedUtilChanges.self,
|
||||||
]
|
// Wait until the feature is turned on before doing the migration that generates
|
||||||
|
// the config dump data
|
||||||
|
(Features.useSharedUtilForUserConfig ?
|
||||||
|
_013_GenerateInitialUserConfigDumps.self :
|
||||||
|
(nil as Migration.Type?)
|
||||||
|
)
|
||||||
|
].compactMap { $0 }
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
import GRDB
|
import GRDB
|
||||||
import SessionUtil
|
import SessionUtil
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
/// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation
|
/// This migration makes the neccessary changes to support the updated user config syncing system
|
||||||
/// searh (currently it's much slower than the global search)
|
|
||||||
enum _012_SharedUtilChanges: Migration {
|
enum _012_SharedUtilChanges: Migration {
|
||||||
static let target: TargetMigrations.Identifier = .messagingKit
|
static let target: TargetMigrations.Identifier = .messagingKit
|
||||||
static let identifier: String = "SharedUtilChanges"
|
static let identifier: String = "SharedUtilChanges"
|
||||||
|
@ -20,10 +20,123 @@ enum _012_SharedUtilChanges: Migration {
|
||||||
t.add(.pinnedPriority, .integer)
|
t.add(.pinnedPriority, .integer)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add an index for the 'ClosedGroupKeyPair' so we can lookup existing keys
|
// 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()
|
||||||
|
.indexed() // Quicker querying
|
||||||
|
.references(SessionThread.self, onDelete: .cascade) // Delete if Thread deleted
|
||||||
|
t.column(.profileId, .text)
|
||||||
|
.notNull()
|
||||||
|
.indexed() // Quicker querying
|
||||||
|
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)
|
||||||
|
|
||||||
|
// 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()
|
||||||
|
.indexed() // Quicker querying
|
||||||
|
.references(ClosedGroup.self, onDelete: .cascade) // Delete if ClosedGroup deleted
|
||||||
|
t.column(.publicKey, .blob).notNull()
|
||||||
|
t.column(.secretKey, .blob).notNull()
|
||||||
|
t.column(.receivedTimestamp, .double)
|
||||||
|
.notNull()
|
||||||
|
.indexed() // Quicker querying
|
||||||
|
t.column(.threadKeyPairHash, .integer)
|
||||||
|
.notNull()
|
||||||
|
.unique()
|
||||||
|
.indexed()
|
||||||
|
}
|
||||||
|
// 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
|
||||||
try db.createIndex(
|
try db.createIndex(
|
||||||
on: ClosedGroupKeyPair.self,
|
on: ClosedGroupKeyPair.self,
|
||||||
columns: [.threadId, .publicKey, .secretKey]
|
columns: [.threadId, .threadKeyPairHash]
|
||||||
)
|
)
|
||||||
|
|
||||||
// New table for storing the latest config dump for each type
|
// New table for storing the latest config dump for each type
|
||||||
|
@ -50,170 +163,11 @@ enum _012_SharedUtilChanges: Migration {
|
||||||
// If we don't have an ed25519 key then no need to create cached dump data
|
// If we don't have an ed25519 key then no need to create cached dump data
|
||||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
|
|
||||||
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else {
|
// There was previously a bug which allowed users to fully delete the 'Note to Self'
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
// conversation but we don't want that, so create it again if it doesn't exists
|
||||||
return
|
try SessionThread
|
||||||
}
|
.fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false)
|
||||||
|
|
||||||
// MARK: - Shared Data
|
|
||||||
|
|
||||||
let allThreads: [String: SessionThread] = try SessionThread
|
|
||||||
.fetchAll(db)
|
|
||||||
.reduce(into: [:]) { result, next in result[next.id] = next }
|
|
||||||
|
|
||||||
// MARK: - UserProfile Config Dump
|
|
||||||
|
|
||||||
let userProfileConf: UnsafeMutablePointer<config_object>? = try SessionUtil.loadState(
|
|
||||||
for: .userProfile,
|
|
||||||
secretKey: secretKey,
|
|
||||||
cachedData: nil
|
|
||||||
)
|
|
||||||
try SessionUtil.update(
|
|
||||||
profile: Profile.fetchOrCreateCurrentUser(db),
|
|
||||||
in: userProfileConf
|
|
||||||
)
|
|
||||||
|
|
||||||
if config_needs_dump(userProfileConf) {
|
|
||||||
try SessionUtil
|
|
||||||
.createDump(
|
|
||||||
conf: userProfileConf,
|
|
||||||
for: .userProfile,
|
|
||||||
publicKey: userPublicKey
|
|
||||||
)?
|
|
||||||
.save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Contact Config Dump
|
|
||||||
|
|
||||||
let contactsData: [ContactInfo] = try Contact
|
|
||||||
.filter(
|
|
||||||
Contact.Columns.isBlocked == true ||
|
|
||||||
allThreads.keys.contains(Contact.Columns.id)
|
|
||||||
)
|
|
||||||
.including(optional: Contact.profile)
|
|
||||||
.asRequest(of: ContactInfo.self)
|
|
||||||
.fetchAll(db)
|
|
||||||
|
|
||||||
let contactsConf: UnsafeMutablePointer<config_object>? = try SessionUtil.loadState(
|
|
||||||
for: .contacts,
|
|
||||||
secretKey: secretKey,
|
|
||||||
cachedData: nil
|
|
||||||
)
|
|
||||||
try SessionUtil.upsert(
|
|
||||||
contactData: contactsData
|
|
||||||
.map { data in
|
|
||||||
SessionUtil.SyncedContactInfo(
|
|
||||||
id: data.contact.id,
|
|
||||||
contact: data.contact,
|
|
||||||
profile: data.profile,
|
|
||||||
priority: Int32(allThreads[data.contact.id]?.pinnedPriority ?? 0),
|
|
||||||
hidden: (allThreads[data.contact.id]?.shouldBeVisible == true)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
in: contactsConf
|
|
||||||
)
|
|
||||||
|
|
||||||
if config_needs_dump(contactsConf) {
|
|
||||||
try SessionUtil
|
|
||||||
.createDump(
|
|
||||||
conf: contactsConf,
|
|
||||||
for: .contacts,
|
|
||||||
publicKey: userPublicKey
|
|
||||||
)?
|
|
||||||
.save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - ConvoInfoVolatile Config Dump
|
|
||||||
|
|
||||||
let volatileThreadInfo: [SessionUtil.VolatileThreadInfo] = SessionUtil.VolatileThreadInfo.fetchAll(db)
|
|
||||||
let convoInfoVolatileConf: UnsafeMutablePointer<config_object>? = try SessionUtil.loadState(
|
|
||||||
for: .convoInfoVolatile,
|
|
||||||
secretKey: secretKey,
|
|
||||||
cachedData: nil
|
|
||||||
)
|
|
||||||
try SessionUtil.upsert(
|
|
||||||
convoInfoVolatileChanges: volatileThreadInfo,
|
|
||||||
in: convoInfoVolatileConf
|
|
||||||
)
|
|
||||||
|
|
||||||
if config_needs_dump(convoInfoVolatileConf) {
|
|
||||||
try SessionUtil
|
|
||||||
.createDump(
|
|
||||||
conf: convoInfoVolatileConf,
|
|
||||||
for: .convoInfoVolatile,
|
|
||||||
publicKey: userPublicKey
|
|
||||||
)?
|
|
||||||
.save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UserGroups Config Dump
|
|
||||||
|
|
||||||
let legacyGroupData: [SessionUtil.LegacyGroupInfo] = try SessionUtil.LegacyGroupInfo.fetchAll(db)
|
|
||||||
let communityData: [SessionUtil.OpenGroupUrlInfo] = try SessionUtil.OpenGroupUrlInfo.fetchAll(db)
|
|
||||||
|
|
||||||
let userGroupsConf: UnsafeMutablePointer<config_object>? = try SessionUtil.loadState(
|
|
||||||
for: .userGroups,
|
|
||||||
secretKey: secretKey,
|
|
||||||
cachedData: nil
|
|
||||||
)
|
|
||||||
try SessionUtil.upsert(
|
|
||||||
legacyGroups: legacyGroupData,
|
|
||||||
in: userGroupsConf
|
|
||||||
)
|
|
||||||
try SessionUtil.upsert(
|
|
||||||
communities: communityData
|
|
||||||
.map { SessionUtil.CommunityInfo(urlInfo: $0) },
|
|
||||||
in: userGroupsConf
|
|
||||||
)
|
|
||||||
|
|
||||||
if config_needs_dump(userGroupsConf) {
|
|
||||||
try SessionUtil
|
|
||||||
.createDump(
|
|
||||||
conf: userGroupsConf,
|
|
||||||
for: .userGroups,
|
|
||||||
publicKey: userPublicKey
|
|
||||||
)?
|
|
||||||
.save(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Threads
|
|
||||||
|
|
||||||
try SessionUtil
|
|
||||||
.updatingThreads(db, Array(allThreads.values))
|
|
||||||
|
|
||||||
// MARK: - Syncing
|
|
||||||
|
|
||||||
// Enqueue a config sync job to ensure the generated configs get synced
|
|
||||||
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
|
|
||||||
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: Fetchable Types
|
|
||||||
|
|
||||||
struct ContactInfo: FetchableRecord, Decodable, ColumnExpressible {
|
|
||||||
typealias Columns = CodingKeys
|
|
||||||
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
|
|
||||||
case contact
|
|
||||||
case profile
|
|
||||||
}
|
|
||||||
|
|
||||||
let contact: Contact
|
|
||||||
let profile: Profile?
|
|
||||||
}
|
|
||||||
|
|
||||||
struct GroupInfo: FetchableRecord, Decodable, ColumnExpressible {
|
|
||||||
typealias Columns = CodingKeys
|
|
||||||
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
|
|
||||||
case closedGroup
|
|
||||||
case disappearingMessagesConfiguration
|
|
||||||
case groupMembers
|
|
||||||
}
|
|
||||||
|
|
||||||
let closedGroup: ClosedGroup
|
|
||||||
let disappearingMessagesConfiguration: DisappearingMessagesConfiguration?
|
|
||||||
let groupMembers: [GroupMember]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,188 @@
|
||||||
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import GRDB
|
||||||
|
import SessionUtil
|
||||||
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
/// This migration goes through the current state of the database and generates config dumps for the user config types
|
||||||
|
///
|
||||||
|
/// **Note:** This migration won't be run until the `useSharedUtilForUserConfig` feature flag is enabled
|
||||||
|
enum _013_GenerateInitialUserConfigDumps: Migration {
|
||||||
|
static let target: TargetMigrations.Identifier = .messagingKit
|
||||||
|
static let identifier: String = "GenerateInitialUserConfigDumps"
|
||||||
|
static let needsConfigSync: Bool = true
|
||||||
|
static let minExpectedRunDuration: TimeInterval = 0.1 // TODO: Need to test this
|
||||||
|
|
||||||
|
static func migrate(_ db: Database) throws {
|
||||||
|
// If we have no ed25519 key then there is no need to create cached dump data
|
||||||
|
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else { return }
|
||||||
|
|
||||||
|
// Load the initial config state if needed
|
||||||
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
|
|
||||||
|
SessionUtil.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey)
|
||||||
|
|
||||||
|
// Retrieve all threads (we are going to base the config dump data on the active
|
||||||
|
// threads rather than anything else in the database)
|
||||||
|
let allThreads: [String: SessionThread] = try SessionThread
|
||||||
|
.fetchAll(db)
|
||||||
|
.reduce(into: [:]) { result, next in result[next.id] = next }
|
||||||
|
|
||||||
|
// MARK: - UserProfile Config Dump
|
||||||
|
|
||||||
|
try SessionUtil
|
||||||
|
.config(for: .userProfile, publicKey: userPublicKey)
|
||||||
|
.mutate { conf in
|
||||||
|
try SessionUtil.update(
|
||||||
|
profile: Profile.fetchOrCreateCurrentUser(db),
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_needs_dump(conf) {
|
||||||
|
try SessionUtil
|
||||||
|
.createDump(
|
||||||
|
conf: conf,
|
||||||
|
for: .userProfile,
|
||||||
|
publicKey: userPublicKey
|
||||||
|
)?
|
||||||
|
.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Contact Config Dump
|
||||||
|
|
||||||
|
try SessionUtil
|
||||||
|
.config(for: .contacts, publicKey: userPublicKey)
|
||||||
|
.mutate { conf in
|
||||||
|
let contactsData: [ContactInfo] = try Contact
|
||||||
|
.filter(
|
||||||
|
Contact.Columns.isBlocked == true ||
|
||||||
|
allThreads.keys.contains(Contact.Columns.id)
|
||||||
|
)
|
||||||
|
.including(optional: Contact.profile)
|
||||||
|
.asRequest(of: ContactInfo.self)
|
||||||
|
.fetchAll(db)
|
||||||
|
|
||||||
|
try SessionUtil.upsert(
|
||||||
|
contactData: contactsData
|
||||||
|
.map { data in
|
||||||
|
SessionUtil.SyncedContactInfo(
|
||||||
|
id: data.contact.id,
|
||||||
|
contact: data.contact,
|
||||||
|
profile: data.profile,
|
||||||
|
hidden: (allThreads[data.contact.id]?.shouldBeVisible == true),
|
||||||
|
priority: Int32(allThreads[data.contact.id]?.pinnedPriority ?? 0)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_needs_dump(conf) {
|
||||||
|
try SessionUtil
|
||||||
|
.createDump(
|
||||||
|
conf: conf,
|
||||||
|
for: .contacts,
|
||||||
|
publicKey: userPublicKey
|
||||||
|
)?
|
||||||
|
.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - ConvoInfoVolatile Config Dump
|
||||||
|
|
||||||
|
try SessionUtil
|
||||||
|
.config(for: .convoInfoVolatile, publicKey: userPublicKey)
|
||||||
|
.mutate { conf in
|
||||||
|
let volatileThreadInfo: [SessionUtil.VolatileThreadInfo] = SessionUtil.VolatileThreadInfo
|
||||||
|
.fetchAll(db, ids: Array(allThreads.keys))
|
||||||
|
|
||||||
|
try SessionUtil.upsert(
|
||||||
|
convoInfoVolatileChanges: volatileThreadInfo,
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_needs_dump(conf) {
|
||||||
|
try SessionUtil
|
||||||
|
.createDump(
|
||||||
|
conf: conf,
|
||||||
|
for: .convoInfoVolatile,
|
||||||
|
publicKey: userPublicKey
|
||||||
|
)?
|
||||||
|
.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UserGroups Config Dump
|
||||||
|
|
||||||
|
try SessionUtil
|
||||||
|
.config(for: .userGroups, publicKey: userPublicKey)
|
||||||
|
.mutate { conf in
|
||||||
|
let legacyGroupData: [SessionUtil.LegacyGroupInfo] = try SessionUtil.LegacyGroupInfo.fetchAll(db)
|
||||||
|
let communityData: [SessionUtil.OpenGroupUrlInfo] = try SessionUtil.OpenGroupUrlInfo
|
||||||
|
.fetchAll(db, ids: Array(allThreads.keys))
|
||||||
|
|
||||||
|
try SessionUtil.upsert(
|
||||||
|
legacyGroups: legacyGroupData,
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
try SessionUtil.upsert(
|
||||||
|
communities: communityData
|
||||||
|
.map { urlInfo in
|
||||||
|
SessionUtil.CommunityInfo(
|
||||||
|
urlInfo: urlInfo,
|
||||||
|
priority: Int32(allThreads[urlInfo.threadId]?.pinnedPriority ?? 0)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
|
||||||
|
if config_needs_dump(conf) {
|
||||||
|
try SessionUtil
|
||||||
|
.createDump(
|
||||||
|
conf: conf,
|
||||||
|
for: .userGroups,
|
||||||
|
publicKey: userPublicKey
|
||||||
|
)?
|
||||||
|
.save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Threads
|
||||||
|
|
||||||
|
try SessionUtil.updatingThreads(db, Array(allThreads.values))
|
||||||
|
|
||||||
|
// MARK: - Syncing
|
||||||
|
|
||||||
|
// Enqueue a config sync job to ensure the generated configs get synced
|
||||||
|
db.afterNextTransactionNestedOnce(dedupeId: SessionUtil.syncDedupeId(userPublicKey)) { db in
|
||||||
|
ConfigurationSyncJob.enqueue(db, publicKey: userPublicKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ContactInfo: FetchableRecord, Decodable, ColumnExpressible {
|
||||||
|
typealias Columns = CodingKeys
|
||||||
|
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
|
||||||
|
case contact
|
||||||
|
case profile
|
||||||
|
}
|
||||||
|
|
||||||
|
let contact: Contact
|
||||||
|
let profile: Profile?
|
||||||
|
}
|
||||||
|
|
||||||
|
struct GroupInfo: FetchableRecord, Decodable, ColumnExpressible {
|
||||||
|
typealias Columns = CodingKeys
|
||||||
|
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
|
||||||
|
case closedGroup
|
||||||
|
case disappearingMessagesConfiguration
|
||||||
|
case groupMembers
|
||||||
|
}
|
||||||
|
|
||||||
|
let closedGroup: ClosedGroup
|
||||||
|
let disappearingMessagesConfiguration: DisappearingMessagesConfiguration?
|
||||||
|
let groupMembers: [GroupMember]
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
import GRDB
|
import GRDB
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
|
|
||||||
|
@ -18,12 +19,14 @@ public struct ClosedGroupKeyPair: Codable, Equatable, FetchableRecord, Persistab
|
||||||
case publicKey
|
case publicKey
|
||||||
case secretKey
|
case secretKey
|
||||||
case receivedTimestamp
|
case receivedTimestamp
|
||||||
|
case threadKeyPairHash
|
||||||
}
|
}
|
||||||
|
|
||||||
public let threadId: String
|
public let threadId: String
|
||||||
public let publicKey: Data
|
public let publicKey: Data
|
||||||
public let secretKey: Data
|
public let secretKey: Data
|
||||||
public let receivedTimestamp: TimeInterval
|
public let receivedTimestamp: TimeInterval
|
||||||
|
public let threadKeyPairHash: String
|
||||||
|
|
||||||
// MARK: - Relationships
|
// MARK: - Relationships
|
||||||
|
|
||||||
|
@ -43,6 +46,12 @@ public struct ClosedGroupKeyPair: Codable, Equatable, FetchableRecord, Persistab
|
||||||
self.publicKey = publicKey
|
self.publicKey = publicKey
|
||||||
self.secretKey = secretKey
|
self.secretKey = secretKey
|
||||||
self.receivedTimestamp = receivedTimestamp
|
self.receivedTimestamp = receivedTimestamp
|
||||||
|
|
||||||
|
// This value has a unique constraint and is used for key de-duping so the formula
|
||||||
|
// shouldn't be modified unless all existing keys have their values updated
|
||||||
|
self.threadKeyPairHash = Insecure.MD5
|
||||||
|
.hash(data: threadId.bytes + publicKey.bytes + secretKey.bytes)
|
||||||
|
.hexString
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
@ -27,7 +27,8 @@ internal extension SessionUtil {
|
||||||
String: (
|
String: (
|
||||||
contact: Contact,
|
contact: Contact,
|
||||||
profile: Profile,
|
profile: Profile,
|
||||||
isHiddenConversation: Bool
|
shouldBeVisible: Bool,
|
||||||
|
priority: Int32
|
||||||
)
|
)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -66,7 +67,8 @@ internal extension SessionUtil {
|
||||||
contactData[contactId] = (
|
contactData[contactId] = (
|
||||||
contactResult,
|
contactResult,
|
||||||
profileResult,
|
profileResult,
|
||||||
contact.hidden
|
(contact.hidden == false),
|
||||||
|
contact.priority
|
||||||
)
|
)
|
||||||
contacts_iterator_advance(contactIterator)
|
contacts_iterator_advance(contactIterator)
|
||||||
}
|
}
|
||||||
|
@ -148,33 +150,40 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
/// If the contact's `hidden` flag doesn't match the visibility of their conversation then create/delete the
|
/// If the contact's `hidden` flag doesn't match the visibility of their conversation then create/delete the
|
||||||
/// associated contact conversation accordingly
|
/// associated contact conversation accordingly
|
||||||
let threadExists: Bool = try SessionThread.exists(db, id: contact.id)
|
let threadInfo: PriorityVisibilityInfo? = try? SessionThread
|
||||||
let threadIsVisible: Bool = try SessionThread
|
.select(.id, .variant, .pinnedPriority, .shouldBeVisible)
|
||||||
.filter(id: contact.id)
|
.asRequest(of: PriorityVisibilityInfo.self)
|
||||||
.select(.shouldBeVisible)
|
|
||||||
.asRequest(of: Bool.self)
|
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
.defaulting(to: false)
|
let threadExists: Bool = (threadInfo != nil)
|
||||||
|
let threadIsVisible: Bool = (threadInfo?.shouldBeVisible ?? false)
|
||||||
|
|
||||||
switch (data.isHiddenConversation, threadExists, threadIsVisible) {
|
switch (data.shouldBeVisible, threadExists, threadIsVisible) {
|
||||||
case (true, true, _):
|
case (false, true, _):
|
||||||
try SessionThread
|
try SessionThread
|
||||||
.filter(id: contact.id)
|
.filter(id: contact.id)
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
case (false, false, _):
|
case (true, false, _):
|
||||||
try SessionThread(
|
try SessionThread(
|
||||||
id: contact.id,
|
id: contact.id,
|
||||||
variant: .contact,
|
variant: .contact,
|
||||||
shouldBeVisible: true
|
shouldBeVisible: true,
|
||||||
|
pinnedPriority: data.priority
|
||||||
).save(db)
|
).save(db)
|
||||||
|
|
||||||
case (false, true, false):
|
case (true, true, false):
|
||||||
|
let changes: [ConfigColumnAssignment] = [
|
||||||
|
SessionThread.Columns.shouldBeVisible.set(to: data.shouldBeVisible),
|
||||||
|
(threadInfo?.pinnedPriority == data.priority ? nil :
|
||||||
|
SessionThread.Columns.pinnedPriority.set(to: data.priority)
|
||||||
|
)
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
try SessionThread
|
try SessionThread
|
||||||
.filter(id: contact.id)
|
.filter(id: contact.id)
|
||||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||||
db,
|
db,
|
||||||
SessionThread.Columns.shouldBeVisible.set(to: !data.isHiddenConversation)
|
changes
|
||||||
)
|
)
|
||||||
|
|
||||||
default: break
|
default: break
|
||||||
|
@ -253,26 +262,7 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - External Outgoing Changes
|
// MARK: - Outgoing Changes
|
||||||
|
|
||||||
public extension SessionUtil {
|
|
||||||
static func hide(_ db: Database, contactIds: [String]) throws {
|
|
||||||
try SessionUtil.performAndPushChange(
|
|
||||||
db,
|
|
||||||
for: .contacts,
|
|
||||||
publicKey: getUserHexEncodedPublicKey(db)
|
|
||||||
) { conf in
|
|
||||||
// Mark the contacts as hidden
|
|
||||||
try SessionUtil.upsert(
|
|
||||||
contactData: contactIds
|
|
||||||
.map { SyncedContactInfo(id: $0, hidden: true) },
|
|
||||||
in: conf
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Convenience
|
|
||||||
|
|
||||||
internal extension SessionUtil {
|
internal extension SessionUtil {
|
||||||
static func updatingContacts<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
static func updatingContacts<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
||||||
|
@ -285,7 +275,6 @@ internal extension SessionUtil {
|
||||||
// If we only updated the current user contact then no need to continue
|
// If we only updated the current user contact then no need to continue
|
||||||
guard !targetContacts.isEmpty else { return updated }
|
guard !targetContacts.isEmpty else { return updated }
|
||||||
|
|
||||||
do {
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .contacts,
|
for: .contacts,
|
||||||
|
@ -324,10 +313,6 @@ internal extension SessionUtil {
|
||||||
in: conf
|
in: conf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch {
|
|
||||||
SNLog("[libSession-util] Failed to dump updated data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
|
@ -357,7 +342,6 @@ internal extension SessionUtil {
|
||||||
existingContactIds.contains($0.id)
|
existingContactIds.contains($0.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
do {
|
|
||||||
// Update the user profile first (if needed)
|
// Update the user profile first (if needed)
|
||||||
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
|
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
|
@ -384,15 +368,49 @@ internal extension SessionUtil {
|
||||||
in: conf
|
in: conf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch {
|
|
||||||
SNLog("[libSession-util] Failed to dump updated data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - External Outgoing Changes
|
||||||
|
|
||||||
|
public extension SessionUtil {
|
||||||
|
static func hide(_ db: Database, contactIds: [String]) throws {
|
||||||
|
try SessionUtil.performAndPushChange(
|
||||||
|
db,
|
||||||
|
for: .contacts,
|
||||||
|
publicKey: getUserHexEncodedPublicKey(db)
|
||||||
|
) { conf in
|
||||||
|
// Mark the contacts as hidden
|
||||||
|
try SessionUtil.upsert(
|
||||||
|
contactData: contactIds
|
||||||
|
.map { SyncedContactInfo(id: $0, hidden: true) },
|
||||||
|
in: conf
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func remove(_ db: Database, contactIds: [String]) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
guard !contactIds.isEmpty else { return }
|
||||||
|
|
||||||
|
try SessionUtil.performAndPushChange(
|
||||||
|
db,
|
||||||
|
for: .contacts,
|
||||||
|
publicKey: getUserHexEncodedPublicKey(db)
|
||||||
|
) { conf in
|
||||||
|
contactIds.forEach { sessionId in
|
||||||
|
var cSessionId: [CChar] = sessionId.cArray
|
||||||
|
|
||||||
|
// Don't care if the contact doesn't exist
|
||||||
|
contacts_erase(conf, &cSessionId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - SyncedContactInfo
|
// MARK: - SyncedContactInfo
|
||||||
|
|
||||||
extension SessionUtil {
|
extension SessionUtil {
|
||||||
|
@ -400,21 +418,21 @@ extension SessionUtil {
|
||||||
let id: String
|
let id: String
|
||||||
let contact: Contact?
|
let contact: Contact?
|
||||||
let profile: Profile?
|
let profile: Profile?
|
||||||
let priority: Int32?
|
|
||||||
let hidden: Bool?
|
let hidden: Bool?
|
||||||
|
let priority: Int32?
|
||||||
|
|
||||||
init(
|
init(
|
||||||
id: String,
|
id: String,
|
||||||
contact: Contact? = nil,
|
contact: Contact? = nil,
|
||||||
profile: Profile? = nil,
|
profile: Profile? = nil,
|
||||||
priority: Int32? = nil,
|
hidden: Bool? = nil,
|
||||||
hidden: Bool? = nil
|
priority: Int32? = nil
|
||||||
) {
|
) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.contact = contact
|
self.contact = contact
|
||||||
self.profile = profile
|
self.profile = profile
|
||||||
self.priority = priority
|
|
||||||
self.hidden = hidden
|
self.hidden = hidden
|
||||||
|
self.priority = priority
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -158,17 +158,11 @@ internal extension SessionUtil {
|
||||||
// If there are no newer local last read timestamps then just return the mergeResult
|
// If there are no newer local last read timestamps then just return the mergeResult
|
||||||
guard !newerLocalChanges.isEmpty else { return }
|
guard !newerLocalChanges.isEmpty else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
|
||||||
db,
|
|
||||||
for: .convoInfoVolatile,
|
|
||||||
publicKey: getUserHexEncodedPublicKey(db)
|
|
||||||
) { conf in
|
|
||||||
try upsert(
|
try upsert(
|
||||||
convoInfoVolatileChanges: newerLocalChanges,
|
convoInfoVolatileChanges: newerLocalChanges,
|
||||||
in: conf
|
in: conf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
static func upsert(
|
static func upsert(
|
||||||
convoInfoVolatileChanges: [VolatileThreadInfo],
|
convoInfoVolatileChanges: [VolatileThreadInfo],
|
||||||
|
@ -176,7 +170,21 @@ internal extension SessionUtil {
|
||||||
) throws {
|
) throws {
|
||||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||||
|
|
||||||
convoInfoVolatileChanges.forEach { threadInfo in
|
// Exclude any invalid thread info
|
||||||
|
let validChanges: [VolatileThreadInfo] = convoInfoVolatileChanges
|
||||||
|
.filter { info in
|
||||||
|
switch info.variant {
|
||||||
|
case .contact:
|
||||||
|
// FIXME: libSession V1 doesn't sync volatileThreadInfo for blinded message requests
|
||||||
|
guard SessionId(from: info.threadId)?.prefix == .standard else { return false }
|
||||||
|
|
||||||
|
return true
|
||||||
|
|
||||||
|
default: return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
validChanges.forEach { threadInfo in
|
||||||
var cThreadId: [CChar] = threadInfo.threadId.cArray
|
var cThreadId: [CChar] = threadInfo.threadId.cArray
|
||||||
|
|
||||||
switch threadInfo.variant {
|
switch threadInfo.variant {
|
||||||
|
@ -250,11 +258,7 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Convenience
|
|
||||||
|
|
||||||
internal extension SessionUtil {
|
|
||||||
static func updateMarkedAsUnreadState(
|
static func updateMarkedAsUnreadState(
|
||||||
_ db: Database,
|
_ db: Database,
|
||||||
threads: [SessionThread]
|
threads: [SessionThread]
|
||||||
|
@ -284,13 +288,20 @@ internal extension SessionUtil {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - External Outgoing Changes
|
||||||
|
|
||||||
|
public extension SessionUtil {
|
||||||
static func syncThreadLastReadIfNeeded(
|
static func syncThreadLastReadIfNeeded(
|
||||||
_ db: Database,
|
_ db: Database,
|
||||||
threadId: String,
|
threadId: String,
|
||||||
threadVariant: SessionThread.Variant,
|
threadVariant: SessionThread.Variant,
|
||||||
lastReadTimestampMs: Int64
|
lastReadTimestampMs: Int64
|
||||||
) throws {
|
) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||||
threadId: threadId,
|
threadId: threadId,
|
||||||
variant: threadVariant,
|
variant: threadVariant,
|
||||||
|
|
|
@ -20,6 +20,7 @@ internal extension SessionUtil {
|
||||||
.appending(contentsOf: columnsRelatedToContacts)
|
.appending(contentsOf: columnsRelatedToContacts)
|
||||||
.appending(contentsOf: columnsRelatedToConvoInfoVolatile)
|
.appending(contentsOf: columnsRelatedToConvoInfoVolatile)
|
||||||
.appending(contentsOf: columnsRelatedToUserGroups)
|
.appending(contentsOf: columnsRelatedToUserGroups)
|
||||||
|
.appending(contentsOf: columnsRelatedToThreads)
|
||||||
.map { ColumnKey($0) }
|
.map { ColumnKey($0) }
|
||||||
.asSet()
|
.asSet()
|
||||||
|
|
||||||
|
@ -34,7 +35,10 @@ internal extension SessionUtil {
|
||||||
) throws {
|
) throws {
|
||||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||||
// type which has blocking access in it's `mutate` closure
|
// type which has blocking access in it's `mutate` closure
|
||||||
let needsPush: Bool = try SessionUtil
|
let needsPush: Bool
|
||||||
|
|
||||||
|
do {
|
||||||
|
needsPush = try SessionUtil
|
||||||
.config(
|
.config(
|
||||||
for: variant,
|
for: variant,
|
||||||
publicKey: publicKey
|
publicKey: publicKey
|
||||||
|
@ -56,6 +60,11 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
return config_needs_push(conf)
|
return config_needs_push(conf)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
SNLog("[libSession] Failed to update/dump updated \(variant) config data")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we need a push before scheduling one
|
// Make sure we need a push before scheduling one
|
||||||
guard needsPush else { return }
|
guard needsPush else { return }
|
||||||
|
@ -80,7 +89,6 @@ internal extension SessionUtil {
|
||||||
.fetchAll(db, ids: updatedThreads.map { $0.id })
|
.fetchAll(db, ids: updatedThreads.map { $0.id })
|
||||||
.reduce(into: [:]) { result, next in result[next.threadId] = next }
|
.reduce(into: [:]) { result, next in result[next.threadId] = next }
|
||||||
|
|
||||||
do {
|
|
||||||
// Update the unread state for the threads first (just in case that's what changed)
|
// Update the unread state for the threads first (just in case that's what changed)
|
||||||
try SessionUtil.updateMarkedAsUnreadState(db, threads: updatedThreads)
|
try SessionUtil.updateMarkedAsUnreadState(db, threads: updatedThreads)
|
||||||
|
|
||||||
|
@ -97,11 +105,10 @@ internal extension SessionUtil {
|
||||||
publicKey: userPublicKey
|
publicKey: userPublicKey
|
||||||
) { conf in
|
) { conf in
|
||||||
try SessionUtil.updateNoteToSelf(
|
try SessionUtil.updateNoteToSelf(
|
||||||
db,
|
hidden: !noteToSelf.shouldBeVisible,
|
||||||
priority: noteToSelf.pinnedPriority
|
priority: noteToSelf.pinnedPriority
|
||||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||||
.defaulting(to: 0),
|
.defaulting(to: 0),
|
||||||
hidden: noteToSelf.shouldBeVisible,
|
|
||||||
in: conf
|
in: conf
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -122,10 +129,10 @@ internal extension SessionUtil {
|
||||||
.map { thread in
|
.map { thread in
|
||||||
SyncedContactInfo(
|
SyncedContactInfo(
|
||||||
id: thread.id,
|
id: thread.id,
|
||||||
|
hidden: !thread.shouldBeVisible,
|
||||||
priority: thread.pinnedPriority
|
priority: thread.pinnedPriority
|
||||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||||
.defaulting(to: 0),
|
.defaulting(to: 0)
|
||||||
hidden: thread.shouldBeVisible
|
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
in: conf
|
in: conf
|
||||||
|
@ -165,7 +172,7 @@ internal extension SessionUtil {
|
||||||
.map { thread in
|
.map { thread in
|
||||||
LegacyGroupInfo(
|
LegacyGroupInfo(
|
||||||
id: thread.id,
|
id: thread.id,
|
||||||
hidden: thread.shouldBeVisible,
|
hidden: !thread.shouldBeVisible,
|
||||||
priority: thread.pinnedPriority
|
priority: thread.pinnedPriority
|
||||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||||
.defaulting(to: 0)
|
.defaulting(to: 0)
|
||||||
|
@ -176,19 +183,70 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
case .group:
|
case .group:
|
||||||
// TODO: Add this
|
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
catch {
|
|
||||||
SNLog("[libSession-util] Failed to dump updated data")
|
|
||||||
}
|
|
||||||
|
|
||||||
return updated
|
return updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - External Outgoing Changes
|
||||||
|
|
||||||
|
public extension SessionUtil {
|
||||||
|
static func conversationExistsInConfig(
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant
|
||||||
|
) -> Bool {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return true }
|
||||||
|
|
||||||
|
let configVariant: ConfigDump.Variant = {
|
||||||
|
switch threadVariant {
|
||||||
|
case .contact: return .contacts
|
||||||
|
case .legacyGroup, .group, .community: return .userGroups
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
return SessionUtil
|
||||||
|
.config(for: configVariant, publicKey: getUserHexEncodedPublicKey())
|
||||||
|
.wrappedValue
|
||||||
|
.map { conf in
|
||||||
|
var cThreadId: [CChar] = threadId.cArray
|
||||||
|
|
||||||
|
switch threadVariant {
|
||||||
|
case .contact: return contacts_get(conf, nil, &cThreadId)
|
||||||
|
|
||||||
|
case .community:
|
||||||
|
let maybeUrlInfo: OpenGroupUrlInfo? = Storage.shared
|
||||||
|
.read { db in try OpenGroupUrlInfo.fetchAll(db, ids: [threadId]) }?
|
||||||
|
.first
|
||||||
|
|
||||||
|
guard let urlInfo: OpenGroupUrlInfo = maybeUrlInfo else { return false }
|
||||||
|
|
||||||
|
var cBaseUrl: [CChar] = urlInfo.server.cArray
|
||||||
|
var cRoom: [CChar] = urlInfo.roomToken.cArray
|
||||||
|
|
||||||
|
return user_groups_get_community(conf, nil, &cBaseUrl, &cRoom)
|
||||||
|
|
||||||
|
case .legacyGroup:
|
||||||
|
let groupInfo: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cThreadId)
|
||||||
|
|
||||||
|
if groupInfo != nil {
|
||||||
|
ugroups_legacy_group_free(groupInfo)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
|
||||||
|
case .group:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.defaulting(to: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - ColumnKey
|
// MARK: - ColumnKey
|
||||||
|
|
||||||
internal extension SessionUtil {
|
internal extension SessionUtil {
|
||||||
|
|
|
@ -25,7 +25,6 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
var communities: [PrioritisedData<OpenGroupUrlInfo>] = []
|
var communities: [PrioritisedData<OpenGroupUrlInfo>] = []
|
||||||
var legacyGroups: [LegacyGroupInfo] = []
|
var legacyGroups: [LegacyGroupInfo] = []
|
||||||
var groups: [PrioritisedData<String>] = []
|
|
||||||
var community: ugroups_community_info = ugroups_community_info()
|
var community: ugroups_community_info = ugroups_community_info()
|
||||||
var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info()
|
var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info()
|
||||||
let groupsIterator: OpaquePointer = user_groups_iterator_new(conf)
|
let groupsIterator: OpaquePointer = user_groups_iterator_new(conf)
|
||||||
|
@ -85,15 +84,25 @@ internal extension SessionUtil {
|
||||||
.defaultWith(groupId)
|
.defaultWith(groupId)
|
||||||
.with(
|
.with(
|
||||||
isEnabled: (legacyGroup.disappearing_timer > 0),
|
isEnabled: (legacyGroup.disappearing_timer > 0),
|
||||||
durationSeconds: (legacyGroup.disappearing_timer == 0 ? nil :
|
durationSeconds: TimeInterval(legacyGroup.disappearing_timer)
|
||||||
TimeInterval(legacyGroup.disappearing_timer)
|
|
||||||
)
|
|
||||||
),
|
),
|
||||||
groupMembers: members.map { memberId, admin in
|
groupMembers: members
|
||||||
|
.filter { _, isAdmin in !isAdmin }
|
||||||
|
.map { memberId, admin in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
groupId: groupId,
|
groupId: groupId,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
role: (admin ? .admin : .standard),
|
role: .standard,
|
||||||
|
isHidden: false
|
||||||
|
)
|
||||||
|
},
|
||||||
|
groupAdmins: members
|
||||||
|
.filter { _, isAdmin in isAdmin }
|
||||||
|
.map { memberId, admin in
|
||||||
|
GroupMember(
|
||||||
|
groupId: groupId,
|
||||||
|
profileId: memberId,
|
||||||
|
role: .admin,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -111,7 +120,7 @@ internal extension SessionUtil {
|
||||||
user_groups_iterator_free(groupsIterator) // Need to free the iterator
|
user_groups_iterator_free(groupsIterator) // Need to free the iterator
|
||||||
|
|
||||||
// If we don't have any conversations then no need to continue
|
// If we don't have any conversations then no need to continue
|
||||||
guard !communities.isEmpty || !legacyGroups.isEmpty || !groups.isEmpty else { return }
|
guard !communities.isEmpty || !legacyGroups.isEmpty else { return }
|
||||||
|
|
||||||
// Extract all community/legacyGroup/group thread priorities
|
// Extract all community/legacyGroup/group thread priorities
|
||||||
let existingThreadInfo: [String: PriorityVisibilityInfo] = (try? SessionThread
|
let existingThreadInfo: [String: PriorityVisibilityInfo] = (try? SessionThread
|
||||||
|
@ -187,7 +196,8 @@ internal extension SessionUtil {
|
||||||
guard
|
guard
|
||||||
let name: String = group.name,
|
let name: String = group.name,
|
||||||
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
||||||
let members: [GroupMember] = group.groupMembers
|
let members: [GroupMember] = group.groupMembers,
|
||||||
|
let updatedAdmins: [GroupMember] = group.groupAdmins
|
||||||
else { return }
|
else { return }
|
||||||
|
|
||||||
if !existingLegacyGroupIds.contains(group.id) {
|
if !existingLegacyGroupIds.contains(group.id) {
|
||||||
|
@ -201,13 +211,12 @@ internal extension SessionUtil {
|
||||||
secretKey: lastKeyPair.secretKey.bytes
|
secretKey: lastKeyPair.secretKey.bytes
|
||||||
),
|
),
|
||||||
members: members
|
members: members
|
||||||
.filter { $0.role == .standard }
|
.appending(contentsOf: updatedAdmins) // Admins should also have 'standard' member entries
|
||||||
.map { $0.profileId },
|
|
||||||
admins: members
|
|
||||||
.filter { $0.role == .admin }
|
|
||||||
.map { $0.profileId },
|
.map { $0.profileId },
|
||||||
|
admins: updatedAdmins.map { $0.profileId },
|
||||||
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
||||||
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000)
|
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000),
|
||||||
|
calledFromConfigHandling: true
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -223,11 +232,7 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
// Add the lastKey if it doesn't already exist
|
// Add the lastKey if it doesn't already exist
|
||||||
let keyPairExists: Bool = ClosedGroupKeyPair
|
let keyPairExists: Bool = ClosedGroupKeyPair
|
||||||
.filter(
|
.filter(ClosedGroupKeyPair.Columns.threadKeyPairHash == lastKeyPair.threadKeyPairHash)
|
||||||
ClosedGroupKeyPair.Columns.threadId == lastKeyPair.threadId &&
|
|
||||||
ClosedGroupKeyPair.Columns.publicKey == lastKeyPair.publicKey &&
|
|
||||||
ClosedGroupKeyPair.Columns.secretKey == lastKeyPair.secretKey
|
|
||||||
)
|
|
||||||
.isNotEmpty(db)
|
.isNotEmpty(db)
|
||||||
|
|
||||||
if !keyPairExists {
|
if !keyPairExists {
|
||||||
|
@ -245,27 +250,70 @@ internal extension SessionUtil {
|
||||||
.saved(db)
|
.saved(db)
|
||||||
|
|
||||||
// Update the members
|
// Update the members
|
||||||
// TODO: This
|
let updatedMembers: [GroupMember] = members
|
||||||
// TODO: Going to need to decide whether we want to update the 'GroupMember' records in the database based on this config message changing
|
.appending(
|
||||||
// let members: [String]
|
contentsOf: updatedAdmins.map { admin in
|
||||||
// let admins: [String]
|
GroupMember(
|
||||||
}
|
groupId: admin.groupId,
|
||||||
|
profileId: admin.profileId,
|
||||||
// Make any thread-specific changes
|
role: .standard,
|
||||||
var threadChanges: [ConfigColumnAssignment] = []
|
isHidden: false
|
||||||
// Set the visibility if it's changed
|
|
||||||
if existingThreadInfo[group.id]?.shouldBeVisible != (group.hidden == false) {
|
|
||||||
threadChanges.append(
|
|
||||||
SessionThread.Columns.shouldBeVisible.set(to: (group.hidden == false))
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Set the priority if it's changed
|
if
|
||||||
if existingThreadInfo[group.id]?.pinnedPriority != group.priority {
|
let existingMembers: [GroupMember] = existingLegacyGroupMembers[group.id]?
|
||||||
threadChanges.append(
|
.filter({ $0.role == .standard || $0.role == .zombie }),
|
||||||
|
existingMembers != updatedMembers
|
||||||
|
{
|
||||||
|
// Add in any new members and remove any removed members
|
||||||
|
try updatedMembers.forEach { try $0.save(db) }
|
||||||
|
try existingMembers
|
||||||
|
.filter { !updatedMembers.contains($0) }
|
||||||
|
.forEach { member in
|
||||||
|
try GroupMember
|
||||||
|
.filter(
|
||||||
|
GroupMember.Columns.groupId == group.id &&
|
||||||
|
GroupMember.Columns.profileId == member.profileId && (
|
||||||
|
GroupMember.Columns.role == GroupMember.Role.standard ||
|
||||||
|
GroupMember.Columns.role == GroupMember.Role.zombie
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if
|
||||||
|
let existingAdmins: [GroupMember] = existingLegacyGroupMembers[group.id]?
|
||||||
|
.filter({ $0.role == .admin }),
|
||||||
|
existingAdmins != updatedAdmins
|
||||||
|
{
|
||||||
|
// Add in any new admins and remove any removed admins
|
||||||
|
try updatedAdmins.forEach { try $0.save(db) }
|
||||||
|
try existingAdmins
|
||||||
|
.filter { !updatedAdmins.contains($0) }
|
||||||
|
.forEach { member in
|
||||||
|
try GroupMember
|
||||||
|
.filter(
|
||||||
|
GroupMember.Columns.groupId == group.id &&
|
||||||
|
GroupMember.Columns.profileId == member.profileId &&
|
||||||
|
GroupMember.Columns.role == GroupMember.Role.admin
|
||||||
|
)
|
||||||
|
.deleteAll(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make any thread-specific changes if needed
|
||||||
|
let threadChanges: [ConfigColumnAssignment] = [
|
||||||
|
(existingThreadInfo[group.id]?.shouldBeVisible == (group.hidden == false) ? nil :
|
||||||
|
SessionThread.Columns.shouldBeVisible.set(to: (group.hidden == false))
|
||||||
|
),
|
||||||
|
(existingThreadInfo[group.id]?.pinnedPriority == group.priority ? nil :
|
||||||
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
||||||
)
|
)
|
||||||
}
|
].compactMap { $0 }
|
||||||
|
|
||||||
if !threadChanges.isEmpty {
|
if !threadChanges.isEmpty {
|
||||||
_ = try? SessionThread
|
_ = try? SessionThread
|
||||||
|
@ -321,6 +369,7 @@ internal extension SessionUtil {
|
||||||
if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair {
|
if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair {
|
||||||
userGroup.pointee.enc_pubkey = lastKeyPair.publicKey.toLibSession()
|
userGroup.pointee.enc_pubkey = lastKeyPair.publicKey.toLibSession()
|
||||||
userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession()
|
userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession()
|
||||||
|
userGroup.pointee.have_enc_keys = true
|
||||||
|
|
||||||
// Store the updated group (needs to happen before variables go out of scope)
|
// Store the updated group (needs to happen before variables go out of scope)
|
||||||
user_groups_set_legacy_group(conf, userGroup)
|
user_groups_set_legacy_group(conf, userGroup)
|
||||||
|
@ -328,7 +377,6 @@ internal extension SessionUtil {
|
||||||
|
|
||||||
// Assign all properties to match the updated disappearing messages config (if there is one)
|
// Assign all properties to match the updated disappearing messages config (if there is one)
|
||||||
if let updatedConfig: DisappearingMessagesConfiguration = legacyGroup.disappearingConfig {
|
if let updatedConfig: DisappearingMessagesConfiguration = legacyGroup.disappearingConfig {
|
||||||
// TODO: double check the 'isEnabled' flag
|
|
||||||
userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 :
|
userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 :
|
||||||
Int64(floor(updatedConfig.durationSeconds))
|
Int64(floor(updatedConfig.durationSeconds))
|
||||||
)
|
)
|
||||||
|
@ -393,6 +441,9 @@ public extension SessionUtil {
|
||||||
rootToken: String,
|
rootToken: String,
|
||||||
publicKey: String
|
publicKey: String
|
||||||
) throws {
|
) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -415,6 +466,9 @@ public extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func remove(_ db: Database, server: String, roomToken: String) throws {
|
static func remove(_ db: Database, server: String, roomToken: String) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -437,9 +491,13 @@ public extension SessionUtil {
|
||||||
latestKeyPairPublicKey: Data,
|
latestKeyPairPublicKey: Data,
|
||||||
latestKeyPairSecretKey: Data,
|
latestKeyPairSecretKey: Data,
|
||||||
latestKeyPairReceivedTimestamp: TimeInterval,
|
latestKeyPairReceivedTimestamp: TimeInterval,
|
||||||
|
disappearingConfig: DisappearingMessagesConfiguration,
|
||||||
members: Set<String>,
|
members: Set<String>,
|
||||||
admins: Set<String>
|
admins: Set<String>
|
||||||
) throws {
|
) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -456,6 +514,7 @@ public extension SessionUtil {
|
||||||
secretKey: latestKeyPairSecretKey,
|
secretKey: latestKeyPairSecretKey,
|
||||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||||
),
|
),
|
||||||
|
disappearingConfig: disappearingConfig,
|
||||||
groupMembers: members
|
groupMembers: members
|
||||||
.map { memberId in
|
.map { memberId in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
|
@ -486,9 +545,13 @@ public extension SessionUtil {
|
||||||
groupPublicKey: String,
|
groupPublicKey: String,
|
||||||
name: String? = nil,
|
name: String? = nil,
|
||||||
latestKeyPair: ClosedGroupKeyPair? = nil,
|
latestKeyPair: ClosedGroupKeyPair? = nil,
|
||||||
|
disappearingConfig: DisappearingMessagesConfiguration? = nil,
|
||||||
members: Set<String>? = nil,
|
members: Set<String>? = nil,
|
||||||
admins: Set<String>? = nil
|
admins: Set<String>? = nil
|
||||||
) throws {
|
) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -500,6 +563,7 @@ public extension SessionUtil {
|
||||||
id: groupPublicKey,
|
id: groupPublicKey,
|
||||||
name: name,
|
name: name,
|
||||||
lastKeyPair: latestKeyPair,
|
lastKeyPair: latestKeyPair,
|
||||||
|
disappearingConfig: disappearingConfig,
|
||||||
groupMembers: members?
|
groupMembers: members?
|
||||||
.map { memberId in
|
.map { memberId in
|
||||||
GroupMember(
|
GroupMember(
|
||||||
|
@ -526,6 +590,8 @@ public extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func hide(_ db: Database, legacyGroupIds: [String]) throws {
|
static func hide(_ db: Database, legacyGroupIds: [String]) throws {
|
||||||
|
guard !legacyGroupIds.isEmpty else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -544,6 +610,10 @@ public extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func remove(_ db: Database, legacyGroupIds: [String]) throws {
|
static func remove(_ db: Database, legacyGroupIds: [String]) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
guard !legacyGroupIds.isEmpty else { return }
|
||||||
|
|
||||||
try SessionUtil.performAndPushChange(
|
try SessionUtil.performAndPushChange(
|
||||||
db,
|
db,
|
||||||
for: .userGroups,
|
for: .userGroups,
|
||||||
|
@ -561,9 +631,13 @@ public extension SessionUtil {
|
||||||
// MARK: -- Group Changes
|
// MARK: -- Group Changes
|
||||||
|
|
||||||
static func hide(_ db: Database, groupIds: [String]) throws {
|
static func hide(_ db: Database, groupIds: [String]) throws {
|
||||||
|
guard !groupIds.isEmpty else { return }
|
||||||
}
|
}
|
||||||
|
|
||||||
static func remove(_ db: Database, groupIds: [String]) throws {
|
static func remove(_ db: Database, groupIds: [String]) throws {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
guard !groupIds.isEmpty else { return }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
|
@ -44,7 +44,7 @@ internal extension SessionUtil {
|
||||||
return .updateTo(
|
return .updateTo(
|
||||||
url: profilePictureUrl,
|
url: profilePictureUrl,
|
||||||
key: Data(
|
key: Data(
|
||||||
libSessionVal: profilePic.url,
|
libSessionVal: profilePic.key,
|
||||||
count: ProfileManager.avatarAES256KeyByteLength
|
count: ProfileManager.avatarAES256KeyByteLength
|
||||||
),
|
),
|
||||||
fileName: nil
|
fileName: nil
|
||||||
|
@ -54,6 +54,52 @@ internal extension SessionUtil {
|
||||||
calledFromConfigHandling: true
|
calledFromConfigHandling: true
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Update the 'Note to Self' visibility and priority
|
||||||
|
let threadInfo: PriorityVisibilityInfo? = try? SessionThread
|
||||||
|
.filter(id: userPublicKey)
|
||||||
|
.select(.id, .variant, .pinnedPriority, .shouldBeVisible)
|
||||||
|
.asRequest(of: PriorityVisibilityInfo.self)
|
||||||
|
.fetchOne(db)
|
||||||
|
let targetPriority: Int32 = user_profile_get_nts_priority(conf)
|
||||||
|
let targetHiddenState: Bool = user_profile_get_nts_hidden(conf)
|
||||||
|
|
||||||
|
// Create the 'Note to Self' thread if it doesn't exist
|
||||||
|
if let threadInfo: PriorityVisibilityInfo = threadInfo {
|
||||||
|
let threadChanges: [ConfigColumnAssignment] = [
|
||||||
|
(threadInfo.shouldBeVisible == (targetHiddenState == false) ? nil :
|
||||||
|
SessionThread.Columns.shouldBeVisible.set(to: (targetHiddenState == false))
|
||||||
|
),
|
||||||
|
(threadInfo.pinnedPriority == targetPriority ? nil :
|
||||||
|
SessionThread.Columns.pinnedPriority.set(to: targetPriority)
|
||||||
|
)
|
||||||
|
].compactMap { $0 }
|
||||||
|
|
||||||
|
if !threadChanges.isEmpty {
|
||||||
|
try SessionThread
|
||||||
|
.filter(id: userPublicKey)
|
||||||
|
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||||
|
db,
|
||||||
|
threadChanges
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
try SessionThread
|
||||||
|
.fetchOrCreate(
|
||||||
|
db,
|
||||||
|
id: userPublicKey,
|
||||||
|
variant: .contact,
|
||||||
|
shouldBeVisible: (targetHiddenState == false)
|
||||||
|
)
|
||||||
|
|
||||||
|
try SessionThread
|
||||||
|
.filter(id: userPublicKey)
|
||||||
|
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||||
|
db,
|
||||||
|
SessionThread.Columns.pinnedPriority.set(to: targetPriority)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Create a contact for the current user if needed (also force-approve the current user
|
// Create a contact for the current user if needed (also force-approve the current user
|
||||||
// in case the account got into a weird state or restored directly from a migration)
|
// in case the account got into a weird state or restored directly from a migration)
|
||||||
let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey)
|
let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey)
|
||||||
|
@ -91,14 +137,18 @@ internal extension SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
static func updateNoteToSelf(
|
static func updateNoteToSelf(
|
||||||
_ db: Database,
|
hidden: Bool? = nil,
|
||||||
priority: Int32,
|
priority: Int32? = nil,
|
||||||
hidden: Bool,
|
|
||||||
in conf: UnsafeMutablePointer<config_object>?
|
in conf: UnsafeMutablePointer<config_object>?
|
||||||
) throws {
|
) throws {
|
||||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||||
|
|
||||||
user_profile_set_nts_priority(conf, priority)
|
if let hidden: Bool = hidden {
|
||||||
user_profile_set_nts_hidden(conf, hidden)
|
user_profile_set_nts_hidden(conf, hidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let priority: Int32 = priority {
|
||||||
|
user_profile_set_nts_priority(conf, priority)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,10 @@ extension ColumnExpression {
|
||||||
|
|
||||||
// MARK: - QueryInterfaceRequest
|
// MARK: - QueryInterfaceRequest
|
||||||
|
|
||||||
public extension QueryInterfaceRequest {
|
public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & TableRecord {
|
||||||
|
|
||||||
|
// MARK: -- updateAll
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func updateAll(
|
func updateAll(
|
||||||
_ db: Database,
|
_ db: Database,
|
||||||
|
@ -61,30 +64,16 @@ public extension QueryInterfaceRequest {
|
||||||
) throws -> Int {
|
) throws -> Int {
|
||||||
let targetAssignments: [ColumnAssignment] = assignments.map { $0.assignment }
|
let targetAssignments: [ColumnAssignment] = assignments.map { $0.assignment }
|
||||||
|
|
||||||
// Before we do anything make sure the changes actually do need to be sunced
|
// Before we do anything custom make sure the changes actually do need to be synced
|
||||||
guard SessionUtil.assignmentsRequireConfigUpdate(assignments) else {
|
guard SessionUtil.assignmentsRequireConfigUpdate(assignments) else {
|
||||||
return try self.updateAll(db, targetAssignments)
|
return try self.updateAll(db, targetAssignments)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch self {
|
return try self.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
||||||
case let contactRequest as QueryInterfaceRequest<Contact>:
|
|
||||||
return try contactRequest.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
|
||||||
|
|
||||||
case let profileRequest as QueryInterfaceRequest<Profile>:
|
|
||||||
return try profileRequest.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
|
||||||
|
|
||||||
case let threadRequest as QueryInterfaceRequest<SessionThread>:
|
|
||||||
return try threadRequest.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
|
||||||
|
|
||||||
case let threadRequest as QueryInterfaceRequest<ClosedGroup>:
|
|
||||||
return try threadRequest.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
|
||||||
|
|
||||||
default: return try self.updateAll(db, targetAssignments)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & TableRecord {
|
// MARK: -- updateAndFetchAll
|
||||||
|
|
||||||
@discardableResult
|
@discardableResult
|
||||||
func updateAndFetchAllAndUpdateConfig(
|
func updateAndFetchAllAndUpdateConfig(
|
||||||
_ db: Database,
|
_ db: Database,
|
||||||
|
@ -120,9 +109,6 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the config dump state where needed
|
// Update the config dump state where needed
|
||||||
|
|
||||||
try SessionUtil.updateThreadPrioritiesIfNeeded(db, assignments, updatedData)
|
|
||||||
|
|
||||||
switch self {
|
switch self {
|
||||||
case is QueryInterfaceRequest<Contact>:
|
case is QueryInterfaceRequest<Contact>:
|
||||||
return try SessionUtil.updatingContacts(db, updatedData)
|
return try SessionUtil.updatingContacts(db, updatedData)
|
||||||
|
@ -130,9 +116,6 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
||||||
case is QueryInterfaceRequest<Profile>:
|
case is QueryInterfaceRequest<Profile>:
|
||||||
return try SessionUtil.updatingProfiles(db, updatedData)
|
return try SessionUtil.updatingProfiles(db, updatedData)
|
||||||
|
|
||||||
case is QueryInterfaceRequest<ClosedGroup>:
|
|
||||||
return updatedData
|
|
||||||
|
|
||||||
case is QueryInterfaceRequest<SessionThread>:
|
case is QueryInterfaceRequest<SessionThread>:
|
||||||
return try SessionUtil.updatingThreads(db, updatedData)
|
return try SessionUtil.updatingThreads(db, updatedData)
|
||||||
|
|
||||||
|
|
|
@ -49,6 +49,9 @@ public enum SessionUtil {
|
||||||
/// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been
|
/// Returns `true` if there is a config which needs to be pushed, but returns `false` if the configs are all up to date or haven't been
|
||||||
/// loaded yet (eg. fresh install)
|
/// loaded yet (eg. fresh install)
|
||||||
public static var needsSync: Bool {
|
public static var needsSync: Bool {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return false }
|
||||||
|
|
||||||
return configStore
|
return configStore
|
||||||
.wrappedValue
|
.wrappedValue
|
||||||
.contains { _, atomicConf in
|
.contains { _, atomicConf in
|
||||||
|
@ -63,15 +66,30 @@ public enum SessionUtil {
|
||||||
// MARK: - Loading
|
// MARK: - Loading
|
||||||
|
|
||||||
public static func loadState(
|
public static func loadState(
|
||||||
|
_ db: Database? = nil,
|
||||||
userPublicKey: String,
|
userPublicKey: String,
|
||||||
ed25519SecretKey: [UInt8]?
|
ed25519SecretKey: [UInt8]?
|
||||||
) {
|
) {
|
||||||
guard let secretKey: [UInt8] = ed25519SecretKey else { return }
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return }
|
||||||
|
|
||||||
|
// Ensure we have the ed25519 key and that we haven't already loaded the state before
|
||||||
|
// we continue
|
||||||
|
guard
|
||||||
|
let secretKey: [UInt8] = ed25519SecretKey,
|
||||||
|
SessionUtil.configStore.wrappedValue.isEmpty
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
// If we weren't given a database instance then get one
|
||||||
|
guard let db: Database = db else {
|
||||||
|
Storage.shared.read { db in
|
||||||
|
SessionUtil.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Retrieve the existing dumps from the database
|
// Retrieve the existing dumps from the database
|
||||||
let existingDumps: Set<ConfigDump> = Storage.shared
|
let existingDumps: Set<ConfigDump> = ((try? ConfigDump.fetchSet(db)) ?? [])
|
||||||
.read { db in try ConfigDump.fetchSet(db) }
|
|
||||||
.defaulting(to: [])
|
|
||||||
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
|
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
|
||||||
.map { $0.variant }
|
.map { $0.variant }
|
||||||
.asSet()
|
.asSet()
|
||||||
|
@ -103,7 +121,7 @@ public enum SessionUtil {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static func loadState(
|
private static func loadState(
|
||||||
for variant: ConfigDump.Variant,
|
for variant: ConfigDump.Variant,
|
||||||
secretKey ed25519SecretKey: [UInt8],
|
secretKey ed25519SecretKey: [UInt8],
|
||||||
cachedData: Data?
|
cachedData: Data?
|
||||||
|
@ -265,6 +283,9 @@ public enum SessionUtil {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func configHashes(for publicKey: String) -> [String] {
|
public static func configHashes(for publicKey: String) -> [String] {
|
||||||
|
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||||
|
guard Features.useSharedUtilForUserConfig else { return [] }
|
||||||
|
|
||||||
return Storage.shared
|
return Storage.shared
|
||||||
.read { db -> [String] in
|
.read { db -> [String] in
|
||||||
guard Identity.userExists(db) else { return [] }
|
guard Identity.userExists(db) else { return [] }
|
||||||
|
@ -342,6 +363,7 @@ public enum SessionUtil {
|
||||||
mergeData.forEach { $0?.deallocate() }
|
mergeData.forEach { $0?.deallocate() }
|
||||||
|
|
||||||
// Apply the updated states to the database
|
// Apply the updated states to the database
|
||||||
|
do {
|
||||||
switch next.key {
|
switch next.key {
|
||||||
case .userProfile:
|
case .userProfile:
|
||||||
try SessionUtil.handleUserProfileUpdate(
|
try SessionUtil.handleUserProfileUpdate(
|
||||||
|
@ -373,6 +395,11 @@ public enum SessionUtil {
|
||||||
latestConfigUpdateSentTimestamp: messageSentTimestamp
|
latestConfigUpdateSentTimestamp: messageSentTimestamp
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
SNLog("[libSession] Failed to process merge of \(next.key) config data")
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
// Need to check if the config needs to be dumped (this might have changed
|
// Need to check if the config needs to be dumped (this might have changed
|
||||||
// after handling the merge changes)
|
// after handling the merge changes)
|
||||||
|
|
|
@ -4,18 +4,6 @@
|
||||||
<dict>
|
<dict>
|
||||||
<key>AvailableLibraries</key>
|
<key>AvailableLibraries</key>
|
||||||
<array>
|
<array>
|
||||||
<dict>
|
|
||||||
<key>LibraryIdentifier</key>
|
|
||||||
<string>ios-arm64</string>
|
|
||||||
<key>LibraryPath</key>
|
|
||||||
<string>libsession-util.a</string>
|
|
||||||
<key>SupportedArchitectures</key>
|
|
||||||
<array>
|
|
||||||
<string>arm64</string>
|
|
||||||
</array>
|
|
||||||
<key>SupportedPlatform</key>
|
|
||||||
<string>ios</string>
|
|
||||||
</dict>
|
|
||||||
<dict>
|
<dict>
|
||||||
<key>LibraryIdentifier</key>
|
<key>LibraryIdentifier</key>
|
||||||
<string>ios-arm64_x86_64-simulator</string>
|
<string>ios-arm64_x86_64-simulator</string>
|
||||||
|
@ -31,6 +19,18 @@
|
||||||
<key>SupportedPlatformVariant</key>
|
<key>SupportedPlatformVariant</key>
|
||||||
<string>simulator</string>
|
<string>simulator</string>
|
||||||
</dict>
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>LibraryIdentifier</key>
|
||||||
|
<string>ios-arm64</string>
|
||||||
|
<key>LibraryPath</key>
|
||||||
|
<string>libsession-util.a</string>
|
||||||
|
<key>SupportedArchitectures</key>
|
||||||
|
<array>
|
||||||
|
<string>arm64</string>
|
||||||
|
</array>
|
||||||
|
<key>SupportedPlatform</key>
|
||||||
|
<string>ios</string>
|
||||||
|
</dict>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>XFWK</string>
|
<string>XFWK</string>
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -488,43 +488,43 @@ public final class OpenGroupManager {
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
try roomDetails.admins.forEach { adminId in
|
try roomDetails.admins.forEach { adminId in
|
||||||
_ = try GroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: adminId,
|
profileId: adminId,
|
||||||
role: .admin,
|
role: .admin,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).saved(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
try roomDetails.hiddenAdmins
|
try roomDetails.hiddenAdmins
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
.forEach { adminId in
|
.forEach { adminId in
|
||||||
_ = try GroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: adminId,
|
profileId: adminId,
|
||||||
role: .admin,
|
role: .admin,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
).saved(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
try roomDetails.moderators.forEach { moderatorId in
|
try roomDetails.moderators.forEach { moderatorId in
|
||||||
_ = try GroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: moderatorId,
|
profileId: moderatorId,
|
||||||
role: .moderator,
|
role: .moderator,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).saved(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
try roomDetails.hiddenModerators
|
try roomDetails.hiddenModerators
|
||||||
.defaulting(to: [])
|
.defaulting(to: [])
|
||||||
.forEach { moderatorId in
|
.forEach { moderatorId in
|
||||||
_ = try GroupMember(
|
try GroupMember(
|
||||||
groupId: threadId,
|
groupId: threadId,
|
||||||
profileId: moderatorId,
|
profileId: moderatorId,
|
||||||
role: .moderator,
|
role: .moderator,
|
||||||
isHidden: true
|
isHidden: true
|
||||||
).saved(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -8,16 +8,56 @@ import SessionUtilitiesKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
|
|
||||||
extension MessageReceiver {
|
extension MessageReceiver {
|
||||||
public static func handleClosedGroupControlMessage(_ db: Database, _ message: ClosedGroupControlMessage) throws {
|
public static func handleClosedGroupControlMessage(
|
||||||
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
message: ClosedGroupControlMessage
|
||||||
|
) throws {
|
||||||
switch message.kind {
|
switch message.kind {
|
||||||
case .new: try handleNewClosedGroup(db, message: message)
|
case .new: try handleNewClosedGroup(db, message: message)
|
||||||
case .encryptionKeyPair: try handleClosedGroupEncryptionKeyPair(db, message: message)
|
|
||||||
case .nameChange: try handleClosedGroupNameChanged(db, message: message)
|
case .encryptionKeyPair:
|
||||||
case .membersAdded: try handleClosedGroupMembersAdded(db, message: message)
|
try handleClosedGroupEncryptionKeyPair(
|
||||||
case .membersRemoved: try handleClosedGroupMembersRemoved(db, message: message)
|
db,
|
||||||
case .memberLeft: try handleClosedGroupMemberLeft(db, message: message)
|
threadId: threadId,
|
||||||
case .encryptionKeyPairRequest:
|
threadVariant: threadVariant,
|
||||||
handleClosedGroupEncryptionKeyPairRequest(db, message: message) // Currently not used
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
case .nameChange:
|
||||||
|
try handleClosedGroupNameChanged(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
case .membersAdded:
|
||||||
|
try handleClosedGroupMembersAdded(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
case .membersRemoved:
|
||||||
|
try handleClosedGroupMembersRemoved(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
case .memberLeft:
|
||||||
|
try handleClosedGroupMemberLeft(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
|
||||||
|
case .encryptionKeyPairRequest: break // Currently not used
|
||||||
|
|
||||||
default: throw MessageReceiverError.invalidMessage
|
default: throw MessageReceiverError.invalidMessage
|
||||||
}
|
}
|
||||||
|
@ -39,7 +79,8 @@ extension MessageReceiver {
|
||||||
members: membersAsData.map { $0.toHexString() },
|
members: membersAsData.map { $0.toHexString() },
|
||||||
admins: adminsAsData.map { $0.toHexString() },
|
admins: adminsAsData.map { $0.toHexString() },
|
||||||
expirationTimer: expirationTimer,
|
expirationTimer: expirationTimer,
|
||||||
messageSentTimestamp: sentTimestamp
|
messageSentTimestamp: sentTimestamp,
|
||||||
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +92,8 @@ extension MessageReceiver {
|
||||||
members: [String],
|
members: [String],
|
||||||
admins: [String],
|
admins: [String],
|
||||||
expirationTimer: UInt32,
|
expirationTimer: UInt32,
|
||||||
messageSentTimestamp: UInt64
|
messageSentTimestamp: UInt64,
|
||||||
|
calledFromConfigHandling: Bool
|
||||||
) throws {
|
) throws {
|
||||||
// With new closed groups we only want to create them if the admin creating the closed group is an
|
// With new closed groups we only want to create them if the admin creating the closed group is an
|
||||||
// approved contact (to prevent spam via closed groups getting around message requests if users are
|
// approved contact (to prevent spam via closed groups getting around message requests if users are
|
||||||
|
@ -65,13 +107,14 @@ extension MessageReceiver {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard hasApprovedAdmin else { return }
|
// If the group came from the updated config handling then it doesn't matter if we
|
||||||
|
// have an approved admin - we should add it regardless (as it's been synced from
|
||||||
|
// antoher device)
|
||||||
|
guard hasApprovedAdmin || calledFromConfigHandling else { return }
|
||||||
|
|
||||||
// Create the group
|
// Create the group
|
||||||
let thread: SessionThread = try SessionThread
|
let thread: SessionThread = try SessionThread
|
||||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
|
||||||
.with(shouldBeVisible: true)
|
|
||||||
.saved(db)
|
|
||||||
let closedGroup: ClosedGroup = try ClosedGroup(
|
let closedGroup: ClosedGroup = try ClosedGroup(
|
||||||
threadId: groupPublicKey,
|
threadId: groupPublicKey,
|
||||||
name: name,
|
name: name,
|
||||||
|
@ -103,7 +146,7 @@ extension MessageReceiver {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the DisappearingMessages config
|
// Update the DisappearingMessages config
|
||||||
try thread.disappearingMessagesConfiguration
|
let disappearingConfig: DisappearingMessagesConfiguration = try thread.disappearingMessagesConfiguration
|
||||||
.fetchOne(db)
|
.fetchOne(db)
|
||||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
||||||
.with(
|
.with(
|
||||||
|
@ -113,15 +156,38 @@ extension MessageReceiver {
|
||||||
(24 * 60 * 60)
|
(24 * 60 * 60)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.save(db)
|
.saved(db)
|
||||||
|
|
||||||
// Store the key pair
|
// Store the key pair if it doesn't already exist
|
||||||
try ClosedGroupKeyPair(
|
let receivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||||
|
let newKeyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
|
||||||
threadId: groupPublicKey,
|
threadId: groupPublicKey,
|
||||||
publicKey: Data(encryptionKeyPair.publicKey),
|
publicKey: Data(encryptionKeyPair.publicKey),
|
||||||
secretKey: Data(encryptionKeyPair.secretKey),
|
secretKey: Data(encryptionKeyPair.secretKey),
|
||||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
receivedTimestamp: receivedTimestamp
|
||||||
).insert(db)
|
)
|
||||||
|
let keyPairExists: Bool = ClosedGroupKeyPair
|
||||||
|
.filter(ClosedGroupKeyPair.Columns.threadKeyPairHash == newKeyPair.threadKeyPairHash)
|
||||||
|
.isNotEmpty(db)
|
||||||
|
|
||||||
|
if !keyPairExists {
|
||||||
|
try newKeyPair.insert(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !calledFromConfigHandling {
|
||||||
|
// Update libSession
|
||||||
|
try? SessionUtil.add(
|
||||||
|
db,
|
||||||
|
groupPublicKey: groupPublicKey,
|
||||||
|
name: name,
|
||||||
|
latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey),
|
||||||
|
latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey),
|
||||||
|
latestKeyPairReceivedTimestamp: receivedTimestamp,
|
||||||
|
disappearingConfig: disappearingConfig,
|
||||||
|
members: members.asSet(),
|
||||||
|
admins: admins.asSet()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Start polling
|
// Start polling
|
||||||
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey)
|
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey)
|
||||||
|
@ -132,18 +198,24 @@ extension MessageReceiver {
|
||||||
|
|
||||||
/// Extracts and adds the new encryption key pair to our list of key pairs if there is one for our public key, AND the message was
|
/// Extracts and adds the new encryption key pair to our list of key pairs if there is one for our public key, AND the message was
|
||||||
/// sent by the group admin.
|
/// sent by the group admin.
|
||||||
private static func handleClosedGroupEncryptionKeyPair(_ db: Database, message: ClosedGroupControlMessage) throws {
|
private static func handleClosedGroupEncryptionKeyPair(
|
||||||
guard
|
_ db: Database,
|
||||||
case let .encryptionKeyPair(explicitGroupPublicKey, wrappers) = message.kind,
|
threadId: String,
|
||||||
let groupPublicKey: String = (explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey)
|
threadVariant: SessionThread.Variant,
|
||||||
else { return }
|
message: ClosedGroupControlMessage
|
||||||
|
) throws {
|
||||||
|
guard case let .encryptionKeyPair(explicitGroupPublicKey, wrappers) = message.kind else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let groupPublicKey: String = (explicitGroupPublicKey?.toHexString() ?? threadId)
|
||||||
|
|
||||||
guard let userKeyPair: Box.KeyPair = Identity.fetchUserKeyPair(db) else {
|
guard let userKeyPair: Box.KeyPair = Identity.fetchUserKeyPair(db) else {
|
||||||
return SNLog("Couldn't find user X25519 key pair.")
|
return SNLog("Couldn't find user X25519 key pair.")
|
||||||
}
|
}
|
||||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupPublicKey) else {
|
||||||
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
|
return SNLog("Ignoring closed group encryption key pair for nonexistent group.")
|
||||||
}
|
}
|
||||||
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { return }
|
|
||||||
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
guard let groupAdmins: [GroupMember] = try? closedGroup.admins.fetchAll(db) else { return }
|
||||||
guard let sender: String = message.sender, groupAdmins.contains(where: { $0.profileId == sender }) else {
|
guard let sender: String = message.sender, groupAdmins.contains(where: { $0.profileId == sender }) else {
|
||||||
return SNLog("Ignoring closed group encryption key pair from non-admin.")
|
return SNLog("Ignoring closed group encryption key pair from non-admin.")
|
||||||
|
@ -203,66 +275,93 @@ extension MessageReceiver {
|
||||||
SNLog("Received a new closed group encryption key pair.")
|
SNLog("Received a new closed group encryption key pair.")
|
||||||
}
|
}
|
||||||
|
|
||||||
private static func handleClosedGroupNameChanged(_ db: Database, message: ClosedGroupControlMessage) throws {
|
private static func handleClosedGroupNameChanged(
|
||||||
guard case let .nameChange(name) = message.kind else { return }
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
message: ClosedGroupControlMessage
|
||||||
|
) throws {
|
||||||
|
guard
|
||||||
|
let messageKind: ClosedGroupControlMessage.Kind = message.kind,
|
||||||
|
case let .nameChange(name) = message.kind
|
||||||
|
else { return }
|
||||||
|
|
||||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
try processIfValid(
|
||||||
_ = try ClosedGroup
|
db,
|
||||||
.filter(id: id)
|
threadId: threadId,
|
||||||
.updateAll(db, ClosedGroup.Columns.name.set(to: name))
|
threadVariant: threadVariant,
|
||||||
|
message: message,
|
||||||
// Notify the user if needed
|
messageKind: messageKind,
|
||||||
guard name != closedGroup.name else { return }
|
infoMessageVariant: .infoClosedGroupUpdated,
|
||||||
|
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||||
_ = try Interaction(
|
// Update libSession
|
||||||
serverHash: message.serverHash,
|
try? SessionUtil.update(
|
||||||
threadId: thread.id,
|
db,
|
||||||
authorId: sender,
|
groupPublicKey: threadId,
|
||||||
variant: .infoClosedGroupUpdated,
|
name: name
|
||||||
body: ClosedGroupControlMessage.Kind
|
|
||||||
.nameChange(name: name)
|
|
||||||
.infoMessage(db, sender: sender),
|
|
||||||
timestampMs: (
|
|
||||||
message.sentTimestamp.map { Int64($0) } ??
|
|
||||||
SnodeAPI.currentOffsetTimestampMs()
|
|
||||||
)
|
)
|
||||||
).inserted(db)
|
|
||||||
|
_ = try ClosedGroup
|
||||||
|
.filter(id: threadId)
|
||||||
|
.updateAll( // Explicit config update so no need to use 'updateAllAndConfig'
|
||||||
|
db,
|
||||||
|
ClosedGroup.Columns.name.set(to: name)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func handleClosedGroupMembersAdded(
|
||||||
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
message: ClosedGroupControlMessage
|
||||||
|
) throws {
|
||||||
|
guard
|
||||||
|
let messageKind: ClosedGroupControlMessage.Kind = message.kind,
|
||||||
|
case let .membersAdded(membersAsData) = message.kind
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
try processIfValid(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message,
|
||||||
|
messageKind: messageKind,
|
||||||
|
infoMessageVariant: .infoClosedGroupUpdated,
|
||||||
|
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||||
|
// Update the group
|
||||||
|
let addedMembers: [String] = membersAsData.map { $0.toHexString() }
|
||||||
|
let currentMemberIds: Set<String> = allMembers
|
||||||
|
.filter { $0.role == .standard }
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
|
||||||
// Update libSession
|
// Update libSession
|
||||||
try? SessionUtil.update(
|
try? SessionUtil.update(
|
||||||
db,
|
db,
|
||||||
groupPublicKey: id,
|
groupPublicKey: threadId,
|
||||||
name: name
|
members: allMembers
|
||||||
)
|
.filter { $0.role == .standard || $0.role == .zombie }
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func handleClosedGroupMembersAdded(_ db: Database, message: ClosedGroupControlMessage) throws {
|
|
||||||
guard case let .membersAdded(membersAsData) = message.kind else { return }
|
|
||||||
|
|
||||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
|
||||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the group
|
|
||||||
let addedMembers: [String] = membersAsData.map { $0.toHexString() }
|
|
||||||
let currentMemberIds: Set<String> = allGroupMembers
|
|
||||||
.filter { $0.role == .standard }
|
|
||||||
.map { $0.profileId }
|
.map { $0.profileId }
|
||||||
.asSet()
|
.asSet()
|
||||||
let members: Set<String> = currentMemberIds.union(addedMembers)
|
.union(addedMembers),
|
||||||
|
admins: allMembers
|
||||||
|
.filter { $0.role == .admin }
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
)
|
||||||
|
|
||||||
// Create records for any new members
|
// Create records for any new members
|
||||||
try addedMembers
|
try addedMembers
|
||||||
.filter { !currentMemberIds.contains($0) }
|
.filter { !currentMemberIds.contains($0) }
|
||||||
.forEach { memberId in
|
.forEach { memberId in
|
||||||
try GroupMember(
|
try GroupMember(
|
||||||
groupId: id,
|
groupId: threadId,
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
role: .standard,
|
role: .standard,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).insert(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send the latest encryption key pair to the added members if the current user is
|
// Send the latest encryption key pair to the added members if the current user is
|
||||||
|
@ -280,56 +379,20 @@ extension MessageReceiver {
|
||||||
// generated by the admin when they saw the member removed message.
|
// generated by the admin when they saw the member removed message.
|
||||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
|
|
||||||
if allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) {
|
if allMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) {
|
||||||
addedMembers.forEach { memberId in
|
addedMembers.forEach { memberId in
|
||||||
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id)
|
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: threadId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any 'zombie' versions of the added members (in case they were re-added)
|
// Remove any 'zombie' versions of the added members (in case they were re-added)
|
||||||
_ = try GroupMember
|
_ = try GroupMember
|
||||||
.filter(GroupMember.Columns.groupId == id)
|
.filter(GroupMember.Columns.groupId == threadId)
|
||||||
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
|
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
|
||||||
.filter(addedMembers.contains(GroupMember.Columns.profileId))
|
.filter(addedMembers.contains(GroupMember.Columns.profileId))
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
// Notify the user if needed
|
|
||||||
guard members != currentMemberIds else { return }
|
|
||||||
|
|
||||||
_ = try Interaction(
|
|
||||||
serverHash: message.serverHash,
|
|
||||||
threadId: thread.id,
|
|
||||||
authorId: sender,
|
|
||||||
variant: .infoClosedGroupUpdated,
|
|
||||||
body: ClosedGroupControlMessage.Kind
|
|
||||||
.membersAdded(
|
|
||||||
members: addedMembers
|
|
||||||
.asSet()
|
|
||||||
.subtracting(currentMemberIds)
|
|
||||||
.map { Data(hex: $0) }
|
|
||||||
)
|
|
||||||
.infoMessage(db, sender: sender),
|
|
||||||
timestampMs: (
|
|
||||||
message.sentTimestamp.map { Int64($0) } ??
|
|
||||||
SnodeAPI.currentOffsetTimestampMs()
|
|
||||||
)
|
|
||||||
).inserted(db)
|
|
||||||
|
|
||||||
// Update libSession
|
|
||||||
try? SessionUtil.update(
|
|
||||||
db,
|
|
||||||
groupPublicKey: id,
|
|
||||||
members: allGroupMembers
|
|
||||||
.filter { $0.role == .standard || $0.role == .zombie }
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet()
|
|
||||||
.union(addedMembers),
|
|
||||||
admins: allGroupMembers
|
|
||||||
.filter { $0.role == .admin }
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Removes the given members from the group IF
|
/// Removes the given members from the group IF
|
||||||
|
@ -337,33 +400,68 @@ extension MessageReceiver {
|
||||||
/// • the admin sent the message (only the admin can truly remove members).
|
/// • the admin sent the message (only the admin can truly remove members).
|
||||||
/// If we're among the users that were removed, delete all encryption key pairs and the group public key, unsubscribe
|
/// If we're among the users that were removed, delete all encryption key pairs and the group public key, unsubscribe
|
||||||
/// from push notifications for this closed group, and remove the given members from the zombie list for this group.
|
/// from push notifications for this closed group, and remove the given members from the zombie list for this group.
|
||||||
private static func handleClosedGroupMembersRemoved(_ db: Database, message: ClosedGroupControlMessage) throws {
|
private static func handleClosedGroupMembersRemoved(
|
||||||
guard case let .membersRemoved(membersAsData) = message.kind else { return }
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
message: ClosedGroupControlMessage
|
||||||
|
) throws {
|
||||||
|
guard
|
||||||
|
let messageKind: ClosedGroupControlMessage.Kind = message.kind,
|
||||||
|
case let .membersRemoved(membersAsData) = messageKind
|
||||||
|
else { return }
|
||||||
|
|
||||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
// Check that the admin wasn't removed
|
let removedMemberIds: [String] = membersAsData.map { $0.toHexString() }
|
||||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
|
try processIfValid(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message,
|
||||||
|
messageKind: messageKind,
|
||||||
|
infoMessageVariant: (removedMemberIds.contains(userPublicKey) ?
|
||||||
|
.infoClosedGroupCurrentUserLeft :
|
||||||
|
.infoClosedGroupUpdated
|
||||||
|
),
|
||||||
|
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||||
let removedMembers = membersAsData.map { $0.toHexString() }
|
let removedMembers = membersAsData.map { $0.toHexString() }
|
||||||
let currentMemberIds: Set<String> = allGroupMembers
|
let currentMemberIds: Set<String> = allMembers
|
||||||
.filter { $0.role == .standard }
|
.filter { $0.role == .standard }
|
||||||
.map { $0.profileId }
|
.map { $0.profileId }
|
||||||
.asSet()
|
.asSet()
|
||||||
let members = currentMemberIds.subtracting(removedMembers)
|
let members = currentMemberIds.subtracting(removedMembers)
|
||||||
|
|
||||||
guard let firstAdminId: String = allGroupMembers.filter({ $0.role == .admin }).first?.profileId, members.contains(firstAdminId) else {
|
// Check that the group creator is still a member and that the message was
|
||||||
return SNLog("Ignoring invalid closed group update.")
|
// sent by a group admin
|
||||||
}
|
guard
|
||||||
// Check that the message was sent by the group admin
|
let firstAdminId: String = allMembers.filter({ $0.role == .admin })
|
||||||
guard allGroupMembers.filter({ $0.role == .admin }).contains(where: { $0.profileId == sender }) else {
|
.first?
|
||||||
return SNLog("Ignoring invalid closed group update.")
|
.profileId,
|
||||||
}
|
members.contains(firstAdminId),
|
||||||
|
allMembers
|
||||||
|
.filter({ $0.role == .admin })
|
||||||
|
.contains(where: { $0.profileId == sender })
|
||||||
|
else { return SNLog("Ignoring invalid closed group update.") }
|
||||||
|
|
||||||
|
// Update libSession
|
||||||
|
try? SessionUtil.update(
|
||||||
|
db,
|
||||||
|
groupPublicKey: threadId,
|
||||||
|
members: allMembers
|
||||||
|
.filter { $0.role == .standard || $0.role == .zombie }
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
.subtracting(removedMembers),
|
||||||
|
admins: allMembers
|
||||||
|
.filter { $0.role == .admin }
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
)
|
||||||
|
|
||||||
// Delete the removed members
|
// Delete the removed members
|
||||||
try GroupMember
|
try GroupMember
|
||||||
.filter(GroupMember.Columns.groupId == id)
|
.filter(GroupMember.Columns.groupId == threadId)
|
||||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
@ -372,11 +470,10 @@ extension MessageReceiver {
|
||||||
// • Stop polling for the group
|
// • Stop polling for the group
|
||||||
// • Remove the key pairs associated with the group
|
// • Remove the key pairs associated with the group
|
||||||
// • Notify the PN server
|
// • Notify the PN server
|
||||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
|
||||||
let wasCurrentUserRemoved: Bool = !members.contains(userPublicKey)
|
let wasCurrentUserRemoved: Bool = !members.contains(userPublicKey)
|
||||||
|
|
||||||
if wasCurrentUserRemoved {
|
if wasCurrentUserRemoved {
|
||||||
ClosedGroupPoller.shared.stopPolling(for: id)
|
ClosedGroupPoller.shared.stopPolling(for: threadId)
|
||||||
|
|
||||||
_ = try closedGroup
|
_ = try closedGroup
|
||||||
.keyPairs
|
.keyPairs
|
||||||
|
@ -384,67 +481,42 @@ extension MessageReceiver {
|
||||||
|
|
||||||
let _ = PushNotificationAPI.performOperation(
|
let _ = PushNotificationAPI.performOperation(
|
||||||
.unsubscribe,
|
.unsubscribe,
|
||||||
for: id,
|
for: threadId,
|
||||||
publicKey: userPublicKey
|
publicKey: userPublicKey
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user if needed
|
|
||||||
guard members != currentMemberIds else { return }
|
|
||||||
|
|
||||||
_ = try Interaction(
|
|
||||||
serverHash: message.serverHash,
|
|
||||||
threadId: thread.id,
|
|
||||||
authorId: sender,
|
|
||||||
variant: (wasCurrentUserRemoved ? .infoClosedGroupCurrentUserLeft : .infoClosedGroupUpdated),
|
|
||||||
body: ClosedGroupControlMessage.Kind
|
|
||||||
.membersRemoved(
|
|
||||||
members: removedMembers
|
|
||||||
.asSet()
|
|
||||||
.intersection(currentMemberIds)
|
|
||||||
.map { Data(hex: $0) }
|
|
||||||
)
|
|
||||||
.infoMessage(db, sender: sender),
|
|
||||||
timestampMs: (
|
|
||||||
message.sentTimestamp.map { Int64($0) } ??
|
|
||||||
SnodeAPI.currentOffsetTimestampMs()
|
|
||||||
)
|
|
||||||
).inserted(db)
|
|
||||||
|
|
||||||
// Update libSession
|
|
||||||
try? SessionUtil.update(
|
|
||||||
db,
|
|
||||||
groupPublicKey: id,
|
|
||||||
members: allGroupMembers
|
|
||||||
.filter { $0.role == .standard || $0.role == .zombie }
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet()
|
|
||||||
.subtracting(removedMembers),
|
|
||||||
admins: allGroupMembers
|
|
||||||
.filter { $0.role == .admin }
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If a regular member left:
|
/// If a regular member left:
|
||||||
/// • Mark them as a zombie (to be removed by the admin later).
|
/// • Mark them as a zombie (to be removed by the admin later).
|
||||||
/// If the admin left:
|
/// If the admin left:
|
||||||
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
|
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
|
||||||
private static func handleClosedGroupMemberLeft(_ db: Database, message: ClosedGroupControlMessage) throws {
|
private static func handleClosedGroupMemberLeft(
|
||||||
guard case .memberLeft = message.kind else { return }
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
threadVariant: SessionThread.Variant,
|
||||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
message: ClosedGroupControlMessage
|
||||||
return
|
) throws {
|
||||||
}
|
guard
|
||||||
|
let messageKind: ClosedGroupControlMessage.Kind = message.kind,
|
||||||
|
case .memberLeft = messageKind
|
||||||
|
else { return }
|
||||||
|
|
||||||
|
try processIfValid(
|
||||||
|
db,
|
||||||
|
threadId: threadId,
|
||||||
|
threadVariant: threadVariant,
|
||||||
|
message: message,
|
||||||
|
messageKind: messageKind,
|
||||||
|
infoMessageVariant: .infoClosedGroupUpdated,
|
||||||
|
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||||
let didAdminLeave: Bool = allGroupMembers.contains(where: { member in
|
let didAdminLeave: Bool = allMembers.contains(where: { member in
|
||||||
member.role == .admin && member.profileId == sender
|
member.role == .admin && member.profileId == sender
|
||||||
})
|
})
|
||||||
let members: [GroupMember] = allGroupMembers.filter { $0.role == .standard }
|
let members: [GroupMember] = allMembers.filter { $0.role == .standard }
|
||||||
let membersToRemove: [GroupMember] = members
|
let membersToRemove: [GroupMember] = members
|
||||||
.filter { member in
|
.filter { member in
|
||||||
didAdminLeave || // If the admin leaves the group is disbanded
|
didAdminLeave || // If the admin leaves the group is disbanded
|
||||||
|
@ -455,24 +527,35 @@ extension MessageReceiver {
|
||||||
.asSet()
|
.asSet()
|
||||||
.subtracting(membersToRemove.map { $0.profileId })
|
.subtracting(membersToRemove.map { $0.profileId })
|
||||||
|
|
||||||
|
// Update libSession
|
||||||
|
try? SessionUtil.update(
|
||||||
|
db,
|
||||||
|
groupPublicKey: threadId,
|
||||||
|
members: allMembers
|
||||||
|
.filter {
|
||||||
|
($0.role == .standard || $0.role == .zombie) &&
|
||||||
|
!membersToRemove.contains($0)
|
||||||
|
}
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet(),
|
||||||
|
admins: allMembers
|
||||||
|
.filter { $0.role == .admin }
|
||||||
|
.map { $0.profileId }
|
||||||
|
.asSet()
|
||||||
|
)
|
||||||
|
|
||||||
// Delete the members to remove
|
// Delete the members to remove
|
||||||
try GroupMember
|
try GroupMember
|
||||||
.filter(GroupMember.Columns.groupId == id)
|
.filter(GroupMember.Columns.groupId == threadId)
|
||||||
.filter(updatedMemberIds.contains(GroupMember.Columns.profileId))
|
.filter(updatedMemberIds.contains(GroupMember.Columns.profileId))
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
if didAdminLeave || sender == userPublicKey {
|
if didAdminLeave || sender == userPublicKey {
|
||||||
// Remove the group from the database and unsubscribe from PNs
|
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||||
ClosedGroupPoller.shared.stopPolling(for: id)
|
db,
|
||||||
|
threadId: threadId,
|
||||||
_ = try closedGroup
|
removeGroupData: false,
|
||||||
.keyPairs
|
calledFromConfigHandling: false
|
||||||
.deleteAll(db)
|
|
||||||
|
|
||||||
let _ = PushNotificationAPI.performOperation(
|
|
||||||
.unsubscribe,
|
|
||||||
for: id,
|
|
||||||
publicKey: userPublicKey
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -480,91 +563,67 @@ extension MessageReceiver {
|
||||||
// group)
|
// group)
|
||||||
if !didAdminLeave {
|
if !didAdminLeave {
|
||||||
try GroupMember(
|
try GroupMember(
|
||||||
groupId: id,
|
groupId: threadId,
|
||||||
profileId: sender,
|
profileId: sender,
|
||||||
role: .zombie,
|
role: .zombie,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).insert(db)
|
).save(db)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify the user if needed
|
// MARK: - Convenience
|
||||||
guard updatedMemberIds != Set(members.map { $0.profileId }) else { return }
|
|
||||||
|
|
||||||
|
private static func processIfValid(
|
||||||
|
_ db: Database,
|
||||||
|
threadId: String,
|
||||||
|
threadVariant: SessionThread.Variant,
|
||||||
|
message: ClosedGroupControlMessage,
|
||||||
|
messageKind: ClosedGroupControlMessage.Kind,
|
||||||
|
infoMessageVariant: Interaction.Variant,
|
||||||
|
legacyGroupChanges: (String, ClosedGroup, [GroupMember]) throws -> ()
|
||||||
|
) throws {
|
||||||
|
guard let sender: String = message.sender else { return }
|
||||||
|
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: threadId) else {
|
||||||
|
return SNLog("Ignoring group update for nonexistent group.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Legacy groups used these control messages for making changes, new groups only use them
|
||||||
|
// for information purposes
|
||||||
|
switch threadVariant {
|
||||||
|
case .legacyGroup:
|
||||||
|
// Check that the message isn't from before the group was created
|
||||||
|
guard Double(message.sentTimestamp ?? 0) > closedGroup.formationTimestamp else {
|
||||||
|
return SNLog("Ignoring legacy group update from before thread was created.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If these values are missing then we probably won't be able to validly handle the message
|
||||||
|
guard
|
||||||
|
let allMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db),
|
||||||
|
allMembers.contains(where: { $0.profileId == sender })
|
||||||
|
else { return SNLog("Ignoring legacy group update from non-member.") }
|
||||||
|
|
||||||
|
try legacyGroupChanges(sender, closedGroup, allMembers)
|
||||||
|
|
||||||
|
case .group:
|
||||||
|
break
|
||||||
|
|
||||||
|
default: return // Ignore as invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the info message for this group control message
|
||||||
_ = try Interaction(
|
_ = try Interaction(
|
||||||
serverHash: message.serverHash,
|
serverHash: message.serverHash,
|
||||||
threadId: thread.id,
|
threadId: threadId,
|
||||||
authorId: sender,
|
authorId: sender,
|
||||||
variant: .infoClosedGroupUpdated,
|
variant: infoMessageVariant,
|
||||||
body: ClosedGroupControlMessage.Kind
|
body: messageKind
|
||||||
.memberLeft
|
|
||||||
.infoMessage(db, sender: sender),
|
.infoMessage(db, sender: sender),
|
||||||
timestampMs: (
|
timestampMs: (
|
||||||
message.sentTimestamp.map { Int64($0) } ??
|
message.sentTimestamp.map { Int64($0) } ??
|
||||||
SnodeAPI.currentOffsetTimestampMs()
|
SnodeAPI.currentOffsetTimestampMs()
|
||||||
)
|
)
|
||||||
).inserted(db)
|
).inserted(db)
|
||||||
|
|
||||||
// Update libSession
|
|
||||||
try? SessionUtil.update(
|
|
||||||
db,
|
|
||||||
groupPublicKey: id,
|
|
||||||
members: allGroupMembers
|
|
||||||
.filter {
|
|
||||||
($0.role == .standard || $0.role == .zombie) &&
|
|
||||||
!membersToRemove.contains($0)
|
|
||||||
}
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet(),
|
|
||||||
admins: allGroupMembers
|
|
||||||
.filter { $0.role == .admin }
|
|
||||||
.map { $0.profileId }
|
|
||||||
.asSet()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static func handleClosedGroupEncryptionKeyPairRequest(_ db: Database, message: ClosedGroupControlMessage) {
|
|
||||||
/*
|
|
||||||
guard case .encryptionKeyPairRequest = message.kind else { return }
|
|
||||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
|
||||||
guard let groupPublicKey = message.groupPublicKey else { return }
|
|
||||||
performIfValid(for: message, using: transaction) { groupID, _, group in
|
|
||||||
let publicKey = message.sender!
|
|
||||||
// Guard against self-sends
|
|
||||||
guard publicKey != getUserHexEncodedPublicKey() else {
|
|
||||||
return SNLog("Ignoring invalid closed group update.")
|
|
||||||
}
|
|
||||||
MessageSender.sendLatestEncryptionKeyPair(to: publicKey, for: groupPublicKey, using: transaction)
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Convenience
|
|
||||||
|
|
||||||
private static func performIfValid(
|
|
||||||
_ db: Database,
|
|
||||||
message: ClosedGroupControlMessage,
|
|
||||||
_ update: (String, String, SessionThread, ClosedGroup
|
|
||||||
) throws -> Void) throws {
|
|
||||||
guard let groupPublicKey: String = message.groupPublicKey else { return }
|
|
||||||
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: groupPublicKey) else {
|
|
||||||
return SNLog("Ignoring closed group update for nonexistent group.")
|
|
||||||
}
|
|
||||||
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(db) else { return }
|
|
||||||
|
|
||||||
// Check that the message isn't from before the group was created
|
|
||||||
guard Double(message.sentTimestamp ?? 0) > closedGroup.formationTimestamp else {
|
|
||||||
return SNLog("Ignoring closed group update from before thread was created.")
|
|
||||||
}
|
|
||||||
|
|
||||||
guard let sender: String = message.sender else { return }
|
|
||||||
guard let members: [GroupMember] = try? closedGroup.members.fetchAll(db) else { return }
|
|
||||||
|
|
||||||
// Check that the sender is a member of the group
|
|
||||||
guard members.contains(where: { $0.profileId == sender }) else {
|
|
||||||
return SNLog("Ignoring closed group update from non-member.")
|
|
||||||
}
|
|
||||||
|
|
||||||
try update(groupPublicKey, sender, thread, closedGroup)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -189,7 +189,8 @@ extension MessageReceiver {
|
||||||
members: [String](closedGroup.members),
|
members: [String](closedGroup.members),
|
||||||
admins: [String](closedGroup.admins),
|
admins: [String](closedGroup.admins),
|
||||||
expirationTimer: closedGroup.expirationTimer,
|
expirationTimer: closedGroup.expirationTimer,
|
||||||
messageSentTimestamp: message.sentTimestamp!
|
messageSentTimestamp: message.sentTimestamp!,
|
||||||
|
calledFromConfigHandling: false // Legacy config isn't an issue
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,6 +35,19 @@ extension MessageReceiver {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Legacy closed groups need to update the SessionUtil
|
||||||
|
switch threadVariant {
|
||||||
|
case .legacyGroup:
|
||||||
|
try SessionUtil
|
||||||
|
.update(
|
||||||
|
db,
|
||||||
|
groupPublicKey: threadId,
|
||||||
|
disappearingConfig: config
|
||||||
|
)
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
// Add an info message for the user
|
// Add an info message for the user
|
||||||
_ = try Interaction(
|
_ = try Interaction(
|
||||||
serverHash: nil, // Intentionally null so sync messages are seen as duplicates
|
serverHash: nil, // Intentionally null so sync messages are seen as duplicates
|
||||||
|
|
|
@ -65,7 +65,8 @@ extension MessageReceiver {
|
||||||
// Loop through all blinded threads and extract any interactions relating to the user accepting
|
// Loop through all blinded threads and extract any interactions relating to the user accepting
|
||||||
// the message request
|
// the message request
|
||||||
try pendingBlindedIdLookups.forEach { blindedIdLookup in
|
try pendingBlindedIdLookups.forEach { blindedIdLookup in
|
||||||
// If the sessionId matches the blindedId then this thread needs to be converted to an un-blinded thread
|
// If the sessionId matches the blindedId then this thread needs to be converted to an
|
||||||
|
// un-blinded thread
|
||||||
guard
|
guard
|
||||||
dependencies.sodium.sessionId(
|
dependencies.sodium.sessionId(
|
||||||
senderId,
|
senderId,
|
||||||
|
@ -111,6 +112,9 @@ extension MessageReceiver {
|
||||||
.filter(ids: blindedContactIds)
|
.filter(ids: blindedContactIds)
|
||||||
.deleteAll(db)
|
.deleteAll(db)
|
||||||
|
|
||||||
|
try? SessionUtil
|
||||||
|
.remove(db, contactIds: blindedContactIds)
|
||||||
|
|
||||||
try updateContactApprovalStatusIfNeeded(
|
try updateContactApprovalStatusIfNeeded(
|
||||||
db,
|
db,
|
||||||
senderSessionId: userPublicKey,
|
senderSessionId: userPublicKey,
|
||||||
|
|
|
@ -46,11 +46,22 @@ extension MessageReceiver {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get or create thread
|
switch threadVariant {
|
||||||
guard let threadInfo: (id: String, variant: SessionThread.Variant) = MessageReceiver.threadInfo(db, message: message, openGroupId: openGroupId) else {
|
case .contact: break // Always continue
|
||||||
|
|
||||||
|
case .community:
|
||||||
|
// Only process visible messages for communities if they have an existing thread
|
||||||
|
guard (try? SessionThread.exists(db, id: threadId)) == true else {
|
||||||
throw MessageReceiverError.noThread
|
throw MessageReceiverError.noThread
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case .legacyGroup, .group:
|
||||||
|
// Only process visible messages for groups if they have a ClosedGroup record
|
||||||
|
guard (try? ClosedGroup.exists(db, id: threadId)) == true else {
|
||||||
|
throw MessageReceiverError.noThread
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Store the message variant so we can run variant-specific behaviours
|
// Store the message variant so we can run variant-specific behaviours
|
||||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, dependencies: dependencies)
|
||||||
let thread: SessionThread = try SessionThread
|
let thread: SessionThread = try SessionThread
|
||||||
|
|
|
@ -60,7 +60,7 @@ extension MessageSender {
|
||||||
profileId: adminId,
|
profileId: adminId,
|
||||||
role: .admin,
|
role: .admin,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).insert(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
try members.forEach { memberId in
|
try members.forEach { memberId in
|
||||||
|
@ -69,7 +69,7 @@ extension MessageSender {
|
||||||
profileId: memberId,
|
profileId: memberId,
|
||||||
role: .standard,
|
role: .standard,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).insert(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update libSession
|
// Update libSession
|
||||||
|
@ -80,6 +80,7 @@ extension MessageSender {
|
||||||
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
||||||
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
||||||
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
||||||
|
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
|
||||||
members: members,
|
members: members,
|
||||||
admins: admins
|
admins: admins
|
||||||
)
|
)
|
||||||
|
@ -462,7 +463,7 @@ extension MessageSender {
|
||||||
profileId: member,
|
profileId: member,
|
||||||
role: .standard,
|
role: .standard,
|
||||||
isHidden: false
|
isHidden: false
|
||||||
).insert(db)
|
).save(db)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -629,12 +630,18 @@ extension MessageSender {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch {
|
catch {
|
||||||
|
switch error {
|
||||||
|
case MessageSenderError.noKeyPair, MessageSenderError.encryptionFailed:
|
||||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||||
db,
|
db,
|
||||||
threadId: groupPublicKey,
|
threadId: groupPublicKey,
|
||||||
removeGroupData: false,
|
removeGroupData: false,
|
||||||
calledFromConfigHandling: false
|
calledFromConfigHandling: false
|
||||||
)
|
)
|
||||||
|
|
||||||
|
default: break
|
||||||
|
}
|
||||||
|
|
||||||
return Fail(error: error)
|
return Fail(error: error)
|
||||||
.eraseToAnyPublisher()
|
.eraseToAnyPublisher()
|
||||||
}
|
}
|
||||||
|
|
|
@ -281,6 +281,17 @@ public enum MessageReceiver {
|
||||||
case is TypingIndicator: break
|
case is TypingIndicator: break
|
||||||
|
|
||||||
default:
|
default:
|
||||||
|
// Only update the `shouldBeVisible` flag if the thread is currently not visible
|
||||||
|
// as we don't want to trigger a config update if not needed
|
||||||
|
let isCurrentlyVisible: Bool = try SessionThread
|
||||||
|
.filter(id: threadId)
|
||||||
|
.select(.shouldBeVisible)
|
||||||
|
.asRequest(of: Bool.self)
|
||||||
|
.fetchOne(db)
|
||||||
|
.defaulting(to: false)
|
||||||
|
|
||||||
|
guard !isCurrentlyVisible else { return }
|
||||||
|
|
||||||
try SessionThread
|
try SessionThread
|
||||||
.filter(id: threadId)
|
.filter(id: threadId)
|
||||||
.updateAllAndConfig(
|
.updateAllAndConfig(
|
||||||
|
|
|
@ -558,23 +558,25 @@ public struct ProfileManager {
|
||||||
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil))
|
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil))
|
||||||
|
|
||||||
case .updateTo(let url, let key, let fileName):
|
case .updateTo(let url, let key, let fileName):
|
||||||
if
|
if url != profile.profilePictureUrl {
|
||||||
(
|
|
||||||
url != profile.profilePictureUrl ||
|
|
||||||
key != profile.profileEncryptionKey
|
|
||||||
) &&
|
|
||||||
key.count == ProfileManager.avatarAES256KeyByteLength &&
|
|
||||||
key != profile.profileEncryptionKey
|
|
||||||
{
|
|
||||||
profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url))
|
profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url))
|
||||||
profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key))
|
|
||||||
avatarNeedsDownload = true
|
avatarNeedsDownload = true
|
||||||
targetAvatarUrl = url
|
targetAvatarUrl = url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if key != profile.profileEncryptionKey && key.count == ProfileManager.avatarAES256KeyByteLength {
|
||||||
|
profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key))
|
||||||
|
}
|
||||||
|
|
||||||
// Profile filename (this isn't synchronized between devices)
|
// Profile filename (this isn't synchronized between devices)
|
||||||
if let fileName: String = fileName {
|
if let fileName: String = fileName {
|
||||||
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName))
|
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName))
|
||||||
|
|
||||||
|
// If we have already downloaded the image then no need to download it again
|
||||||
|
avatarNeedsDownload = (
|
||||||
|
avatarNeedsDownload &&
|
||||||
|
!ProfileManager.hasProfileImageData(with: fileName)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,17 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="18122" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21507" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
|
||||||
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
<device id="retina4_7" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="18093"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21505"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
<scenes>
|
<scenes>
|
||||||
<!--ShareVC-->
|
<!--Share Nav Controller-->
|
||||||
<scene sceneID="ceB-am-kn3">
|
<scene sceneID="ceB-am-kn3">
|
||||||
<objects>
|
<objects>
|
||||||
<viewController id="j1y-V4-xli" customClass="ShareVC" customModule="SessionShareExtension" customModuleProvider="target" sceneMemberID="viewController">
|
<viewController id="j1y-V4-xli" customClass="ShareNavController" customModule="SessionShareExtension" customModuleProvider="target" sceneMemberID="viewController">
|
||||||
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
|
|
@ -27,6 +27,10 @@ public extension Database {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func drop<T>(table: T.Type) throws where T: TableRecord {
|
||||||
|
try drop(table: T.databaseTableName)
|
||||||
|
}
|
||||||
|
|
||||||
func createIndex<T>(
|
func createIndex<T>(
|
||||||
withCustomName customName: String? = nil,
|
withCustomName customName: String? = nil,
|
||||||
on table: T.Type,
|
on table: T.Type,
|
||||||
|
|
Loading…
Reference in New Issue