diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift
index b3cb6f453..f9e645af6 100644
--- a/Session/Conversations/ConversationVC+Interaction.swift
+++ b/Session/Conversations/ConversationVC+Interaction.swift
@@ -1172,7 +1172,7 @@ extension ConversationVC:
openGroupPublicKey: String?
) {
guard viewModel.threadData.canWrite else { return }
- // FIXME: Add in support for starting a thread with a 'blinded25' id
+ // FIXME: Add in support for starting a thread with a 'blinded25' id (disabled until we support this decoding)
guard (try? SessionId.Prefix(from: sessionId)) != .blinded25 else { return }
guard (try? SessionId.Prefix(from: sessionId)) == .blinded15 else {
viewModel.dependencies[singleton: .storage].write { [dependencies = viewModel.dependencies] db in
diff --git a/Session/Settings/DeveloperSettingsViewModel.swift b/Session/Settings/DeveloperSettingsViewModel.swift
index 368f7bf82..c0db73b01 100644
--- a/Session/Settings/DeveloperSettingsViewModel.swift
+++ b/Session/Settings/DeveloperSettingsViewModel.swift
@@ -63,6 +63,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case debugDisappearingMessageDurations
case updatedGroups
+ case updatedGroupsDisableAutoApprove
case updatedGroupsRemoveMessagesOnKick
case updatedGroupsAllowHistoricAccessOnInvite
case updatedGroupsAllowDisplayPicture
@@ -84,6 +85,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
let updatedDisappearingMessages: Bool
let updatedGroups: Bool
+ let updatedGroupsDisableAutoApprove: Bool
let updatedGroupsRemoveMessagesOnKick: Bool
let updatedGroupsAllowHistoricAccessOnInvite: Bool
let updatedGroupsAllowDisplayPicture: Bool
@@ -102,6 +104,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
debugDisappearingMessageDurations: dependencies[feature: .debugDisappearingMessageDurations],
updatedDisappearingMessages: dependencies[feature: .updatedDisappearingMessages],
updatedGroups: dependencies[feature: .updatedGroups],
+ updatedGroupsDisableAutoApprove: dependencies[feature: .updatedGroupsDisableAutoApprove],
updatedGroupsRemoveMessagesOnKick: dependencies[feature: .updatedGroupsRemoveMessagesOnKick],
updatedGroupsAllowHistoricAccessOnInvite: dependencies[feature: .updatedGroupsAllowHistoricAccessOnInvite],
updatedGroupsAllowDisplayPicture: dependencies[feature: .updatedGroupsAllowDisplayPicture],
@@ -257,6 +260,27 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
),
onTap: { self?.updateFlag(for: .updatedGroups, to: !current.updatedGroups) }
),
+ SessionCell.Info(
+ id: .updatedGroupsDisableAutoApprove,
+ title: "Disable Auto Approve",
+ subtitle: """
+ Prevents a group from automatically getting approved if the admin is already approved.
+
+ Note: The default behaviour is to automatically approve new groups if the admin that sent the invitation is an approved contact.
+ """,
+ trailingAccessory: .toggle(
+ .boolValue(
+ current.updatedGroupsDisableAutoApprove,
+ oldValue: (previous ?? current).updatedGroupsDisableAutoApprove
+ )
+ ),
+ onTap: {
+ self?.updateFlag(
+ for: .updatedGroupsDisableAutoApprove,
+ to: !current.updatedGroupsDisableAutoApprove
+ )
+ }
+ ),
SessionCell.Info(
id: .updatedGroupsRemoveMessagesOnKick,
title: "Remove Messages on Kick",
@@ -403,6 +427,7 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
case .updatedDisappearingMessages: updateFlag(for: .updatedDisappearingMessages, to: nil)
case .updatedGroups: updateFlag(for: .updatedGroups, to: nil)
+ case .updatedGroupsDisableAutoApprove: updateFlag(for: .updatedGroupsDisableAutoApprove, to: nil)
case .updatedGroupsRemoveMessagesOnKick: updateFlag(for: .updatedGroupsRemoveMessagesOnKick, to: nil)
case .updatedGroupsAllowHistoricAccessOnInvite:
updateFlag(for: .updatedGroupsAllowHistoricAccessOnInvite, to: nil)
@@ -472,6 +497,13 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
networkCache.currentRequests = [:]
}
+ /// Unsubscribe from push notifications (do this after cancelling pending network requests as we don't want these to be cancelled)
+ if let existingToken: String = dependencies[singleton: .storage, key: .lastRecordedPushToken] {
+ PushNotificationAPI
+ .unsubscribeAll(token: Data(hex: existingToken), using: dependencies)
+ .sinkUntilComplete()
+ }
+
/// Clear the snodeAPI and getSnodePool caches
dependencies.mutate(cache: .snodeAPI) {
$0.snodePool = []
@@ -535,6 +567,9 @@ class DeveloperSettingsViewModel: SessionTableViewModel, NavigatableStateHolder,
onComplete: { [dependencies] _ in
/// Restart the current user poller (there won't be any other pollers though)
dependencies[singleton: .currentUserPoller].start(using: dependencies)
+
+ /// Re-sync the push tokens (if there are any)
+ SyncPushTokensJob.run(uploadOnlyIfStale: false)
},
using: dependencies
)
diff --git a/Session/Shared/UserListViewModel.swift b/Session/Shared/UserListViewModel.swift
index a31eac67f..a66ea4bae 100644
--- a/Session/Shared/UserListViewModel.swift
+++ b/Session/Shared/UserListViewModel.swift
@@ -255,7 +255,7 @@ public enum UserListError: LocalizedError {
public var errorDescription: String? {
switch self {
- case .error(let content): content
+ case .error(let content): return content
}
}
}
diff --git a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift
index 5ce2996c3..d74c7a8de 100644
--- a/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift
+++ b/SessionMessagingKit/Database/Migrations/_014_GenerateInitialUserConfigDumps.swift
@@ -55,13 +55,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
in: config
)
- if config.needsDump {
+ if config.needsDump(using: dependencies) {
try SessionUtil
.createDump(
config: config,
for: .userProfile,
sessionId: userSessionId,
- timestampMs: timestampMs
+ timestampMs: timestampMs,
+ using: dependencies
)?
.upsert(db)
}
@@ -121,13 +122,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
in: config
)
- if config.needsDump {
+ if config.needsDump(using: dependencies) {
try SessionUtil
.createDump(
config: config,
for: .contacts,
sessionId: userSessionId,
- timestampMs: timestampMs
+ timestampMs: timestampMs,
+ using: dependencies
)?
.upsert(db)
}
@@ -146,13 +148,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
in: config
)
- if config.needsDump {
+ if config.needsDump(using: dependencies) {
try SessionUtil
.createDump(
config: config,
for: .convoInfoVolatile,
sessionId: userSessionId,
- timestampMs: timestampMs
+ timestampMs: timestampMs,
+ using: dependencies
)?
.upsert(db)
}
@@ -182,13 +185,14 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
in: config
)
- if config.needsDump {
+ if config.needsDump(using: dependencies) {
try SessionUtil
.createDump(
config: config,
for: .userGroups,
sessionId: userSessionId,
- timestampMs: timestampMs
+ timestampMs: timestampMs,
+ using: dependencies
)?
.upsert(db)
}
diff --git a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift
index b07fde00b..30b6fa3a6 100644
--- a/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift
+++ b/SessionMessagingKit/Messages/Control Messages/Group Update Messages/GroupUpdateInfoChangeMessage.swift
@@ -1,4 +1,6 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
+//
+// stringlint:disable
import Foundation
import GRDB
diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift
index 7fc96e51d..a5bdfac34 100644
--- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift
+++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Groups.swift
@@ -132,7 +132,11 @@ extension MessageReceiver {
/// With updated groups they should be considered message requests (`invited: true`) unless person sending the invitation is
/// an approved contact of the user, this is designed to reduce spam via groups getting around message requests if users are on old
/// or modified clients
- let inviteSenderIsApproved: Bool = ((try? Contact.fetchOne(db, id: sender))?.isApproved == true)
+ let inviteSenderIsApproved: Bool = {
+ guard !dependencies[feature: .updatedGroupsDisableAutoApprove] else { return false }
+
+ return ((try? Contact.fetchOne(db, id: sender))?.isApproved == true)
+ }()
let threadAlreadyExisted: Bool = ((try? SessionThread.exists(db, id: message.groupSessionId.hexString)) ?? false)
let wasKickedFromGroup: Bool = SessionUtil.wasKickedFromGroup(
groupSessionId: message.groupSessionId,
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift
index 14b9e6e37..3a9d87b3e 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift
@@ -37,7 +37,7 @@ internal extension SessionUtil {
serverTimestampMs: Int64,
using dependencies: Dependencies
) throws {
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
// The current users contact data is handled separately so exclude it if it's present (as that's
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift
index c4ea213ea..e41e338db 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift
@@ -19,7 +19,7 @@ internal extension SessionUtil {
in config: Config?,
using dependencies: Dependencies
) throws {
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
// Get the volatile thread info from the conf and local conversations
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift
index a17146674..cd1b51477 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupInfo.swift
@@ -40,7 +40,7 @@ internal extension SessionUtil {
) throws {
typealias GroupData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?)
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
// If the group is destroyed then remove the group date (want to keep the group itself around because
@@ -67,11 +67,16 @@ internal extension SessionUtil {
let groupName: String = String(cString: groupNamePtr)
let groupDesc: String? = groupDescPtr.map { String(cString: $0) }
let formationTimestamp: TimeInterval = TimeInterval(groups_info_get_created(conf))
+
+ // The `displayPic.key` can contain junk data so if the `displayPictureUrl` is null then just
+ // set the `displayPictureKey` to null as well
let displayPic: user_profile_pic = groups_info_get_pic(conf)
let displayPictureUrl: String? = String(libSessionVal: displayPic.url, nullIfEmpty: true)
- let displayPictureKey: Data? = Data(
- libSessionVal: displayPic.key,
- count: DisplayPictureManager.aes256KeyByteLength
+ let displayPictureKey: Data? = (displayPictureUrl == nil ? nil :
+ Data(
+ libSessionVal: displayPic.key,
+ count: DisplayPictureManager.aes256KeyByteLength
+ )
)
// Update the group name
@@ -90,7 +95,7 @@ internal extension SessionUtil {
((existingGroup?.groupDescription == groupDesc) ? nil :
ClosedGroup.Columns.groupDescription.set(to: groupDesc)
),
- ((existingGroup?.formationTimestamp != formationTimestamp && formationTimestamp != 0) ? nil :
+ ((existingGroup?.formationTimestamp == formationTimestamp || formationTimestamp == 0) ? nil :
ClosedGroup.Columns.formationTimestamp.set(to: formationTimestamp)
),
// If we are removing the display picture do so here
@@ -104,7 +109,7 @@ internal extension SessionUtil {
ClosedGroup.Columns.displayPictureEncryptionKey.set(to: nil)
),
(!needsDisplayPictureUpdate || displayPictureUrl != nil ? nil :
- ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: dependencies.dateNow)
+ ClosedGroup.Columns.lastDisplayPictureUpdate.set(to: (serverTimestampMs / 1000))
)
].compactMap { $0 }
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift
index ae6367795..b355bfa52 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+GroupMembers.swift
@@ -29,7 +29,7 @@ internal extension SessionUtil {
serverTimestampMs: Int64,
using dependencies: Dependencies
) throws {
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
// Get the two member sets
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift
index 9a902b852..21742fc1a 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Shared.swift
@@ -79,13 +79,14 @@ internal extension SessionUtil {
if let lastError: SessionUtilError = config?.lastError { throw lastError }
// If we don't need to dump the data the we can finish early
- guard config.needsDump else { return config.needsPush }
+ guard config.needsDump(using: dependencies) else { return config.needsPush }
try SessionUtil.createDump(
config: config,
for: variant,
sessionId: sessionId,
- timestampMs: SnodeAPI.currentOffsetTimestampMs()
+ timestampMs: SnodeAPI.currentOffsetTimestampMs(using: dependencies),
+ using: dependencies
)?.upsert(db)
return config.needsPush
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift
index eed5b6a57..a7ac5ed88 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+SharedGroup.swift
@@ -152,7 +152,8 @@ internal extension SessionUtil {
config: config,
for: variant,
sessionId: SessionId(.group, hex: group.id),
- timestampMs: Int64(floor(group.formationTimestamp * 1000))
+ timestampMs: Int64(floor(group.formationTimestamp * 1000)),
+ using: dependencies
)?.upsert(db)
}
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift
index 5f6d48cf2..525e27c88 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserGroups.swift
@@ -39,7 +39,7 @@ internal extension SessionUtil {
serverTimestampMs: Int64,
using dependencies: Dependencies
) throws {
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
var infiniteLoopGuard: Int = 0
diff --git a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift
index 2e43b2d07..165ee7c06 100644
--- a/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift
+++ b/SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift
@@ -26,7 +26,7 @@ internal extension SessionUtil {
) throws {
typealias ProfileData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?)
- guard config.needsDump else { return }
+ guard config.needsDump(using: dependencies) else { return }
guard case .object(let conf) = config else { throw SessionUtilError.invalidConfigObject }
// A profile must have a name so if this is null then it's invalid and can be ignored
diff --git a/SessionMessagingKit/SessionUtil/SessionUtil.swift b/SessionMessagingKit/SessionUtil/SessionUtil.swift
index 39093566f..bae77dd10 100644
--- a/SessionMessagingKit/SessionUtil/SessionUtil.swift
+++ b/SessionMessagingKit/SessionUtil/SessionUtil.swift
@@ -11,20 +11,6 @@ import SessionUtilitiesKit
public enum SessionUtil {
internal static let logLevel: config_log_level = LOG_LEVEL_INFO
- public struct ConfResult {
- let needsPush: Bool
- let needsDump: Bool
- }
-
- public struct IncomingConfResult {
- let needsPush: Bool
- let needsDump: Bool
- let messageHashes: [String]
- let latestSentTimestamp: TimeInterval
-
- var result: ConfResult { ConfResult(needsPush: needsPush, needsDump: needsDump) }
- }
-
// MARK: - Variables
internal static func syncDedupeId(_ sessionIdHexString: String) -> String {
@@ -296,11 +282,12 @@ public enum SessionUtil {
config: Config?,
for variant: ConfigDump.Variant,
sessionId: SessionId,
- timestampMs: Int64
+ timestampMs: Int64,
+ using dependencies: Dependencies
) throws -> ConfigDump? {
// If it doesn't need a dump then do nothing
guard
- config.needsDump,
+ config.needsDump(using: dependencies),
let dumpData: Data = try config?.dump()
else { return nil }
@@ -384,7 +371,8 @@ public enum SessionUtil {
config: config,
for: variant,
sessionId: sessionId,
- timestampMs: sentTimestamp
+ timestampMs: sentTimestamp,
+ using: dependencies
)
}
}
@@ -499,7 +487,7 @@ public enum SessionUtil {
// Need to check if the config needs to be dumped (this might have changed
// after handling the merge changes)
- guard config.needsDump else {
+ guard config.needsDump(using: dependencies) else {
try ConfigDump
.filter(
ConfigDump.Columns.variant == next.key &&
@@ -517,7 +505,8 @@ public enum SessionUtil {
config: config,
for: next.key,
sessionId: sessionId,
- timestampMs: latestServerTimestampMs
+ timestampMs: latestServerTimestampMs,
+ using: dependencies
)?.upsert(db)
}
catch {
diff --git a/SessionMessagingKit/SessionUtil/Types/Config.swift b/SessionMessagingKit/SessionUtil/Types/Config.swift
index 6f0ec630d..8a710c245 100644
--- a/SessionMessagingKit/SessionUtil/Types/Config.swift
+++ b/SessionMessagingKit/SessionUtil/Types/Config.swift
@@ -51,14 +51,6 @@ public extension SessionUtil {
}
}
- var needsDump: Bool {
- switch self {
- case .invalid: return false
- case .object(let conf): return config_needs_dump(conf)
- case .groupKeys(let conf, _, _): return groups_keys_needs_dump(conf)
- }
- }
-
var lastError: SessionUtilError? {
let maybeErrorString: String? = {
switch self {
@@ -82,6 +74,19 @@ public extension SessionUtil {
// MARK: - Functions
+ func needsDump(using dependencies: Dependencies) -> Bool {
+ return dependencies.mockableValue(
+ key: "needsDump",
+ {
+ switch self {
+ case .invalid: return false
+ case .object(let conf): return config_needs_dump(conf)
+ case .groupKeys(let conf, _, _): return groups_keys_needs_dump(conf)
+ }
+ }()
+ )
+ }
+
func addingLogger() -> Config {
switch self {
case .object(let conf):
@@ -344,13 +349,6 @@ public extension Optional where Wrapped == SessionUtil.Config {
}
}
- var needsDump: Bool {
- switch self {
- case .some(let config): return config.needsDump
- case .none: return false
- }
- }
-
var lastError: SessionUtilError? {
switch self {
case .some(let config): return config.lastError
@@ -360,6 +358,13 @@ public extension Optional where Wrapped == SessionUtil.Config {
// MARK: - Functions
+ func needsDump(using dependencies: Dependencies) -> Bool {
+ switch self {
+ case .some(let config): return config.needsDump(using: dependencies)
+ case .none: return false
+ }
+ }
+
func confirmPushed(seqNo: Int64, hash: String) {
switch self {
case .some(let config): return config.confirmPushed(seqNo: seqNo, hash: hash)
@@ -386,5 +391,14 @@ public extension Optional where Wrapped == SessionUtil.Config {
public extension Atomic where Value == Optional {
var needsPush: Bool { return wrappedValue.needsPush }
- var needsDump: Bool { return wrappedValue.needsDump }
+
+ func needsDump(using dependencies: Dependencies) -> Bool { return wrappedValue.needsDump(using: dependencies) }
+}
+
+// MARK: - Formatting
+
+extension String.StringInterpolation {
+ mutating func appendInterpolation(_ error: SessionUtilError?) {
+ appendLiteral(error.map { "\($0)" } ?? "Unknown Error") // stringlint:disable
+ }
}
diff --git a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift
index c6c9750fe..739867334 100644
--- a/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift
+++ b/SessionMessagingKitTests/Jobs/MessageSendJobSpec.swift
@@ -57,9 +57,9 @@ class MessageSendJobSpec: QuickSpec {
.thenReturn([:])
jobRunner
.when { $0.insert(any(), job: any(), before: any()) }
- .then { args in
- let db: Database = args[0] as! Database
- var job: Job = args[1] as! Job
+ .then { args, untrackedArgs in
+ let db: Database = untrackedArgs[0] as! Database
+ var job: Job = args[0] as! Job
job.id = 1000
try! job.insert(db)
diff --git a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift
index 594fb0ff5..effa3494f 100644
--- a/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift
+++ b/SessionMessagingKitTests/LibSessionUtil/SessionUtilSpec.swift
@@ -55,6 +55,28 @@ class SessionUtilSpec: QuickSpec {
)
}
)
+ @TestState(singleton: .jobRunner, in: dependencies) var mockJobRunner: MockJobRunner! = MockJobRunner(
+ initialSetup: { jobRunner in
+ jobRunner
+ .when { $0.add(any(), job: any(), dependantJob: any(), canStartJob: any(), using: any()) }
+ .thenReturn(nil)
+ }
+ )
+
+ @TestState var createGroupOutput: SessionUtil.CreatedGroupInfo! = {
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.createGroup(
+ db,
+ name: "TestGroup",
+ description: nil,
+ displayPictureUrl: nil,
+ displayPictureFilename: nil,
+ displayPictureEncryptionKey: nil,
+ members: [],
+ using: dependencies
+ )
+ }
+ }()
@TestState(cache: .sessionUtil, in: dependencies) var mockSessionUtilCache: MockSessionUtilCache! = MockSessionUtilCache(
initialSetup: { cache in
var conf: UnsafeMutablePointer!
@@ -64,9 +86,14 @@ class SessionUtilSpec: QuickSpec {
cache.when { $0.setConfig(for: any(), sessionId: any(), to: any()) }.thenReturn(())
cache.when { $0.config(for: .userGroups, sessionId: any()) }
.thenReturn(Atomic(.object(conf)))
+ cache.when { $0.config(for: .groupInfo, sessionId: any()) }
+ .thenReturn(Atomic(createGroupOutput.groupState[.groupInfo]))
+ cache.when { $0.config(for: .groupMembers, sessionId: any()) }
+ .thenReturn(Atomic(createGroupOutput.groupState[.groupMembers]))
+ cache.when { $0.config(for: .groupKeys, sessionId: any()) }
+ .thenReturn(Atomic(createGroupOutput.groupState[.groupKeys]))
}
)
- @TestState var createGroupOutput: SessionUtil.CreatedGroupInfo!
@TestState var userGroupsConfig: SessionUtil.Config!
// MARK: - SessionUtil
@@ -272,7 +299,7 @@ class SessionUtilSpec: QuickSpec {
}
}
- // MARK: - when creating a group
+ // MARK: -- when creating a group
context("when creating a group") {
beforeEach {
var userGroupsConf: UnsafeMutablePointer!
@@ -285,7 +312,7 @@ class SessionUtilSpec: QuickSpec {
.thenReturn(Atomic(userGroupsConfig))
}
- // MARK: -- throws when there is no user ed25519 keyPair
+ // MARK: ---- throws when there is no user ed25519 keyPair
it("throws when there is no user ed25519 keyPair") {
var resultError: Error? = nil
@@ -311,7 +338,7 @@ class SessionUtilSpec: QuickSpec {
expect(resultError).to(matchError(MessageSenderError.noKeyPair))
}
- // MARK: -- throws when it fails to generate a new identity ed25519 keyPair
+ // MARK: ---- throws when it fails to generate a new identity ed25519 keyPair
it("throws when it fails to generate a new identity ed25519 keyPair") {
var resultError: Error? = nil
@@ -338,7 +365,7 @@ class SessionUtilSpec: QuickSpec {
expect(resultError).to(matchError(MessageSenderError.noKeyPair))
}
- // MARK: -- throws when given an invalid member id
+ // MARK: ---- throws when given an invalid member id
it("throws when given an invalid member id") {
var resultError: Error? = nil
@@ -375,7 +402,7 @@ class SessionUtilSpec: QuickSpec {
))
}
- // MARK: -- returns the correct identity keyPair
+ // MARK: ---- returns the correct identity keyPair
it("returns the correct identity keyPair") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -399,7 +426,7 @@ class SessionUtilSpec: QuickSpec {
))
}
- // MARK: -- returns a closed group with the correct data set
+ // MARK: ---- returns a closed group with the correct data set
it("returns a closed group with the correct data set") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -429,7 +456,7 @@ class SessionUtilSpec: QuickSpec {
expect(createGroupOutput.group.invited).to(beFalse())
}
- // MARK: -- returns the members setup correctly
+ // MARK: ---- returns the members setup correctly
it("returns the members setup correctly") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -475,7 +502,7 @@ class SessionUtilSpec: QuickSpec {
]))
}
- // MARK: -- adds the current user as an admin when not provided
+ // MARK: ---- adds the current user as an admin when not provided
it("adds the current user as an admin when not provided") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -503,7 +530,7 @@ class SessionUtilSpec: QuickSpec {
expect(createGroupOutput.members.map { $0.role }).to(contain(.admin))
}
- // MARK: -- handles members without profile data correctly
+ // MARK: ---- handles members without profile data correctly
it("handles members without profile data correctly") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -529,7 +556,7 @@ class SessionUtilSpec: QuickSpec {
expect(createGroupOutput.members.map { $0.role }).to(contain(.standard))
}
- // MARK: -- stores the config states in the cache correctly
+ // MARK: ---- stores the config states in the cache correctly
it("stores the config states in the cache correctly") {
createGroupOutput = mockStorage.write(using: dependencies) { db in
try SessionUtil.createGroup(
@@ -586,9 +613,9 @@ class SessionUtilSpec: QuickSpec {
}
}
- // MARK: - when saving a created a group
+ // MARK: -- when saving a created a group
context("when saving a created a group") {
- // MARK: -- saves config dumps for the stored configs
+ // MARK: ---- saves config dumps for the stored configs
it("saves config dumps for the stored configs") {
mockStorage.write(using: dependencies) { db in
createGroupOutput = try SessionUtil.createGroup(
@@ -630,7 +657,7 @@ class SessionUtilSpec: QuickSpec {
.to(contain([1234567890000]))
}
- // MARK: -- adds the group to the user groups config
+ // MARK: ---- adds the group to the user groups config
it("adds the group to the user groups config") {
mockStorage.write(using: dependencies) { db in
createGroupOutput = try SessionUtil.createGroup(
@@ -663,6 +690,287 @@ class SessionUtilSpec: QuickSpec {
expect(result?.map { $0.timestampMs }.asSet()).to(contain([1234567890000]))
}
}
+
+ // MARK: -- when receiving a GROUP_INFO update
+ context("when receiving a GROUP_INFO update") {
+ @TestState var latestGroup: ClosedGroup?
+ @TestState var initialDisappearingConfig: DisappearingMessagesConfiguration?
+ @TestState var latestDisappearingConfig: DisappearingMessagesConfiguration?
+
+ beforeEach {
+ mockStorage.write(using: dependencies) { db in
+ try SessionThread.fetchOrCreate(
+ db,
+ id: createGroupOutput.group.threadId,
+ variant: .group,
+ shouldBeVisible: true,
+ calledFromConfigHandling: false,
+ using: dependencies
+ )
+ try createGroupOutput.group.insert(db)
+ try createGroupOutput.members.forEach { try $0.insert(db) }
+ initialDisappearingConfig = try DisappearingMessagesConfiguration
+ .fetchOne(db, id: createGroupOutput.group.threadId)
+ .defaulting(
+ to: DisappearingMessagesConfiguration.defaultWith(createGroupOutput.group.threadId)
+ )
+ }
+ }
+
+ // MARK: ---- does nothing if there are no changes
+ it("does nothing if there are no changes") {
+ dependencies.setMockableValue(key: "needsDump", false)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(createGroupOutput.groupState[.groupInfo]).toNot(beNil())
+ expect(createGroupOutput.group).to(equal(latestGroup))
+ }
+
+ // MARK: ---- throws if the config is invalid
+ it("throws if the config is invalid") {
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ expect {
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: .invalid,
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+ .to(throwError())
+ }
+ }
+
+ // MARK: ---- removes group data if the group is destroyed
+ it("removes group data if the group is destroyed") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_destroy_group($0) }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(latestGroup?.authData).to(beNil())
+ expect(latestGroup?.groupIdentityPrivateKey).to(beNil())
+ }
+
+ // MARK: ---- updates the name if it changed
+ it("updates the name if it changed") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map {
+ var updatedName: [CChar] = "UpdatedName".cArray.nullTerminated()
+ groups_info_set_name($0, &updatedName)
+ }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(createGroupOutput.group.name).to(equal("TestGroup"))
+ expect(latestGroup?.name).to(equal("UpdatedName"))
+ }
+
+ // MARK: ---- updates the description if it changed
+ it("updates the description if it changed") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map {
+ var updatedDesc: [CChar] = "UpdatedDesc".cArray.nullTerminated()
+ groups_info_set_description($0, &updatedDesc)
+ }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(createGroupOutput.group.groupDescription).to(beNil())
+ expect(latestGroup?.groupDescription).to(equal("UpdatedDesc"))
+ }
+
+ // MARK: ---- updates the formation timestamp if it changed
+ it("updates the formation timestamp if it changed") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_created($0, 54321) }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(createGroupOutput.group.formationTimestamp).to(equal(1234567890))
+ expect(latestGroup?.formationTimestamp).to(equal(54321))
+ }
+
+ // MARK: ---- and the display picture was changed
+ context("and the display picture was changed") {
+ // MARK: ------ removes the display picture
+ it("removes the display picture") {
+ mockStorage.write(using: dependencies) { db in
+ try ClosedGroup
+ .updateAll(
+ db,
+ ClosedGroup.Columns.displayPictureUrl.set(to: "TestUrl"),
+ ClosedGroup.Columns.displayPictureEncryptionKey.set(to: Data([1, 2, 3])),
+ ClosedGroup.Columns.displayPictureFilename.set(to: "TestFilename")
+ )
+ }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestGroup = mockStorage.read(using: dependencies) { db in
+ try ClosedGroup.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(latestGroup?.displayPictureUrl).to(beNil())
+ expect(latestGroup?.displayPictureEncryptionKey).to(beNil())
+ expect(latestGroup?.displayPictureFilename).to(beNil())
+ expect(latestGroup?.lastDisplayPictureUpdate).to(equal(1234567891))
+ }
+
+ // MARK: ------ schedules a display picture download job if there is a new one
+ it("schedules a display picture download job if there is a new one") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map {
+ var displayPic: user_profile_pic = user_profile_pic()
+ displayPic.url = "https://www.oxen.io/file/1234".toLibSession()
+ displayPic.key = Data(
+ repeating: 1,
+ count: DisplayPictureManager.aes256KeyByteLength
+ ).toLibSession()
+ groups_info_set_pic($0, displayPic)
+ }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ expect(mockJobRunner)
+ .to(call(.exactly(times: 1), matchingParameters: .all) { jobRunner in
+ jobRunner.add(
+ any(),
+ job: Job(
+ variant: .displayPictureDownload,
+ behaviour: .runOnce,
+ shouldBlock: false,
+ shouldBeUnique: true,
+ shouldSkipLaunchBecomeActive: false,
+ details: DisplayPictureDownloadJob.Details(
+ target: .group(
+ id: createGroupOutput.group.threadId,
+ url: "https://www.oxen.io/file/1234",
+ encryptionKey: Data(
+ repeating: 1,
+ count: DisplayPictureManager.aes256KeyByteLength
+ )
+ ),
+ timestamp: 1234567891
+ )
+ ),
+ canStartJob: true,
+ using: any()
+ )
+ })
+ }
+ }
+
+ // MARK: ---- updates the disappearing messages config
+ it("updates the disappearing messages config") {
+ createGroupOutput.groupState[.groupInfo]?.conf.map { groups_info_set_expiry_timer($0, 10) }
+ dependencies.setMockableValue(key: "needsDump", true)
+
+ mockStorage.write(using: dependencies) { db in
+ try SessionUtil.handleGroupInfoUpdate(
+ db,
+ in: createGroupOutput.groupState[.groupInfo],
+ groupSessionId: SessionId(.group, hex: createGroupOutput.group.threadId),
+ serverTimestampMs: 1234567891000,
+ using: dependencies
+ )
+ }
+
+ latestDisappearingConfig = mockStorage.read(using: dependencies) { db in
+ try DisappearingMessagesConfiguration.fetchOne(db, id: createGroupOutput.group.threadId)
+ }
+ expect(initialDisappearingConfig?.isEnabled).to(beFalse())
+ expect(initialDisappearingConfig?.durationSeconds).to(equal(0))
+ expect(latestDisappearingConfig?.isEnabled).to(beTrue())
+ expect(latestDisappearingConfig?.durationSeconds).to(equal(10))
+ }
+ }
+ }
+ }
+}
+
+// MARK: - Convenience
+
+private extension SessionUtil.Config {
+ var conf: UnsafeMutablePointer? {
+ switch self {
+ case .object(let conf): return conf
+ default: return nil
}
}
}
diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift
index 696bc660c..fb3f3cab9 100644
--- a/SessionUtilitiesKit/Database/Storage.swift
+++ b/SessionUtilitiesKit/Database/Storage.swift
@@ -798,7 +798,6 @@ public extension ValueObservation {
// MARK: - Debug Convenience
-#if DEBUG
public extension Storage {
func exportInfo(password: String) throws -> (dbPath: String, keyPath: String) {
var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
@@ -829,4 +828,3 @@ public extension Storage {
)
}
}
-#endif
diff --git a/SessionUtilitiesKit/General/Feature.swift b/SessionUtilitiesKit/General/Feature.swift
index e47e15c65..1ceb12609 100644
--- a/SessionUtilitiesKit/General/Feature.swift
+++ b/SessionUtilitiesKit/General/Feature.swift
@@ -29,6 +29,10 @@ public extension FeatureStorage {
)
)
+ static let updatedGroupsDisableAutoApprove: FeatureConfig = Dependencies.create(
+ identifier: "updatedGroupsDisableAutoApprove"
+ )
+
static let updatedGroupsRemoveMessagesOnKick: FeatureConfig = Dependencies.create(
identifier: "updatedGroupsRemoveMessagesOnKick"
)
diff --git a/_SharedTestUtilities/Mock.swift b/_SharedTestUtilities/Mock.swift
index 2b8a64039..edb47be8a 100644
--- a/_SharedTestUtilities/Mock.swift
+++ b/_SharedTestUtilities/Mock.swift
@@ -28,45 +28,48 @@ public class Mock {
// MARK: - MockFunctionHandler
- @discardableResult internal func mock