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
|
||||
|
||||
//
|
||||
// ListLocalizableStrings.swift
|
||||
// Archa
|
||||
//
|
||||
// Created by Morgan Pretty on 18/5/20.
|
||||
// Copyright © 2020 Archa. All rights reserved.
|
||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||
//
|
||||
// This script is based on https://github.com/ginowu7/CleanSwiftLocalizableExample the main difference
|
||||
// is canges to the localized usage regex
|
||||
|
@ -56,7 +51,6 @@ var executableFiles: [String] = {
|
|||
/// - Parameter path: path of file
|
||||
/// - Returns: content in file
|
||||
func contents(atPath path: String) -> String {
|
||||
print("Path: \(path)")
|
||||
guard let data = fileManager.contents(atPath: path), let content = String(data: data, encoding: .utf8) else {
|
||||
fatalError("Could not read from path: \(path)")
|
||||
}
|
||||
|
@ -109,8 +103,6 @@ func localizedStringsInCode() -> [LocalizationCodeFile] {
|
|||
///
|
||||
/// - Parameter files: list of localizable files to validate
|
||||
func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
||||
print("------------ Validating keys match in all localizable files ------------")
|
||||
|
||||
guard let base = files.first, files.count > 1 else { return }
|
||||
|
||||
let files = Array(files.dropFirst())
|
||||
|
@ -128,8 +120,6 @@ func validateMatchKeys(_ files: [LocalizationStringsFile]) {
|
|||
/// - codeFiles: Array of LocalizationCodeFile
|
||||
/// - localizationFiles: Array of LocalizableStringFiles
|
||||
func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||||
print("------------ Checking for missing keys -----------")
|
||||
|
||||
guard let baseFile = localizationFiles.first else {
|
||||
fatalError("Could not locate base localization file")
|
||||
}
|
||||
|
@ -150,8 +140,6 @@ func validateMissingKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles:
|
|||
/// - codeFiles: Array of LocalizationCodeFile
|
||||
/// - localizationFiles: Array of LocalizableStringFiles
|
||||
func validateDeadKeys(_ codeFiles: [LocalizationCodeFile], localizationFiles: [LocalizationStringsFile]) {
|
||||
print("------------ Checking for any dead keys in localizable file -----------")
|
||||
|
||||
guard let baseFile = localizationFiles.first else {
|
||||
fatalError("Could not locate base localization file")
|
||||
}
|
||||
|
@ -174,14 +162,18 @@ protocol Pathable {
|
|||
struct LocalizationStringsFile: Pathable {
|
||||
let path: String
|
||||
let kv: [String: String]
|
||||
let duplicates: [(key: String, path: String)]
|
||||
|
||||
var keys: [String] {
|
||||
return Array(kv.keys)
|
||||
}
|
||||
|
||||
init(path: String) {
|
||||
let result = ContentParser.parse(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
|
||||
|
@ -204,9 +196,7 @@ struct ContentParser {
|
|||
///
|
||||
/// - Parameter path: Localizable file paths
|
||||
/// - Returns: localizable key and value for content at path
|
||||
static func parse(_ path: String) -> [String: String] {
|
||||
print("------------ Checking for duplicate keys: \(path) ------------")
|
||||
|
||||
static func parse(_ path: String) -> (kv: [String: String], duplicates: [(key: String, path: String)]) {
|
||||
let content = contents(atPath: path)
|
||||
let trimmed = content
|
||||
.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)")
|
||||
}
|
||||
|
||||
return zip(keys, values).reduce(into: [String: String]()) { results, keyValue in
|
||||
if results[keyValue.0] != nil {
|
||||
printPretty("error: Found duplicate key: \(keyValue.0) in file: \(path)")
|
||||
abort()
|
||||
var duplicates: [(key: String, path: String)] = []
|
||||
let kv: [String: String] = zip(keys, values)
|
||||
.reduce(into: [:]) { results, keyValue in
|
||||
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: ""))
|
||||
}
|
||||
|
||||
let stringFiles = create()
|
||||
// MARK: - Processing
|
||||
|
||||
let stringFiles: [LocalizationStringsFile] = create()
|
||||
|
||||
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)
|
||||
|
||||
// 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() }
|
||||
|
||||
let codeFiles = localizedStringsInCode()
|
||||
let codeFiles: [LocalizationCodeFile] = localizedStringsInCode()
|
||||
validateMissingKeys(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 */; };
|
||||
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 */; };
|
||||
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; };
|
||||
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9BA27CF20AF005E1583 /* SessionIdSpec.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>"; };
|
||||
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>"; };
|
||||
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; };
|
||||
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>"; };
|
||||
|
@ -3629,6 +3631,7 @@
|
|||
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
|
||||
FD432431299C6933008A0213 /* _011_AddPendingReadReceipts.swift */,
|
||||
FD8ECF7C2934293A00C0D1BB /* _012_SharedUtilChanges.swift */,
|
||||
FD778B6329B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift */,
|
||||
);
|
||||
path = Migrations;
|
||||
sourceTree = "<group>";
|
||||
|
@ -5791,6 +5794,7 @@
|
|||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
|
||||
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */,
|
||||
FD778B6429B189FF001BAC6B /* _013_GenerateInitialUserConfigDumps.swift in Sources */,
|
||||
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
|
||||
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.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
|
||||
self?.viewModel.sentMessageBeforeUpdate = true
|
||||
|
||||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
}
|
||||
|
||||
let authorId: String = {
|
||||
if let blindedId = self?.viewModel.threadData.currentUserBlindedPublicKey {
|
||||
|
@ -585,10 +587,12 @@ extension ConversationVC:
|
|||
// Let the viewModel know we are about to send a message
|
||||
self?.viewModel.sentMessageBeforeUpdate = true
|
||||
|
||||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
_ = try SessionThread
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
}
|
||||
|
||||
// Create the interaction
|
||||
let interaction: Interaction = try Interaction(
|
||||
|
@ -1301,7 +1305,7 @@ extension ConversationVC:
|
|||
.suffix(19))
|
||||
.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
|
||||
Storage.shared
|
||||
.writePublisherFlatMap(receiveOn: DispatchQueue.global(qos: .userInitiated)) { db -> AnyPublisher<MessageSender.PreparedSendData?, Error> in
|
||||
|
@ -1311,10 +1315,12 @@ extension ConversationVC:
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Update the thread to be visible
|
||||
_ = try SessionThread
|
||||
.filter(id: thread.id)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
// Update the thread to be visible (if it isn't already)
|
||||
if !thread.shouldBeVisible {
|
||||
_ = try SessionThread
|
||||
.filter(id: thread.id)
|
||||
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
|
||||
}
|
||||
|
||||
let pendingReaction: Reaction? = {
|
||||
if remove {
|
||||
|
|
|
@ -528,6 +528,25 @@ final class ConversationVC: BaseVC, ConversationSearchControllerDelegate, UITabl
|
|||
mediaCache.removeAllObjects()
|
||||
hasReloadedThreadDataAfterDisappearance = 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) {
|
||||
|
|
|
@ -30,6 +30,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
|
||||
private let dependencies: Dependencies
|
||||
private let threadId: String
|
||||
private let threadVariant: SessionThread.Variant
|
||||
private let config: DisappearingMessagesConfiguration
|
||||
private var storedSelection: TimeInterval
|
||||
private var currentSelection: CurrentValueSubject<TimeInterval, Never>
|
||||
|
@ -39,10 +40,12 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
init(
|
||||
dependencies: Dependencies = Dependencies(),
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
config: DisappearingMessagesConfiguration
|
||||
) {
|
||||
self.dependencies = dependencies
|
||||
self.threadId = threadId
|
||||
self.threadVariant = threadVariant
|
||||
self.config = config
|
||||
self.storedSelection = (config.isEnabled ? config.durationSeconds : 0)
|
||||
self.currentSelection = CurrentValueSubject(self.storedSelection)
|
||||
|
@ -134,6 +137,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
|
||||
private func saveChanges() {
|
||||
let threadId: String = self.threadId
|
||||
let threadVariant: SessionThread.Variant = self.threadVariant
|
||||
let currentSelection: TimeInterval = self.currentSelection.value
|
||||
let updatedConfig: DisappearingMessagesConfiguration = self.config
|
||||
.with(
|
||||
|
@ -175,6 +179,19 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
interactionId: interaction.id,
|
||||
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(
|
||||
viewModel: ThreadDisappearingMessagesSettingsViewModel(
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
config: disappearingMessagesConfig
|
||||
)
|
||||
)
|
||||
|
|
|
@ -731,7 +731,7 @@ final class HomeVC: BaseVC, UITableViewDataSource, UITableViewDelegate, SeedRemi
|
|||
cancelStyle: .alert_text,
|
||||
dismissOnConfirm: true,
|
||||
onConfirm: { [weak self] _ in
|
||||
self?.viewModel.delete(
|
||||
self?.viewModel.deleteOrLeave(
|
||||
threadId: threadViewModel.threadId,
|
||||
threadVariant: threadViewModel.threadVariant
|
||||
)
|
||||
|
|
|
@ -4,6 +4,7 @@ import Foundation
|
|||
import GRDB
|
||||
import DifferenceKit
|
||||
import SignalUtilitiesKit
|
||||
import SessionMessagingKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public class HomeViewModel {
|
||||
|
@ -365,10 +366,27 @@ public class HomeViewModel {
|
|||
threadViewModel.markAsUnread()
|
||||
}
|
||||
|
||||
public func delete(threadId: String, threadVariant: SessionThread.Variant) {
|
||||
public func deleteOrLeave(threadId: String, threadVariant: SessionThread.Variant) {
|
||||
Storage.shared.writeAsync { db in
|
||||
switch threadVariant {
|
||||
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
|
||||
.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
|
||||
// set the avatar directly
|
||||
guard
|
||||
let type: Any = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey])
|
||||
.allValues
|
||||
.first,
|
||||
let typeString: String = type as? String,
|
||||
let type: URLResourceValues = try? imageUrl.resourceValues(forKeys: [.typeIdentifierKey]),
|
||||
let typeString: String = type.typeIdentifier,
|
||||
MIMETypeUtil.supportedAnimatedImageUTITypes().contains(typeString)
|
||||
else {
|
||||
let viewController: CropScaleImageViewController = CropScaleImageViewController(
|
||||
|
|
|
@ -610,8 +610,6 @@ class SettingsViewModel: SessionTableViewModel<SettingsViewModel.NavButton, Sett
|
|||
DispatchQueue.main.async {
|
||||
button.isUserInteractionEnabled = false
|
||||
|
||||
|
||||
|
||||
UIView.transition(
|
||||
with: button,
|
||||
duration: 0.25,
|
||||
|
|
|
@ -259,33 +259,33 @@ enum MockDataGenerator {
|
|||
.saved(db)
|
||||
|
||||
members.forEach { memberId in
|
||||
_ = try! GroupMember(
|
||||
try! GroupMember(
|
||||
groupId: randomGroupPublicKey,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
)
|
||||
.saved(db)
|
||||
.save(db)
|
||||
}
|
||||
[members.randomElement(using: &cgThreadRandomGenerator) ?? userSessionId].forEach { adminId in
|
||||
_ = try! GroupMember(
|
||||
try! GroupMember(
|
||||
groupId: randomGroupPublicKey,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
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
|
||||
let encryptionKeyPair = Curve25519.generateKeyPair()
|
||||
_ = try! ClosedGroupKeyPair(
|
||||
try! ClosedGroupKeyPair(
|
||||
threadId: randomGroupPublicKey,
|
||||
publicKey: encryptionKeyPair.publicKey,
|
||||
secretKey: encryptionKeyPair.privateKey,
|
||||
receivedTimestamp: timestampNow
|
||||
)
|
||||
.saved(db)
|
||||
.save(db)
|
||||
|
||||
// Generate the message history (Note: Unapproved message requests will only include incoming 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
|
||||
[
|
||||
_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.
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import GRDB
|
||||
import SessionUtil
|
||||
import SessionUtilitiesKit
|
||||
|
||||
/// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation
|
||||
/// searh (currently it's much slower than the global search)
|
||||
/// This migration makes the neccessary changes to support the updated user config syncing system
|
||||
enum _012_SharedUtilChanges: Migration {
|
||||
static let target: TargetMigrations.Identifier = .messagingKit
|
||||
static let identifier: String = "SharedUtilChanges"
|
||||
|
@ -20,10 +20,123 @@ enum _012_SharedUtilChanges: Migration {
|
|||
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(
|
||||
on: ClosedGroupKeyPair.self,
|
||||
columns: [.threadId, .publicKey, .secretKey]
|
||||
columns: [.threadId, .threadKeyPairHash]
|
||||
)
|
||||
|
||||
// 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
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
guard let secretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey else {
|
||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
return
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// There was previously a bug which allowed users to fully delete the 'Note to Self'
|
||||
// conversation but we don't want that, so create it again if it doesn't exists
|
||||
try SessionThread
|
||||
.fetchOrCreate(db, id: userPublicKey, variant: .contact, shouldBeVisible: false)
|
||||
|
||||
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
import GRDB
|
||||
import SessionUtilitiesKit
|
||||
|
||||
|
@ -18,12 +19,14 @@ public struct ClosedGroupKeyPair: Codable, Equatable, FetchableRecord, Persistab
|
|||
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
|
||||
|
||||
// MARK: - Relationships
|
||||
|
||||
|
@ -43,6 +46,12 @@ public struct ClosedGroupKeyPair: Codable, Equatable, FetchableRecord, Persistab
|
|||
self.publicKey = publicKey
|
||||
self.secretKey = secretKey
|
||||
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 GRDB
|
||||
|
@ -27,7 +27,8 @@ internal extension SessionUtil {
|
|||
String: (
|
||||
contact: Contact,
|
||||
profile: Profile,
|
||||
isHiddenConversation: Bool
|
||||
shouldBeVisible: Bool,
|
||||
priority: Int32
|
||||
)
|
||||
]
|
||||
|
||||
|
@ -66,7 +67,8 @@ internal extension SessionUtil {
|
|||
contactData[contactId] = (
|
||||
contactResult,
|
||||
profileResult,
|
||||
contact.hidden
|
||||
(contact.hidden == false),
|
||||
contact.priority
|
||||
)
|
||||
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
|
||||
/// associated contact conversation accordingly
|
||||
let threadExists: Bool = try SessionThread.exists(db, id: contact.id)
|
||||
let threadIsVisible: Bool = try SessionThread
|
||||
.filter(id: contact.id)
|
||||
.select(.shouldBeVisible)
|
||||
.asRequest(of: Bool.self)
|
||||
let threadInfo: PriorityVisibilityInfo? = try? SessionThread
|
||||
.select(.id, .variant, .pinnedPriority, .shouldBeVisible)
|
||||
.asRequest(of: PriorityVisibilityInfo.self)
|
||||
.fetchOne(db)
|
||||
.defaulting(to: false)
|
||||
let threadExists: Bool = (threadInfo != nil)
|
||||
let threadIsVisible: Bool = (threadInfo?.shouldBeVisible ?? false)
|
||||
|
||||
switch (data.isHiddenConversation, threadExists, threadIsVisible) {
|
||||
case (true, true, _):
|
||||
switch (data.shouldBeVisible, threadExists, threadIsVisible) {
|
||||
case (false, true, _):
|
||||
try SessionThread
|
||||
.filter(id: contact.id)
|
||||
.deleteAll(db)
|
||||
|
||||
case (false, false, _):
|
||||
case (true, false, _):
|
||||
try SessionThread(
|
||||
id: contact.id,
|
||||
variant: .contact,
|
||||
shouldBeVisible: true
|
||||
shouldBeVisible: true,
|
||||
pinnedPriority: data.priority
|
||||
).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
|
||||
.filter(id: contact.id)
|
||||
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
|
||||
db,
|
||||
SessionThread.Columns.shouldBeVisible.set(to: !data.isHiddenConversation)
|
||||
changes
|
||||
)
|
||||
|
||||
default: break
|
||||
|
@ -253,26 +262,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
// MARK: - Outgoing Changes
|
||||
|
||||
internal extension SessionUtil {
|
||||
static func updatingContacts<T>(_ db: Database, _ updated: [T]) throws -> [T] {
|
||||
|
@ -285,48 +275,43 @@ internal extension SessionUtil {
|
|||
// If we only updated the current user contact then no need to continue
|
||||
guard !targetContacts.isEmpty else { return updated }
|
||||
|
||||
do {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
// When inserting new contacts (or contacts with invalid profile data) we want
|
||||
// to add any valid profile information we have so identify if any of the updated
|
||||
// contacts are new/invalid, and if so, fetch any profile data we have for them
|
||||
let newContactIds: [String] = targetContacts
|
||||
.compactMap { contactData -> String? in
|
||||
var cContactId: [CChar] = contactData.id.cArray
|
||||
var contact: contacts_contact = contacts_contact()
|
||||
|
||||
guard
|
||||
contacts_get(conf, &contact, &cContactId),
|
||||
String(libSessionVal: contact.name, nullIfEmpty: true) != nil
|
||||
else { return contactData.id }
|
||||
|
||||
return nil
|
||||
}
|
||||
let newProfiles: [String: Profile] = try Profile
|
||||
.fetchAll(db, ids: newContactIds)
|
||||
.reduce(into: [:]) { result, next in result[next.id] = next }
|
||||
|
||||
// Upsert the updated contact data
|
||||
try SessionUtil
|
||||
.upsert(
|
||||
contactData: targetContacts
|
||||
.map { contact in
|
||||
SyncedContactInfo(
|
||||
id: contact.id,
|
||||
contact: contact,
|
||||
profile: newProfiles[contact.id]
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
// When inserting new contacts (or contacts with invalid profile data) we want
|
||||
// to add any valid profile information we have so identify if any of the updated
|
||||
// contacts are new/invalid, and if so, fetch any profile data we have for them
|
||||
let newContactIds: [String] = targetContacts
|
||||
.compactMap { contactData -> String? in
|
||||
var cContactId: [CChar] = contactData.id.cArray
|
||||
var contact: contacts_contact = contacts_contact()
|
||||
|
||||
guard
|
||||
contacts_get(conf, &contact, &cContactId),
|
||||
String(libSessionVal: contact.name, nullIfEmpty: true) != nil
|
||||
else { return contactData.id }
|
||||
|
||||
return nil
|
||||
}
|
||||
let newProfiles: [String: Profile] = try Profile
|
||||
.fetchAll(db, ids: newContactIds)
|
||||
.reduce(into: [:]) { result, next in result[next.id] = next }
|
||||
|
||||
// Upsert the updated contact data
|
||||
try SessionUtil
|
||||
.upsert(
|
||||
contactData: targetContacts
|
||||
.map { contact in
|
||||
SyncedContactInfo(
|
||||
id: contact.id,
|
||||
contact: contact,
|
||||
profile: newProfiles[contact.id]
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
return updated
|
||||
|
@ -357,42 +342,75 @@ internal extension SessionUtil {
|
|||
existingContactIds.contains($0.id)
|
||||
}
|
||||
|
||||
do {
|
||||
// Update the user profile first (if needed)
|
||||
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.update(
|
||||
profile: updatedUserProfile,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the user profile first (if needed)
|
||||
if let updatedUserProfile: Profile = updatedProfiles.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil
|
||||
.upsert(
|
||||
contactData: targetProfiles
|
||||
.map { SyncedContactInfo(id: $0.id, profile: $0) },
|
||||
in: conf
|
||||
)
|
||||
try SessionUtil.update(
|
||||
profile: updatedUserProfile,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil
|
||||
.upsert(
|
||||
contactData: targetProfiles
|
||||
.map { SyncedContactInfo(id: $0.id, profile: $0) },
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
extension SessionUtil {
|
||||
|
@ -400,21 +418,21 @@ extension SessionUtil {
|
|||
let id: String
|
||||
let contact: Contact?
|
||||
let profile: Profile?
|
||||
let priority: Int32?
|
||||
let hidden: Bool?
|
||||
let priority: Int32?
|
||||
|
||||
init(
|
||||
id: String,
|
||||
contact: Contact? = nil,
|
||||
profile: Profile? = nil,
|
||||
priority: Int32? = nil,
|
||||
hidden: Bool? = nil
|
||||
hidden: Bool? = nil,
|
||||
priority: Int32? = nil
|
||||
) {
|
||||
self.id = id
|
||||
self.contact = contact
|
||||
self.profile = profile
|
||||
self.priority = priority
|
||||
self.hidden = hidden
|
||||
self.priority = priority
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -158,16 +158,10 @@ internal extension SessionUtil {
|
|||
// If there are no newer local last read timestamps then just return the mergeResult
|
||||
guard !newerLocalChanges.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .convoInfoVolatile,
|
||||
publicKey: getUserHexEncodedPublicKey(db)
|
||||
) { conf in
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: newerLocalChanges,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
try upsert(
|
||||
convoInfoVolatileChanges: newerLocalChanges,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
static func upsert(
|
||||
|
@ -176,7 +170,21 @@ internal extension SessionUtil {
|
|||
) throws {
|
||||
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
|
||||
|
||||
switch threadInfo.variant {
|
||||
|
@ -250,11 +258,7 @@ internal extension SessionUtil {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
internal extension SessionUtil {
|
||||
|
||||
static func updateMarkedAsUnreadState(
|
||||
_ db: Database,
|
||||
threads: [SessionThread]
|
||||
|
@ -284,13 +288,20 @@ internal extension SessionUtil {
|
|||
)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: - External Outgoing Changes
|
||||
|
||||
public extension SessionUtil {
|
||||
static func syncThreadLastReadIfNeeded(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
lastReadTimestampMs: Int64
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
let change: VolatileThreadInfo = VolatileThreadInfo(
|
||||
threadId: threadId,
|
||||
variant: threadVariant,
|
||||
|
|
|
@ -20,6 +20,7 @@ internal extension SessionUtil {
|
|||
.appending(contentsOf: columnsRelatedToContacts)
|
||||
.appending(contentsOf: columnsRelatedToConvoInfoVolatile)
|
||||
.appending(contentsOf: columnsRelatedToUserGroups)
|
||||
.appending(contentsOf: columnsRelatedToThreads)
|
||||
.map { ColumnKey($0) }
|
||||
.asSet()
|
||||
|
||||
|
@ -34,28 +35,36 @@ internal extension SessionUtil {
|
|||
) throws {
|
||||
// Since we are doing direct memory manipulation we are using an `Atomic`
|
||||
// type which has blocking access in it's `mutate` closure
|
||||
let needsPush: Bool = try SessionUtil
|
||||
.config(
|
||||
for: variant,
|
||||
publicKey: publicKey
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// Peform the change
|
||||
try change(conf)
|
||||
|
||||
// If we don't need to dump the data the we can finish early
|
||||
guard config_needs_dump(conf) else { return config_needs_push(conf) }
|
||||
|
||||
try SessionUtil.createDump(
|
||||
conf: conf,
|
||||
let needsPush: Bool
|
||||
|
||||
do {
|
||||
needsPush = try SessionUtil
|
||||
.config(
|
||||
for: variant,
|
||||
publicKey: publicKey
|
||||
)?.save(db)
|
||||
|
||||
return config_needs_push(conf)
|
||||
}
|
||||
)
|
||||
.mutate { conf in
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
// Peform the change
|
||||
try change(conf)
|
||||
|
||||
// If we don't need to dump the data the we can finish early
|
||||
guard config_needs_dump(conf) else { return config_needs_push(conf) }
|
||||
|
||||
try SessionUtil.createDump(
|
||||
conf: conf,
|
||||
for: variant,
|
||||
publicKey: publicKey
|
||||
)?.save(db)
|
||||
|
||||
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
|
||||
guard needsPush else { return }
|
||||
|
@ -80,115 +89,164 @@ internal extension SessionUtil {
|
|||
.fetchAll(db, ids: updatedThreads.map { $0.id })
|
||||
.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)
|
||||
try SessionUtil.updateMarkedAsUnreadState(db, threads: updatedThreads)
|
||||
|
||||
// Then update the `hidden` and `priority` values
|
||||
try groupedThreads.forEach { variant, threads in
|
||||
switch variant {
|
||||
case .contact:
|
||||
// If the 'Note to Self' conversation is pinned then we need to custom handle it
|
||||
// first as it's part of the UserProfile config
|
||||
if let noteToSelf: SessionThread = threads.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.updateNoteToSelf(
|
||||
db,
|
||||
priority: noteToSelf.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0),
|
||||
hidden: noteToSelf.shouldBeVisible,
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the 'Note to Self' convo from the list for updating contact priorities
|
||||
let remainingThreads: [SessionThread] = threads.filter { $0.id != userPublicKey }
|
||||
|
||||
guard !remainingThreads.isEmpty else { return }
|
||||
|
||||
// Update the unread state for the threads first (just in case that's what changed)
|
||||
try SessionUtil.updateMarkedAsUnreadState(db, threads: updatedThreads)
|
||||
|
||||
// Then update the `hidden` and `priority` values
|
||||
try groupedThreads.forEach { variant, threads in
|
||||
switch variant {
|
||||
case .contact:
|
||||
// If the 'Note to Self' conversation is pinned then we need to custom handle it
|
||||
// first as it's part of the UserProfile config
|
||||
if let noteToSelf: SessionThread = threads.first(where: { $0.id == userPublicKey }) {
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
for: .userProfile,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
contactData: remainingThreads
|
||||
.map { thread in
|
||||
SyncedContactInfo(
|
||||
id: thread.id,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0),
|
||||
hidden: thread.shouldBeVisible
|
||||
)
|
||||
},
|
||||
try SessionUtil.updateNoteToSelf(
|
||||
hidden: !noteToSelf.shouldBeVisible,
|
||||
priority: noteToSelf.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0),
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
case .community:
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
communities: threads
|
||||
.compactMap { thread -> CommunityInfo? in
|
||||
urlInfo[thread.id].map { urlInfo in
|
||||
CommunityInfo(
|
||||
urlInfo: urlInfo,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0)
|
||||
)
|
||||
}
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
case .legacyGroup:
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
legacyGroups: threads
|
||||
.map { thread in
|
||||
LegacyGroupInfo(
|
||||
id: thread.id,
|
||||
hidden: thread.shouldBeVisible,
|
||||
}
|
||||
|
||||
// Remove the 'Note to Self' convo from the list for updating contact priorities
|
||||
let remainingThreads: [SessionThread] = threads.filter { $0.id != userPublicKey }
|
||||
|
||||
guard !remainingThreads.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .contacts,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
contactData: remainingThreads
|
||||
.map { thread in
|
||||
SyncedContactInfo(
|
||||
id: thread.id,
|
||||
hidden: !thread.shouldBeVisible,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0)
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
case .community:
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
communities: threads
|
||||
.compactMap { thread -> CommunityInfo? in
|
||||
urlInfo[thread.id].map { urlInfo in
|
||||
CommunityInfo(
|
||||
urlInfo: urlInfo,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0)
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
}
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
case .group:
|
||||
// TODO: Add this
|
||||
break
|
||||
}
|
||||
case .legacyGroup:
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
publicKey: userPublicKey
|
||||
) { conf in
|
||||
try SessionUtil.upsert(
|
||||
legacyGroups: threads
|
||||
.map { thread in
|
||||
LegacyGroupInfo(
|
||||
id: thread.id,
|
||||
hidden: !thread.shouldBeVisible,
|
||||
priority: thread.pinnedPriority
|
||||
.map { Int32($0 == 0 ? 0 : max($0, 1)) }
|
||||
.defaulting(to: 0)
|
||||
)
|
||||
},
|
||||
in: conf
|
||||
)
|
||||
}
|
||||
|
||||
case .group:
|
||||
break
|
||||
}
|
||||
}
|
||||
catch {
|
||||
SNLog("[libSession-util] Failed to dump updated data")
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
internal extension SessionUtil {
|
||||
|
|
|
@ -25,7 +25,6 @@ internal extension SessionUtil {
|
|||
|
||||
var communities: [PrioritisedData<OpenGroupUrlInfo>] = []
|
||||
var legacyGroups: [LegacyGroupInfo] = []
|
||||
var groups: [PrioritisedData<String>] = []
|
||||
var community: ugroups_community_info = ugroups_community_info()
|
||||
var legacyGroup: ugroups_legacy_group_info = ugroups_legacy_group_info()
|
||||
let groupsIterator: OpaquePointer = user_groups_iterator_new(conf)
|
||||
|
@ -85,18 +84,28 @@ internal extension SessionUtil {
|
|||
.defaultWith(groupId)
|
||||
.with(
|
||||
isEnabled: (legacyGroup.disappearing_timer > 0),
|
||||
durationSeconds: (legacyGroup.disappearing_timer == 0 ? nil :
|
||||
TimeInterval(legacyGroup.disappearing_timer)
|
||||
)
|
||||
durationSeconds: TimeInterval(legacyGroup.disappearing_timer)
|
||||
),
|
||||
groupMembers: members.map { memberId, admin in
|
||||
GroupMember(
|
||||
groupId: groupId,
|
||||
profileId: memberId,
|
||||
role: (admin ? .admin : .standard),
|
||||
isHidden: false
|
||||
)
|
||||
},
|
||||
groupMembers: members
|
||||
.filter { _, isAdmin in !isAdmin }
|
||||
.map { memberId, admin in
|
||||
GroupMember(
|
||||
groupId: groupId,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
)
|
||||
},
|
||||
groupAdmins: members
|
||||
.filter { _, isAdmin in isAdmin }
|
||||
.map { memberId, admin in
|
||||
GroupMember(
|
||||
groupId: groupId,
|
||||
profileId: memberId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
)
|
||||
},
|
||||
hidden: legacyGroup.hidden,
|
||||
priority: legacyGroup.priority
|
||||
)
|
||||
|
@ -111,7 +120,7 @@ internal extension SessionUtil {
|
|||
user_groups_iterator_free(groupsIterator) // Need to free the iterator
|
||||
|
||||
// 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
|
||||
let existingThreadInfo: [String: PriorityVisibilityInfo] = (try? SessionThread
|
||||
|
@ -187,7 +196,8 @@ internal extension SessionUtil {
|
|||
guard
|
||||
let name: String = group.name,
|
||||
let lastKeyPair: ClosedGroupKeyPair = group.lastKeyPair,
|
||||
let members: [GroupMember] = group.groupMembers
|
||||
let members: [GroupMember] = group.groupMembers,
|
||||
let updatedAdmins: [GroupMember] = group.groupAdmins
|
||||
else { return }
|
||||
|
||||
if !existingLegacyGroupIds.contains(group.id) {
|
||||
|
@ -201,13 +211,12 @@ internal extension SessionUtil {
|
|||
secretKey: lastKeyPair.secretKey.bytes
|
||||
),
|
||||
members: members
|
||||
.filter { $0.role == .standard }
|
||||
.map { $0.profileId },
|
||||
admins: members
|
||||
.filter { $0.role == .admin }
|
||||
.appending(contentsOf: updatedAdmins) // Admins should also have 'standard' member entries
|
||||
.map { $0.profileId },
|
||||
admins: updatedAdmins.map { $0.profileId },
|
||||
expirationTimer: UInt32(group.disappearingConfig?.durationSeconds ?? 0),
|
||||
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000)
|
||||
messageSentTimestamp: UInt64(latestConfigUpdateSentTimestamp * 1000),
|
||||
calledFromConfigHandling: true
|
||||
)
|
||||
}
|
||||
else {
|
||||
|
@ -223,11 +232,7 @@ internal extension SessionUtil {
|
|||
|
||||
// Add the lastKey if it doesn't already exist
|
||||
let keyPairExists: Bool = ClosedGroupKeyPair
|
||||
.filter(
|
||||
ClosedGroupKeyPair.Columns.threadId == lastKeyPair.threadId &&
|
||||
ClosedGroupKeyPair.Columns.publicKey == lastKeyPair.publicKey &&
|
||||
ClosedGroupKeyPair.Columns.secretKey == lastKeyPair.secretKey
|
||||
)
|
||||
.filter(ClosedGroupKeyPair.Columns.threadKeyPairHash == lastKeyPair.threadKeyPairHash)
|
||||
.isNotEmpty(db)
|
||||
|
||||
if !keyPairExists {
|
||||
|
@ -245,27 +250,70 @@ internal extension SessionUtil {
|
|||
.saved(db)
|
||||
|
||||
// Update the members
|
||||
// TODO: This
|
||||
// TODO: Going to need to decide whether we want to update the 'GroupMember' records in the database based on this config message changing
|
||||
// let members: [String]
|
||||
// let admins: [String]
|
||||
let updatedMembers: [GroupMember] = members
|
||||
.appending(
|
||||
contentsOf: updatedAdmins.map { admin in
|
||||
GroupMember(
|
||||
groupId: admin.groupId,
|
||||
profileId: admin.profileId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
if
|
||||
let existingMembers: [GroupMember] = existingLegacyGroupMembers[group.id]?
|
||||
.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
|
||||
var threadChanges: [ConfigColumnAssignment] = []
|
||||
// Set the visibility if it's changed
|
||||
if existingThreadInfo[group.id]?.shouldBeVisible != (group.hidden == false) {
|
||||
threadChanges.append(
|
||||
// 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))
|
||||
)
|
||||
}
|
||||
|
||||
// Set the priority if it's changed
|
||||
if existingThreadInfo[group.id]?.pinnedPriority != group.priority {
|
||||
threadChanges.append(
|
||||
),
|
||||
(existingThreadInfo[group.id]?.pinnedPriority == group.priority ? nil :
|
||||
SessionThread.Columns.pinnedPriority.set(to: group.priority)
|
||||
)
|
||||
}
|
||||
].compactMap { $0 }
|
||||
|
||||
if !threadChanges.isEmpty {
|
||||
_ = try? SessionThread
|
||||
|
@ -321,6 +369,7 @@ internal extension SessionUtil {
|
|||
if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair {
|
||||
userGroup.pointee.enc_pubkey = lastKeyPair.publicKey.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)
|
||||
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)
|
||||
if let updatedConfig: DisappearingMessagesConfiguration = legacyGroup.disappearingConfig {
|
||||
// TODO: double check the 'isEnabled' flag
|
||||
userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 :
|
||||
Int64(floor(updatedConfig.durationSeconds))
|
||||
)
|
||||
|
@ -393,6 +441,9 @@ public extension SessionUtil {
|
|||
rootToken: String,
|
||||
publicKey: String
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -415,6 +466,9 @@ public extension SessionUtil {
|
|||
}
|
||||
|
||||
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(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -437,9 +491,13 @@ public extension SessionUtil {
|
|||
latestKeyPairPublicKey: Data,
|
||||
latestKeyPairSecretKey: Data,
|
||||
latestKeyPairReceivedTimestamp: TimeInterval,
|
||||
disappearingConfig: DisappearingMessagesConfiguration,
|
||||
members: Set<String>,
|
||||
admins: Set<String>
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -456,6 +514,7 @@ public extension SessionUtil {
|
|||
secretKey: latestKeyPairSecretKey,
|
||||
receivedTimestamp: latestKeyPairReceivedTimestamp
|
||||
),
|
||||
disappearingConfig: disappearingConfig,
|
||||
groupMembers: members
|
||||
.map { memberId in
|
||||
GroupMember(
|
||||
|
@ -486,9 +545,13 @@ public extension SessionUtil {
|
|||
groupPublicKey: String,
|
||||
name: String? = nil,
|
||||
latestKeyPair: ClosedGroupKeyPair? = nil,
|
||||
disappearingConfig: DisappearingMessagesConfiguration? = nil,
|
||||
members: Set<String>? = nil,
|
||||
admins: Set<String>? = nil
|
||||
) throws {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -500,6 +563,7 @@ public extension SessionUtil {
|
|||
id: groupPublicKey,
|
||||
name: name,
|
||||
lastKeyPair: latestKeyPair,
|
||||
disappearingConfig: disappearingConfig,
|
||||
groupMembers: members?
|
||||
.map { memberId in
|
||||
GroupMember(
|
||||
|
@ -526,6 +590,8 @@ public extension SessionUtil {
|
|||
}
|
||||
|
||||
static func hide(_ db: Database, legacyGroupIds: [String]) throws {
|
||||
guard !legacyGroupIds.isEmpty else { return }
|
||||
|
||||
try SessionUtil.performAndPushChange(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -544,6 +610,10 @@ public extension SessionUtil {
|
|||
}
|
||||
|
||||
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(
|
||||
db,
|
||||
for: .userGroups,
|
||||
|
@ -561,9 +631,13 @@ public extension SessionUtil {
|
|||
// MARK: -- Group Changes
|
||||
|
||||
static func hide(_ db: Database, groupIds: [String]) throws {
|
||||
guard !groupIds.isEmpty else { return }
|
||||
}
|
||||
|
||||
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 GRDB
|
||||
|
@ -44,7 +44,7 @@ internal extension SessionUtil {
|
|||
return .updateTo(
|
||||
url: profilePictureUrl,
|
||||
key: Data(
|
||||
libSessionVal: profilePic.url,
|
||||
libSessionVal: profilePic.key,
|
||||
count: ProfileManager.avatarAES256KeyByteLength
|
||||
),
|
||||
fileName: nil
|
||||
|
@ -54,6 +54,52 @@ internal extension SessionUtil {
|
|||
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
|
||||
// in case the account got into a weird state or restored directly from a migration)
|
||||
let userContact: Contact = Contact.fetchOrCreate(db, id: userPublicKey)
|
||||
|
@ -91,14 +137,18 @@ internal extension SessionUtil {
|
|||
}
|
||||
|
||||
static func updateNoteToSelf(
|
||||
_ db: Database,
|
||||
priority: Int32,
|
||||
hidden: Bool,
|
||||
hidden: Bool? = nil,
|
||||
priority: Int32? = nil,
|
||||
in conf: UnsafeMutablePointer<config_object>?
|
||||
) throws {
|
||||
guard conf != nil else { throw SessionUtilError.nilConfigObject }
|
||||
|
||||
user_profile_set_nts_priority(conf, priority)
|
||||
user_profile_set_nts_hidden(conf, hidden)
|
||||
if let hidden: Bool = 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
|
||||
|
||||
public extension QueryInterfaceRequest {
|
||||
public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & TableRecord {
|
||||
|
||||
// MARK: -- updateAll
|
||||
|
||||
@discardableResult
|
||||
func updateAll(
|
||||
_ db: Database,
|
||||
|
@ -61,30 +64,16 @@ public extension QueryInterfaceRequest {
|
|||
) throws -> Int {
|
||||
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 {
|
||||
return try self.updateAll(db, targetAssignments)
|
||||
}
|
||||
|
||||
switch self {
|
||||
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)
|
||||
}
|
||||
return try self.updateAndFetchAllAndUpdateConfig(db, assignments).count
|
||||
}
|
||||
}
|
||||
|
||||
public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & TableRecord {
|
||||
|
||||
// MARK: -- updateAndFetchAll
|
||||
|
||||
@discardableResult
|
||||
func updateAndFetchAllAndUpdateConfig(
|
||||
_ db: Database,
|
||||
|
@ -120,9 +109,6 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
|||
}
|
||||
|
||||
// Update the config dump state where needed
|
||||
|
||||
try SessionUtil.updateThreadPrioritiesIfNeeded(db, assignments, updatedData)
|
||||
|
||||
switch self {
|
||||
case is QueryInterfaceRequest<Contact>:
|
||||
return try SessionUtil.updatingContacts(db, updatedData)
|
||||
|
@ -130,9 +116,6 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
|
|||
case is QueryInterfaceRequest<Profile>:
|
||||
return try SessionUtil.updatingProfiles(db, updatedData)
|
||||
|
||||
case is QueryInterfaceRequest<ClosedGroup>:
|
||||
return updatedData
|
||||
|
||||
case is QueryInterfaceRequest<SessionThread>:
|
||||
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
|
||||
/// loaded yet (eg. fresh install)
|
||||
public static var needsSync: Bool {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return false }
|
||||
|
||||
return configStore
|
||||
.wrappedValue
|
||||
.contains { _, atomicConf in
|
||||
|
@ -63,15 +66,30 @@ public enum SessionUtil {
|
|||
// MARK: - Loading
|
||||
|
||||
public static func loadState(
|
||||
_ db: Database? = nil,
|
||||
userPublicKey: String,
|
||||
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
|
||||
let existingDumps: Set<ConfigDump> = Storage.shared
|
||||
.read { db in try ConfigDump.fetchSet(db) }
|
||||
.defaulting(to: [])
|
||||
let existingDumps: Set<ConfigDump> = ((try? ConfigDump.fetchSet(db)) ?? [])
|
||||
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
|
||||
.map { $0.variant }
|
||||
.asSet()
|
||||
|
@ -103,7 +121,7 @@ public enum SessionUtil {
|
|||
}
|
||||
}
|
||||
|
||||
internal static func loadState(
|
||||
private static func loadState(
|
||||
for variant: ConfigDump.Variant,
|
||||
secretKey ed25519SecretKey: [UInt8],
|
||||
cachedData: Data?
|
||||
|
@ -265,6 +283,9 @@ public enum SessionUtil {
|
|||
}
|
||||
|
||||
public static func configHashes(for publicKey: String) -> [String] {
|
||||
// FIXME: Remove this once `useSharedUtilForUserConfig` is permanent
|
||||
guard Features.useSharedUtilForUserConfig else { return [] }
|
||||
|
||||
return Storage.shared
|
||||
.read { db -> [String] in
|
||||
guard Identity.userExists(db) else { return [] }
|
||||
|
@ -342,36 +363,42 @@ public enum SessionUtil {
|
|||
mergeData.forEach { $0?.deallocate() }
|
||||
|
||||
// Apply the updated states to the database
|
||||
switch next.key {
|
||||
case .userProfile:
|
||||
try SessionUtil.handleUserProfileUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf),
|
||||
latestConfigUpdateSentTimestamp: messageSentTimestamp
|
||||
)
|
||||
|
||||
case .contacts:
|
||||
try SessionUtil.handleContactsUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf)
|
||||
)
|
||||
|
||||
case .convoInfoVolatile:
|
||||
try SessionUtil.handleConvoInfoVolatileUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf)
|
||||
)
|
||||
|
||||
case .userGroups:
|
||||
try SessionUtil.handleGroupsUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf),
|
||||
latestConfigUpdateSentTimestamp: messageSentTimestamp
|
||||
)
|
||||
do {
|
||||
switch next.key {
|
||||
case .userProfile:
|
||||
try SessionUtil.handleUserProfileUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf),
|
||||
latestConfigUpdateSentTimestamp: messageSentTimestamp
|
||||
)
|
||||
|
||||
case .contacts:
|
||||
try SessionUtil.handleContactsUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf)
|
||||
)
|
||||
|
||||
case .convoInfoVolatile:
|
||||
try SessionUtil.handleConvoInfoVolatileUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf)
|
||||
)
|
||||
|
||||
case .userGroups:
|
||||
try SessionUtil.handleGroupsUpdate(
|
||||
db,
|
||||
in: conf,
|
||||
mergeNeedsDump: config_needs_dump(conf),
|
||||
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
|
||||
|
|
|
@ -4,18 +4,6 @@
|
|||
<dict>
|
||||
<key>AvailableLibraries</key>
|
||||
<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>
|
||||
<key>LibraryIdentifier</key>
|
||||
<string>ios-arm64_x86_64-simulator</string>
|
||||
|
@ -31,6 +19,18 @@
|
|||
<key>SupportedPlatformVariant</key>
|
||||
<string>simulator</string>
|
||||
</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>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XFWK</string>
|
||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -488,43 +488,43 @@ public final class OpenGroupManager {
|
|||
.deleteAll(db)
|
||||
|
||||
try roomDetails.admins.forEach { adminId in
|
||||
_ = try GroupMember(
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).saved(db)
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try roomDetails.hiddenAdmins
|
||||
.defaulting(to: [])
|
||||
.forEach { adminId in
|
||||
_ = try GroupMember(
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: true
|
||||
).saved(db)
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try roomDetails.moderators.forEach { moderatorId in
|
||||
_ = try GroupMember(
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: moderatorId,
|
||||
role: .moderator,
|
||||
isHidden: false
|
||||
).saved(db)
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try roomDetails.hiddenModerators
|
||||
.defaulting(to: [])
|
||||
.forEach { moderatorId in
|
||||
_ = try GroupMember(
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: moderatorId,
|
||||
role: .moderator,
|
||||
isHidden: true
|
||||
).saved(db)
|
||||
).save(db)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +8,56 @@ import SessionUtilitiesKit
|
|||
import SessionSnodeKit
|
||||
|
||||
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 {
|
||||
case .new: try handleNewClosedGroup(db, message: message)
|
||||
case .encryptionKeyPair: try handleClosedGroupEncryptionKeyPair(db, message: message)
|
||||
case .nameChange: try handleClosedGroupNameChanged(db, message: message)
|
||||
case .membersAdded: try handleClosedGroupMembersAdded(db, message: message)
|
||||
case .membersRemoved: try handleClosedGroupMembersRemoved(db, message: message)
|
||||
case .memberLeft: try handleClosedGroupMemberLeft(db, message: message)
|
||||
case .encryptionKeyPairRequest:
|
||||
handleClosedGroupEncryptionKeyPairRequest(db, message: message) // Currently not used
|
||||
|
||||
case .encryptionKeyPair:
|
||||
try handleClosedGroupEncryptionKeyPair(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
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
|
||||
}
|
||||
|
@ -39,7 +79,8 @@ extension MessageReceiver {
|
|||
members: membersAsData.map { $0.toHexString() },
|
||||
admins: adminsAsData.map { $0.toHexString() },
|
||||
expirationTimer: expirationTimer,
|
||||
messageSentTimestamp: sentTimestamp
|
||||
messageSentTimestamp: sentTimestamp,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -51,7 +92,8 @@ extension MessageReceiver {
|
|||
members: [String],
|
||||
admins: [String],
|
||||
expirationTimer: UInt32,
|
||||
messageSentTimestamp: UInt64
|
||||
messageSentTimestamp: UInt64,
|
||||
calledFromConfigHandling: Bool
|
||||
) throws {
|
||||
// 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
|
||||
|
@ -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
|
||||
let thread: SessionThread = try SessionThread
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup)
|
||||
.with(shouldBeVisible: true)
|
||||
.saved(db)
|
||||
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
|
||||
let closedGroup: ClosedGroup = try ClosedGroup(
|
||||
threadId: groupPublicKey,
|
||||
name: name,
|
||||
|
@ -103,7 +146,7 @@ extension MessageReceiver {
|
|||
}
|
||||
|
||||
// Update the DisappearingMessages config
|
||||
try thread.disappearingMessagesConfiguration
|
||||
let disappearingConfig: DisappearingMessagesConfiguration = try thread.disappearingMessagesConfiguration
|
||||
.fetchOne(db)
|
||||
.defaulting(to: DisappearingMessagesConfiguration.defaultWith(thread.id))
|
||||
.with(
|
||||
|
@ -113,15 +156,38 @@ extension MessageReceiver {
|
|||
(24 * 60 * 60)
|
||||
)
|
||||
)
|
||||
.save(db)
|
||||
.saved(db)
|
||||
|
||||
// Store the key pair
|
||||
try ClosedGroupKeyPair(
|
||||
// Store the key pair if it doesn't already exist
|
||||
let receivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
let newKeyPair: ClosedGroupKeyPair = ClosedGroupKeyPair(
|
||||
threadId: groupPublicKey,
|
||||
publicKey: Data(encryptionKeyPair.publicKey),
|
||||
secretKey: Data(encryptionKeyPair.secretKey),
|
||||
receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
|
||||
).insert(db)
|
||||
receivedTimestamp: receivedTimestamp
|
||||
)
|
||||
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
|
||||
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
|
||||
/// sent by the group admin.
|
||||
private static func handleClosedGroupEncryptionKeyPair(_ db: Database, message: ClosedGroupControlMessage) throws {
|
||||
guard
|
||||
case let .encryptionKeyPair(explicitGroupPublicKey, wrappers) = message.kind,
|
||||
let groupPublicKey: String = (explicitGroupPublicKey?.toHexString() ?? message.groupPublicKey)
|
||||
else { return }
|
||||
private static func handleClosedGroupEncryptionKeyPair(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
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 {
|
||||
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.")
|
||||
}
|
||||
guard let closedGroup: ClosedGroup = try? thread.closedGroup.fetchOne(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 {
|
||||
return SNLog("Ignoring closed group encryption key pair from non-admin.")
|
||||
|
@ -203,368 +275,355 @@ extension MessageReceiver {
|
|||
SNLog("Received a new closed group encryption key pair.")
|
||||
}
|
||||
|
||||
private static func handleClosedGroupNameChanged(_ db: Database, message: ClosedGroupControlMessage) throws {
|
||||
guard case let .nameChange(name) = message.kind else { return }
|
||||
private static func handleClosedGroupNameChanged(
|
||||
_ 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 ClosedGroup
|
||||
.filter(id: id)
|
||||
.updateAll(db, ClosedGroup.Columns.name.set(to: name))
|
||||
|
||||
// Notify the user if needed
|
||||
guard name != closedGroup.name else { return }
|
||||
|
||||
_ = try Interaction(
|
||||
serverHash: message.serverHash,
|
||||
threadId: thread.id,
|
||||
authorId: sender,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.nameChange(name: name)
|
||||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
try processIfValid(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message,
|
||||
messageKind: messageKind,
|
||||
infoMessageVariant: .infoClosedGroupUpdated,
|
||||
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
name: name
|
||||
)
|
||||
).inserted(db)
|
||||
|
||||
// Update libSession
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: id,
|
||||
name: name
|
||||
)
|
||||
}
|
||||
|
||||
_ = 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, message: ClosedGroupControlMessage) throws {
|
||||
guard case let .membersAdded(membersAsData) = message.kind else { return }
|
||||
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 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 }
|
||||
.asSet()
|
||||
let members: Set<String> = currentMemberIds.union(addedMembers)
|
||||
|
||||
// Create records for any new members
|
||||
try addedMembers
|
||||
.filter { !currentMemberIds.contains($0) }
|
||||
.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: id,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Send the latest encryption key pair to the added members if the current user is
|
||||
// the admin of the group
|
||||
//
|
||||
// This fixes a race condition where:
|
||||
// • A member removes another member.
|
||||
// • A member adds someone to the group and sends them the latest group key pair.
|
||||
// • The admin is offline during all of this.
|
||||
// • When the admin comes back online they see the member removed message and generate +
|
||||
// distribute a new key pair, but they don't know about the added member yet.
|
||||
// • Now they see the member added message.
|
||||
//
|
||||
// Without the code below, the added member(s) would never get the key pair that was
|
||||
// generated by the admin when they saw the member removed message.
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
if allGroupMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) {
|
||||
addedMembers.forEach { memberId in
|
||||
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: id)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any 'zombie' versions of the added members (in case they were re-added)
|
||||
_ = try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == id)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
|
||||
.filter(addedMembers.contains(GroupMember.Columns.profileId))
|
||||
.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()
|
||||
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
|
||||
try? SessionUtil.update(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
members: allMembers
|
||||
.filter { $0.role == .standard || $0.role == .zombie }
|
||||
.map { $0.profileId }
|
||||
.asSet()
|
||||
.union(addedMembers),
|
||||
admins: allMembers
|
||||
.filter { $0.role == .admin }
|
||||
.map { $0.profileId }
|
||||
.asSet()
|
||||
)
|
||||
).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()
|
||||
)
|
||||
}
|
||||
|
||||
// Create records for any new members
|
||||
try addedMembers
|
||||
.filter { !currentMemberIds.contains($0) }
|
||||
.forEach { memberId in
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Send the latest encryption key pair to the added members if the current user is
|
||||
// the admin of the group
|
||||
//
|
||||
// This fixes a race condition where:
|
||||
// • A member removes another member.
|
||||
// • A member adds someone to the group and sends them the latest group key pair.
|
||||
// • The admin is offline during all of this.
|
||||
// • When the admin comes back online they see the member removed message and generate +
|
||||
// distribute a new key pair, but they don't know about the added member yet.
|
||||
// • Now they see the member added message.
|
||||
//
|
||||
// Without the code below, the added member(s) would never get the key pair that was
|
||||
// generated by the admin when they saw the member removed message.
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
|
||||
if allMembers.contains(where: { $0.role == .admin && $0.profileId == userPublicKey }) {
|
||||
addedMembers.forEach { memberId in
|
||||
MessageSender.sendLatestEncryptionKeyPair(db, to: memberId, for: threadId)
|
||||
}
|
||||
}
|
||||
|
||||
// Remove any 'zombie' versions of the added members (in case they were re-added)
|
||||
_ = try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.filter(GroupMember.Columns.role == GroupMember.Role.zombie)
|
||||
.filter(addedMembers.contains(GroupMember.Columns.profileId))
|
||||
.deleteAll(db)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
/// Removes the given members from the group IF
|
||||
/// • it wasn't the admin that was removed (that should happen through a `MEMBER_LEFT` message).
|
||||
/// • 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
|
||||
/// 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 {
|
||||
guard case let .membersRemoved(membersAsData) = message.kind else { return }
|
||||
private static func handleClosedGroupMembersRemoved(
|
||||
_ 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
|
||||
// Check that the admin wasn't removed
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
return
|
||||
}
|
||||
|
||||
let removedMembers = membersAsData.map { $0.toHexString() }
|
||||
let currentMemberIds: Set<String> = allGroupMembers
|
||||
.filter { $0.role == .standard }
|
||||
.map { $0.profileId }
|
||||
.asSet()
|
||||
let members = currentMemberIds.subtracting(removedMembers)
|
||||
|
||||
guard let firstAdminId: String = allGroupMembers.filter({ $0.role == .admin }).first?.profileId, members.contains(firstAdminId) else {
|
||||
return SNLog("Ignoring invalid closed group update.")
|
||||
}
|
||||
// Check that the message was sent by the group admin
|
||||
guard allGroupMembers.filter({ $0.role == .admin }).contains(where: { $0.profileId == sender }) else {
|
||||
return SNLog("Ignoring invalid closed group update.")
|
||||
}
|
||||
|
||||
// Delete the removed members
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == id)
|
||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||
.deleteAll(db)
|
||||
|
||||
// If the current user was removed:
|
||||
// • Stop polling for the group
|
||||
// • Remove the key pairs associated with the group
|
||||
// • Notify the PN server
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let wasCurrentUserRemoved: Bool = !members.contains(userPublicKey)
|
||||
|
||||
if wasCurrentUserRemoved {
|
||||
ClosedGroupPoller.shared.stopPolling(for: id)
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let removedMemberIds: [String] = membersAsData.map { $0.toHexString() }
|
||||
|
||||
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 currentMemberIds: Set<String> = allMembers
|
||||
.filter { $0.role == .standard }
|
||||
.map { $0.profileId }
|
||||
.asSet()
|
||||
let members = currentMemberIds.subtracting(removedMembers)
|
||||
|
||||
_ = try closedGroup
|
||||
.keyPairs
|
||||
// Check that the group creator is still a member and that the message was
|
||||
// sent by a group admin
|
||||
guard
|
||||
let firstAdminId: String = allMembers.filter({ $0.role == .admin })
|
||||
.first?
|
||||
.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
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.filter(removedMembers.contains(GroupMember.Columns.profileId))
|
||||
.filter([ GroupMember.Role.standard, GroupMember.Role.zombie ].contains(GroupMember.Columns.role))
|
||||
.deleteAll(db)
|
||||
|
||||
let _ = PushNotificationAPI.performOperation(
|
||||
.unsubscribe,
|
||||
for: id,
|
||||
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) }
|
||||
// If the current user was removed:
|
||||
// • Stop polling for the group
|
||||
// • Remove the key pairs associated with the group
|
||||
// • Notify the PN server
|
||||
let wasCurrentUserRemoved: Bool = !members.contains(userPublicKey)
|
||||
|
||||
if wasCurrentUserRemoved {
|
||||
ClosedGroupPoller.shared.stopPolling(for: threadId)
|
||||
|
||||
_ = try closedGroup
|
||||
.keyPairs
|
||||
.deleteAll(db)
|
||||
|
||||
let _ = PushNotificationAPI.performOperation(
|
||||
.unsubscribe,
|
||||
for: threadId,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
.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:
|
||||
/// • Mark them as a zombie (to be removed by the admin later).
|
||||
/// If the admin left:
|
||||
/// • Unsubscribe from PNs, delete the group public key, etc. as the group will be disbanded.
|
||||
private static func handleClosedGroupMemberLeft(_ db: Database, message: ClosedGroupControlMessage) throws {
|
||||
guard case .memberLeft = message.kind else { return }
|
||||
private static func handleClosedGroupMemberLeft(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
message: ClosedGroupControlMessage
|
||||
) throws {
|
||||
guard
|
||||
let messageKind: ClosedGroupControlMessage.Kind = message.kind,
|
||||
case .memberLeft = messageKind
|
||||
else { return }
|
||||
|
||||
try performIfValid(db, message: message) { id, sender, thread, closedGroup in
|
||||
guard let allGroupMembers: [GroupMember] = try? closedGroup.allMembers.fetchAll(db) else {
|
||||
return
|
||||
}
|
||||
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let didAdminLeave: Bool = allGroupMembers.contains(where: { member in
|
||||
member.role == .admin && member.profileId == sender
|
||||
})
|
||||
let members: [GroupMember] = allGroupMembers.filter { $0.role == .standard }
|
||||
let membersToRemove: [GroupMember] = members
|
||||
.filter { member in
|
||||
didAdminLeave || // If the admin leaves the group is disbanded
|
||||
member.profileId == sender
|
||||
}
|
||||
let updatedMemberIds: Set<String> = members
|
||||
.map { $0.profileId }
|
||||
.asSet()
|
||||
.subtracting(membersToRemove.map { $0.profileId })
|
||||
|
||||
// Delete the members to remove
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == id)
|
||||
.filter(updatedMemberIds.contains(GroupMember.Columns.profileId))
|
||||
.deleteAll(db)
|
||||
|
||||
if didAdminLeave || sender == userPublicKey {
|
||||
// Remove the group from the database and unsubscribe from PNs
|
||||
ClosedGroupPoller.shared.stopPolling(for: id)
|
||||
|
||||
_ = try closedGroup
|
||||
.keyPairs
|
||||
.deleteAll(db)
|
||||
|
||||
let _ = PushNotificationAPI.performOperation(
|
||||
.unsubscribe,
|
||||
for: id,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
}
|
||||
|
||||
// Re-add the removed member as a zombie (unless the admin left which disbands the
|
||||
// group)
|
||||
if !didAdminLeave {
|
||||
try GroupMember(
|
||||
groupId: id,
|
||||
profileId: sender,
|
||||
role: .zombie,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
}
|
||||
|
||||
// Notify the user if needed
|
||||
guard updatedMemberIds != Set(members.map { $0.profileId }) else { return }
|
||||
|
||||
_ = try Interaction(
|
||||
serverHash: message.serverHash,
|
||||
threadId: thread.id,
|
||||
authorId: sender,
|
||||
variant: .infoClosedGroupUpdated,
|
||||
body: ClosedGroupControlMessage.Kind
|
||||
.memberLeft
|
||||
.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) &&
|
||||
!membersToRemove.contains($0)
|
||||
try processIfValid(
|
||||
db,
|
||||
threadId: threadId,
|
||||
threadVariant: threadVariant,
|
||||
message: message,
|
||||
messageKind: messageKind,
|
||||
infoMessageVariant: .infoClosedGroupUpdated,
|
||||
legacyGroupChanges: { sender, closedGroup, allMembers in
|
||||
let userPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let didAdminLeave: Bool = allMembers.contains(where: { member in
|
||||
member.role == .admin && member.profileId == sender
|
||||
})
|
||||
let members: [GroupMember] = allMembers.filter { $0.role == .standard }
|
||||
let membersToRemove: [GroupMember] = members
|
||||
.filter { member in
|
||||
didAdminLeave || // If the admin leaves the group is disbanded
|
||||
member.profileId == sender
|
||||
}
|
||||
.map { $0.profileId }
|
||||
.asSet(),
|
||||
admins: allGroupMembers
|
||||
.filter { $0.role == .admin }
|
||||
let updatedMemberIds: Set<String> = members
|
||||
.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.")
|
||||
.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
|
||||
try GroupMember
|
||||
.filter(GroupMember.Columns.groupId == threadId)
|
||||
.filter(updatedMemberIds.contains(GroupMember.Columns.profileId))
|
||||
.deleteAll(db)
|
||||
|
||||
if didAdminLeave || sender == userPublicKey {
|
||||
try ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: threadId,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
}
|
||||
|
||||
// Re-add the removed member as a zombie (unless the admin left which disbands the
|
||||
// group)
|
||||
if !didAdminLeave {
|
||||
try GroupMember(
|
||||
groupId: threadId,
|
||||
profileId: sender,
|
||||
role: .zombie,
|
||||
isHidden: false
|
||||
).save(db)
|
||||
}
|
||||
}
|
||||
MessageSender.sendLatestEncryptionKeyPair(to: publicKey, for: groupPublicKey, using: transaction)
|
||||
}
|
||||
*/
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private static func performIfValid(
|
||||
private static func processIfValid(
|
||||
_ db: Database,
|
||||
threadId: String,
|
||||
threadVariant: SessionThread.Variant,
|
||||
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.")
|
||||
}
|
||||
|
||||
messageKind: ClosedGroupControlMessage.Kind,
|
||||
infoMessageVariant: Interaction.Variant,
|
||||
legacyGroupChanges: (String, ClosedGroup, [GroupMember]) throws -> ()
|
||||
) throws {
|
||||
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.")
|
||||
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: threadId) else {
|
||||
return SNLog("Ignoring group update for nonexistent group.")
|
||||
}
|
||||
|
||||
try update(groupPublicKey, sender, thread, closedGroup)
|
||||
// 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(
|
||||
serverHash: message.serverHash,
|
||||
threadId: threadId,
|
||||
authorId: sender,
|
||||
variant: infoMessageVariant,
|
||||
body: messageKind
|
||||
.infoMessage(db, sender: sender),
|
||||
timestampMs: (
|
||||
message.sentTimestamp.map { Int64($0) } ??
|
||||
SnodeAPI.currentOffsetTimestampMs()
|
||||
)
|
||||
).inserted(db)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -189,7 +189,8 @@ extension MessageReceiver {
|
|||
members: [String](closedGroup.members),
|
||||
admins: [String](closedGroup.admins),
|
||||
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
|
||||
_ = try Interaction(
|
||||
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
|
||||
// the message request
|
||||
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
|
||||
dependencies.sodium.sessionId(
|
||||
senderId,
|
||||
|
@ -111,6 +112,9 @@ extension MessageReceiver {
|
|||
.filter(ids: blindedContactIds)
|
||||
.deleteAll(db)
|
||||
|
||||
try? SessionUtil
|
||||
.remove(db, contactIds: blindedContactIds)
|
||||
|
||||
try updateContactApprovalStatusIfNeeded(
|
||||
db,
|
||||
senderSessionId: userPublicKey,
|
||||
|
|
|
@ -46,9 +46,20 @@ extension MessageReceiver {
|
|||
)
|
||||
}
|
||||
|
||||
// Get or create thread
|
||||
guard let threadInfo: (id: String, variant: SessionThread.Variant) = MessageReceiver.threadInfo(db, message: message, openGroupId: openGroupId) else {
|
||||
throw MessageReceiverError.noThread
|
||||
switch threadVariant {
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
|
|
|
@ -60,7 +60,7 @@ extension MessageSender {
|
|||
profileId: adminId,
|
||||
role: .admin,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
).save(db)
|
||||
}
|
||||
|
||||
try members.forEach { memberId in
|
||||
|
@ -69,7 +69,7 @@ extension MessageSender {
|
|||
profileId: memberId,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
).save(db)
|
||||
}
|
||||
|
||||
// Update libSession
|
||||
|
@ -80,6 +80,7 @@ extension MessageSender {
|
|||
latestKeyPairPublicKey: encryptionKeyPair.publicKey,
|
||||
latestKeyPairSecretKey: encryptionKeyPair.privateKey,
|
||||
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
|
||||
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
|
||||
members: members,
|
||||
admins: admins
|
||||
)
|
||||
|
@ -462,7 +463,7 @@ extension MessageSender {
|
|||
profileId: member,
|
||||
role: .standard,
|
||||
isHidden: false
|
||||
).insert(db)
|
||||
).save(db)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -629,12 +630,18 @@ extension MessageSender {
|
|||
}
|
||||
}
|
||||
catch {
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: groupPublicKey,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
switch error {
|
||||
case MessageSenderError.noKeyPair, MessageSenderError.encryptionFailed:
|
||||
try? ClosedGroup.removeKeysAndUnsubscribe(
|
||||
db,
|
||||
threadId: groupPublicKey,
|
||||
removeGroupData: false,
|
||||
calledFromConfigHandling: false
|
||||
)
|
||||
|
||||
default: break
|
||||
}
|
||||
|
||||
return Fail(error: error)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
|
|
@ -281,6 +281,17 @@ public enum MessageReceiver {
|
|||
case is TypingIndicator: break
|
||||
|
||||
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
|
||||
.filter(id: threadId)
|
||||
.updateAllAndConfig(
|
||||
|
|
|
@ -558,23 +558,25 @@ public struct ProfileManager {
|
|||
profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil))
|
||||
|
||||
case .updateTo(let url, let key, let fileName):
|
||||
if
|
||||
(
|
||||
url != profile.profilePictureUrl ||
|
||||
key != profile.profileEncryptionKey
|
||||
) &&
|
||||
key.count == ProfileManager.avatarAES256KeyByteLength &&
|
||||
key != profile.profileEncryptionKey
|
||||
{
|
||||
if url != profile.profilePictureUrl {
|
||||
profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url))
|
||||
profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key))
|
||||
avatarNeedsDownload = true
|
||||
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)
|
||||
if let fileName: String = 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"?>
|
||||
<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"/>
|
||||
<dependencies>
|
||||
<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="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<scenes>
|
||||
<!--ShareVC-->
|
||||
<!--Share Nav Controller-->
|
||||
<scene sceneID="ceB-am-kn3">
|
||||
<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">
|
||||
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
|
||||
<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>(
|
||||
withCustomName customName: String? = nil,
|
||||
on table: T.Type,
|
||||
|
|
Loading…
Reference in New Issue