[WIP] Initial start on updated groups via configs

Started writing up wrappers and unit tests for group config types
Refactored some duplicate batch & prepared request code to be more generic and reusable
Renamed a number of legacy closed group functions to have the term 'legacy' in them for ease of coding
This commit is contained in:
Morgan Pretty 2023-08-25 17:22:12 +10:00
parent 812a951aba
commit f44b545265
110 changed files with 8230 additions and 3384 deletions

@ -1 +1 @@
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d
Subproject commit 8d9ce6e30153a785b13354c99a9a210d5e8fc1a7

15
Podfile
View File

@ -39,7 +39,6 @@ abstract_target 'GlobalDependencies' do
pod 'SignalCoreKit', git: 'https://github.com/oxen-io/session-ios-core-kit', branch: 'session-version'
target 'SessionNotificationServiceExtension'
target 'SessionSnodeKit'
# Dependencies that are shared across a number of extensions/frameworks but not all
abstract_target 'ExtendedDependencies' do
@ -88,6 +87,20 @@ abstract_target 'GlobalDependencies' do
pod 'Nimble'
end
end
target 'SessionSnodeKit' do
target 'SessionSnodeKitTests' do
inherit! :complete
pod 'Quick'
pod 'Nimble'
# Need to include these for the tests because otherwise it won't actually build
pod 'SAMKeychain'
pod 'YYImage/libwebp', git: 'https://github.com/signalapp/YYImage'
pod 'DifferenceKit'
end
end
end
end

View File

@ -133,6 +133,6 @@ SPEC CHECKSUMS:
xcbeautify: 6e2f57af5c3a86d490376d5758030a8dcc201c1b
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
PODFILE CHECKSUM: a5e8cbfd90e94ce2afb8c687b96bceb93f658e2e
PODFILE CHECKSUM: 61875903156c6d0a9bdd56cb9af69f77648f2009
COCOAPODS: 1.12.1

View File

@ -31,6 +31,7 @@
34D1F0521F7E8EA30066283D /* GiphyDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D1F0511F7E8EA30066283D /* GiphyDownloader.swift */; };
34D99CE4217509C2000AFB39 /* AppEnvironment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34D99CE3217509C1000AFB39 /* AppEnvironment.swift */; };
34F308A21ECB469700BB7697 /* OWSBezierPathView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34F308A11ECB469700BB7697 /* OWSBezierPathView.m */; };
36F011B7CEB4059DD0E31523 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionSnodeKit_SessionSnodeKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 926A0D0D67FD41010FE84169 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionSnodeKit_SessionSnodeKitTests.framework */; };
4503F1BE20470A5B00CEE724 /* classic-quiet.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BB20470A5B00CEE724 /* classic-quiet.aifc */; };
4503F1BF20470A5B00CEE724 /* classic.aifc in Resources */ = {isa = PBXBuildFile; fileRef = 4503F1BC20470A5B00CEE724 /* classic.aifc */; };
450DF2091E0DD2C6003D14BE /* UserNotificationsAdaptee.swift in Sources */ = {isa = PBXBuildFile; fileRef = 450DF2081E0DD2C6003D14BE /* UserNotificationsAdaptee.swift */; };
@ -86,6 +87,7 @@
4CA46F4C219CCC630038ABDE /* CaptionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA46F4B219CCC630038ABDE /* CaptionView.swift */; };
4CA485BB2232339F004B9E7D /* PhotoCaptureViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CA485BA2232339F004B9E7D /* PhotoCaptureViewController.swift */; };
4CC613362227A00400E21A3A /* ConversationSearch.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CC613352227A00400E21A3A /* ConversationSearch.swift */; };
64349178A217B35B0CA8170E /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 20C893ED841B5CA3952827C9 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionSnodeKit.framework */; };
70377AAB1918450100CAF501 /* MobileCoreServices.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 70377AAA1918450100CAF501 /* MobileCoreServices.framework */; };
76C87F19181EFCE600C4ACAB /* MediaPlayer.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 76C87F18181EFCE600C4ACAB /* MediaPlayer.framework */; };
7B0EFDEE274F598600FFAAE7 /* TimestampUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B0EFDED274F598600FFAAE7 /* TimestampUtils.swift */; };
@ -165,7 +167,6 @@
7BFD1A8A2745C4F000FB91B9 /* Permissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A892745C4F000FB91B9 /* Permissions.swift */; };
7BFD1A8C2747150E00FB91B9 /* TurnServerInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7BFD1A8B2747150E00FB91B9 /* TurnServerInfo.swift */; };
7BFD1A972747689000FB91B9 /* Session-Turn-Server in Resources */ = {isa = PBXBuildFile; fileRef = 7BFD1A962747689000FB91B9 /* Session-Turn-Server */; };
9593A1E796C9E6BE2352EA6F /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F8B0BA5257C58DC6FF797278 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionSnodeKit.framework */; };
99978E3F7A80275823CA9014 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 29E827FDF6C1032BB985740C /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; };
A11CD70D17FA230600A2D1B1 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A11CD70C17FA230600A2D1B1 /* QuartzCore.framework */; };
A163E8AB16F3F6AA0094D68B /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A163E8AA16F3F6A90094D68B /* Security.framework */; };
@ -281,7 +282,7 @@
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */; };
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */; };
C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAA8255A57FF00E217F9 /* BuildConfiguration.swift */; };
C32C5A88256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+ClosedGroups.swift */; };
C32C5A88256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C32C5A87256DBCF9003C73A2 /* MessageReceiver+LegacyClosedGroups.swift */; };
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB75255A581000E217F9 /* AppReadiness.m */; };
C32C5C46256DCBB2003C73A2 /* AppReadiness.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDB01255A580700E217F9 /* AppReadiness.h */; settings = {ATTRIBUTES = (Public, ); }; };
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
@ -342,7 +343,7 @@
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = C379DCF3256735770002D4EB /* VisibleMessage+Attachment.swift */; };
C37F5414255BAFA7002AEA92 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
C37F54DC255BB84A002AEA92 /* SessionSnodeKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A59F255385C100C340D1 /* SessionSnodeKit.framework */; };
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+ClosedGroups.swift */; };
C38D5E8D2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38D5E8C2575011E00B6A65C /* MessageSender+LegacyClosedGroups.swift */; };
C38EF00C255B61CC007E1867 /* SignalUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C33FD9AB255A548A00E217F9 /* SignalUtilitiesKit.framework */; };
C38EF22B255B6D5D007E1867 /* ShareViewDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF226255B6D5D007E1867 /* ShareViewDelegate.swift */; };
C38EF22C255B6D5D007E1867 /* OWSVideoPlayer.swift in Sources */ = {isa = PBXBuildFile; fileRef = C38EF227255B6D5D007E1867 /* OWSVideoPlayer.swift */; };

View File

@ -142,6 +142,18 @@
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDB5DAF92A981C42002C8721"
BuildableName = "SessionSnodeKitTests.xctest"
BlueprintName = "SessionSnodeKitTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "YES"

View File

@ -0,0 +1,93 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1400"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A59E255385C100C340D1"
BuildableName = "SessionSnodeKit.framework"
BlueprintName = "SessionSnodeKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "NO"
buildForProfiling = "NO"
buildForArchiving = "NO"
buildForAnalyzing = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDB5DAF92A981C42002C8721"
BuildableName = "SessionSnodeKitTests.xctest"
BlueprintName = "SessionSnodeKitTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES"
testExecutionOrdering = "random">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "FDB5DAF92A981C42002C8721"
BuildableName = "SessionSnodeKitTests.xctest"
BlueprintName = "SessionSnodeKitTests"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
</LaunchAction>
<ProfileAction
buildConfiguration = "App Store Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "C3C2A59E255385C100C340D1"
BuildableName = "SessionSnodeKit.framework"
BlueprintName = "SessionSnodeKit"
ReferencedContainer = "container:Session.xcodeproj">
</BuildableReference>
</MacroExpansion>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "App Store Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -474,11 +474,10 @@ final class EditClosedGroupVC: BaseVC, UITableViewDataSource, UITableViewDelegat
groupPublicKey: threadId,
deleteThread: true
)
}
.flatMap {
MessageSender.update(
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
with: updatedMemberIds,
name: updatedName
)

View File

@ -1212,7 +1212,7 @@ extension ConversationVC:
guard cellViewModel.threadVariant == .community else { return }
Storage.shared
.readPublisher { db -> (OpenGroupAPI.PreparedSendData<OpenGroupAPI.ReactionRemoveAllResponse>, OpenGroupAPI.PendingChange) in
.readPublisher { db -> (HTTP.PreparedRequest<OpenGroupAPI.ReactionRemoveAllResponse>, OpenGroupAPI.PendingChange) in
guard
let openGroup: OpenGroup = try? OpenGroup
.fetchOne(db, id: cellViewModel.threadId),
@ -1223,7 +1223,7 @@ extension ConversationVC:
.fetchOne(db)
else { throw StorageError.objectNotFound }
let sendData: OpenGroupAPI.PreparedSendData<OpenGroupAPI.ReactionRemoveAllResponse> = try OpenGroupAPI
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.ReactionRemoveAllResponse> = try OpenGroupAPI
.preparedReactionDeleteAll(
db,
emoji: emoji,
@ -1240,11 +1240,11 @@ extension ConversationVC:
type: .removeAll
)
return (sendData, pendingChange)
return (preparedRequest, pendingChange)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.flatMap { sendData, pendingChange in
OpenGroupAPI.send(data: sendData)
.flatMap { preparedRequest, pendingChange in
preparedRequest.send(using: dependencies)
.handleEvents(
receiveOutput: { _, response in
OpenGroupManager
@ -1312,7 +1312,7 @@ extension ConversationVC:
typealias OpenGroupInfo = (
pendingReaction: Reaction?,
pendingChange: OpenGroupAPI.PendingChange,
sendData: OpenGroupAPI.PreparedSendData<Int64?>
preparedRequest: HTTP.PreparedRequest<Int64?>
)
/// Perform the sending logic, we generate the pending reaction first in a deferred future closure to prevent the OpenGroup
@ -1399,7 +1399,7 @@ extension ConversationVC:
OpenGroupManager.doesOpenGroupSupport(db, capability: .reactions, on: openGroupServer)
else { throw MessageSenderError.invalidMessage }
let sendData: OpenGroupAPI.PreparedSendData<Int64?> = try {
let preparedRequest: HTTP.PreparedRequest<Int64?> = try {
guard !remove else {
return try OpenGroupAPI
.preparedReactionDelete(
@ -1423,7 +1423,7 @@ extension ConversationVC:
.map { _, response in response.seqNo }
}()
return (nil, (pendingReaction, pendingChange, sendData))
return (nil, (pendingReaction, pendingChange, preparedRequest))
default:
let sendData: MessageSender.PreparedSendData = try MessageSender.preparedSendData(
@ -1463,7 +1463,7 @@ extension ConversationVC:
return MessageSender.sendImmediate(data: sendData, using: dependencies)
case (_, .some(let info)):
return OpenGroupAPI.send(data: info.sendData)
return info.preparedRequest.send(using: dependencies)
.handleEvents(
receiveOutput: { _, seqNo in
OpenGroupManager
@ -1970,7 +1970,7 @@ extension ConversationVC:
on: openGroup.server
)
}
.flatMap { OpenGroupAPI.send(data: $0) }
.flatMap { $0.send(using: dependencies) }
.map { _ in () }
.eraseToAnyPublisher()
) { [weak self] in
@ -2160,7 +2160,7 @@ extension ConversationVC:
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
Storage.shared
.readPublisher { db -> OpenGroupAPI.PreparedSendData<NoResponse> in
.readPublisher { db -> HTTP.PreparedRequest<NoResponse> in
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
throw StorageError.objectNotFound
}
@ -2173,7 +2173,7 @@ extension ConversationVC:
on: openGroup.server
)
}
.flatMap { OpenGroupAPI.send(data: $0) }
.flatMap { $0.send(using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
@ -2215,7 +2215,7 @@ extension ConversationVC:
confirmTitle: "BUTTON_OK".localized(),
cancelStyle: .alert_text,
onConfirm: { [weak self] _ in
Storage.shared
dependencies.storage
.readPublisher { db in
guard let openGroup: OpenGroup = try OpenGroup.fetchOne(db, id: threadId) else {
throw StorageError.objectNotFound
@ -2229,7 +2229,7 @@ extension ConversationVC:
on: openGroup.server
)
}
.flatMap { OpenGroupAPI.send(data: $0) }
.flatMap { $0.send(using: dependencies) }
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(

View File

@ -537,7 +537,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
try SessionUtil
.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
disappearingConfig: updatedConfig
)

View File

@ -1,8 +1,10 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import DifferenceKit
import SessionUIKit
import SignalUtilitiesKit
import SessionUtilitiesKit

View File

@ -53,7 +53,8 @@ public enum SyncPushTokensJob: JobExecutor {
// Unregister from our server
if let existingToken: String = lastRecordedPushToken {
SNLog("[SyncPushTokensJob] Unregister using last recorded push token: \(redact(existingToken))")
return PushNotificationAPI.unsubscribe(token: Data(hex: existingToken))
return PushNotificationAPI
.unsubscribeAll(token: Data(hex: existingToken), using: dependencies)
.map { _ in () }
.eraseToAnyPublisher()
}
@ -86,7 +87,7 @@ public enum SyncPushTokensJob: JobExecutor {
PushRegistrationManager.shared.requestPushTokens()
.flatMap { (pushToken: String, voipToken: String) -> AnyPublisher<Void, Error> in
PushNotificationAPI
.subscribe(
.subscribeAll(
token: Data(hex: pushToken),
isForcedUpdate: true,
using: dependencies

View File

@ -152,7 +152,7 @@ final class NukeDataModal: Modal {
private func clearDeviceOnly() {
ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: false) { [weak self] _ in
ConfigurationSyncJob.run()
ConfigurationSyncJob.run(publicKey: getUserHexEncodedPublicKey())
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.receive(on: DispatchQueue.main)
.sinkUntilComplete(
@ -164,13 +164,16 @@ final class NukeDataModal: Modal {
}
}
private func clearEntireAccount(presentedViewController: UIViewController) {
private func clearEntireAccount(
presentedViewController: UIViewController,
using dependencies: Dependencies = Dependencies()
) {
ModalActivityIndicatorViewController
.present(fromViewController: presentedViewController, canCancel: false) { [weak self] _ in
Publishers
.MergeMany(
Storage.shared
.read { db -> [(String, OpenGroupAPI.PreparedSendData<OpenGroupAPI.DeleteInboxResponse>)] in
.read { db -> [(String, HTTP.PreparedRequest<OpenGroupAPI.DeleteInboxResponse>)] in
return try OpenGroup
.filter(OpenGroup.Columns.isActive == true)
.select(.server)
@ -180,22 +183,22 @@ final class NukeDataModal: Modal {
.map { ($0, try OpenGroupAPI.preparedClearInbox(db, on: $0))}
}
.defaulting(to: [])
.compactMap { server, data in
OpenGroupAPI
.send(data: data)
.compactMap { server, preparedRequest in
preparedRequest
.send(using: dependencies)
.map { _ in [server: true] }
.eraseToAnyPublisher()
}
)
.collect()
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.flatMap { results in
SnodeAPI
.deleteAllMessages(namespace: .all)
.map { results.reduce($0) { result, next in result.updated(with: next) } }
.eraseToAnyPublisher()
}
.receive(on: DispatchQueue.main)
.receive(on: DispatchQueue.main, using: dependencies)
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
@ -257,7 +260,7 @@ final class NukeDataModal: Modal {
if isUsingFullAPNs, let deviceToken: String = maybeDeviceToken {
PushNotificationAPI
.unsubscribe(token: Data(hex: deviceToken))
.unsubscribeAll(token: Data(hex: deviceToken), using: dependencies)
.sinkUntilComplete()
}

View File

@ -14,7 +14,7 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
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 {
guard Identity.fetchUserEd25519KeyPair(db) != nil else {
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
return
}
@ -23,7 +23,7 @@ enum _014_GenerateInitialUserConfigDumps: Migration {
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let timestampMs: Int64 = Int64(Date().timeIntervalSince1970 * 1000)
SessionUtil.loadState(db, userPublicKey: userPublicKey, ed25519SecretKey: secretKey)
SessionUtil.loadState(db)
// Retrieve all threads (we are going to base the config dump data on the active
// threads rather than anything else in the database)

View File

@ -75,7 +75,7 @@ enum _016_DisappearingMessagesConfiguration: Migration {
}
// Update the configs so the settings are synced
_ = try SessionUtil.updatingDisappearingConfigs(db, contactUpdate)
_ = try SessionUtil.updatingDisappearingConfigsOneToOne(db, contactUpdate)
_ = try SessionUtil.batchUpdate(db, disappearingConfigs: legacyGroupUpdate)
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration

View File

@ -0,0 +1,33 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
enum _017_GroupsRebuildChanges: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "GroupsRebuildChanges"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static var requirements: [MigrationRequirement] = [.sessionUtilStateLoaded]
static func migrate(_ db: GRDB.Database) throws {
try db.alter(table: ClosedGroup.self) { t in
t.add(.displayPictureUrl, .text)
t.add(.displayPictureFilename, .text)
t.add(.displayPictureEncryptionKey, .blob)
t.add(.lastDisplayPictureUpdate, .integer)
.notNull()
.defaults(to: 0)
t.add(.groupIdentityPrivateKey, .blob)
t.add(.tag, .blob)
t.add(.subkey, .blob)
t.add(.approved, .boolean)
.notNull()
.defaults(to: true)
}
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}
}

View File

@ -968,6 +968,10 @@ extension Attachment {
return true
}
public static func downloadUrl(for fileId: String) -> String {
return "\(FileServerAPI.server)/file/\(fileId)"
}
public static func fileId(for downloadUrl: String?) -> String? {
return downloadUrl
.map { urlString -> String? in
@ -1042,12 +1046,18 @@ extension Attachment {
internal func upload(
to destination: Attachment.Destination,
using dependencies: Dependencies
) -> AnyPublisher<String?, Error> {
) -> AnyPublisher<String, Error> {
// This can occur if an AttachmnetUploadJob was explicitly created for a message
// dependant on the attachment being uploaded (in this case the attachment has
// already been uploaded so just succeed)
guard state != .uploaded else {
return Just(Attachment.fileId(for: self.downloadUrl))
guard let fileId: String = Attachment.fileId(for: self.downloadUrl) else {
SNLog("Previously uploaded attachment had invalid fileId.")
return Fail(error: AttachmentError.invalidData)
.eraseToAnyPublisher()
}
return Just(fileId)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -1061,8 +1071,8 @@ extension Attachment {
let attachmentId: String = self.id
return Storage.shared
.writePublisher { db -> (OpenGroupAPI.PreparedSendData<FileUploadResponse>?, String?, Data?, Data?) in
return dependencies.storage
.writePublisher { db -> (HTTP.PreparedRequest<FileUploadResponse>?, String?, Data?, Data?) in
// If the attachment is a downloaded attachment, check if it came from
// the server and if so just succeed immediately (no use re-uploading
// an attachment that is already present on the server) - or if we want
@ -1108,7 +1118,7 @@ extension Attachment {
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.uploading))
// We need database access for OpenGroup uploads so generate prepared data
let preparedSendData: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = try {
let preparedRequest: HTTP.PreparedRequest<FileUploadResponse>? = try {
switch destination {
case .openGroup(let openGroup):
return try OpenGroupAPI
@ -1124,13 +1134,13 @@ extension Attachment {
}()
return (
preparedSendData,
preparedRequest,
nil,
(destination.shouldEncrypt ? encryptionKey as Data : nil),
(destination.shouldEncrypt ? digest as Data : nil)
)
}
.flatMap { preparedSendData, existingFileId, encryptionKey, digest -> AnyPublisher<(String?, Data?, Data?), Error> in
.flatMap { preparedRequest, existingFileId, encryptionKey, digest -> AnyPublisher<(String, Data?, Data?), Error> in
// No need to upload if the file was already uploaded
if let fileId: String = existingFileId {
return Just((fileId, encryptionKey, digest))
@ -1140,7 +1150,7 @@ extension Attachment {
switch destination {
case .openGroup:
return OpenGroupAPI.send(data: preparedSendData)
return preparedRequest.send(using: dependencies)
.map { _, response -> (String, Data?, Data?) in (response.id, encryptionKey, digest) }
.eraseToAnyPublisher()
@ -1150,12 +1160,12 @@ extension Attachment {
.eraseToAnyPublisher()
}
}
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String?, Error> in
.flatMap { fileId, encryptionKey, digest -> AnyPublisher<String, Error> in
/// Save the final upload info
///
/// **Note:** We **MUST** use the `.with` function here to ensure the `isValid` flag is
/// updated correctly
Storage.shared
dependencies.storage
.writePublisher { db in
try self
.with(
@ -1165,7 +1175,7 @@ extension Attachment {
self.creationTimestamp ??
(TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
),
downloadUrl: fileId.map { "\(FileServerAPI.server)/file/\($0)" },
downloadUrl: Attachment.downloadUrl(for: fileId),
encryptionKey: encryptionKey,
digest: digest
)
@ -1179,7 +1189,7 @@ extension Attachment {
switch result {
case .finished: break
case .failure:
Storage.shared.write { db in
dependencies.storage.write { db in
try Attachment
.filter(id: attachmentId)
.updateAll(db, Attachment.Columns.state.set(to: Attachment.State.failedUpload))

View File

@ -3,6 +3,7 @@
import Foundation
import GRDB
import DifferenceKit
import SessionSnodeKit
import SessionUtilitiesKit
public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
@ -20,6 +21,16 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
case threadId
case name
case formationTimestamp
case displayPictureUrl
case displayPictureFilename
case displayPictureEncryptionKey
case lastDisplayPictureUpdate
case groupIdentityPrivateKey
case tag
case subkey
case approved
}
/// The Group public key takes up 32 bytes
@ -38,6 +49,34 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
public let name: String
public let formationTimestamp: TimeInterval
/// The URL from which to fetch the groups's display picture.
public let displayPictureUrl: String?
/// The file name of the groups's display picture on local storage.
public let displayPictureFilename: String?
/// The key with which the display picture is encrypted.
public let displayPictureEncryptionKey: Data?
/// The timestamp (in seconds since epoch) that the display picture was last updated
public let lastDisplayPictureUpdate: TimeInterval
/// The private key for performing admin actions on this group
public let groupIdentityPrivateKey: Data?
/// The unique tag for the user within the group
///
/// **Note:** This will be `null` if the `groupIdentityPrivateKey` is set
public let tag: Data?
/// The unique subkey for the user within the group
///
/// **Note:** This will be `null` if the `groupIdentityPrivateKey` is set
public let subkey: Data?
/// A flag indicating whether the user has approved the group invitation
public let approved: Bool
// MARK: - Relationships
public var thread: QueryInterfaceRequest<SessionThread> {
@ -77,11 +116,27 @@ public struct ClosedGroup: Codable, Identifiable, FetchableRecord, PersistableRe
public init(
threadId: String,
name: String,
formationTimestamp: TimeInterval
formationTimestamp: TimeInterval,
displayPictureUrl: String? = nil,
displayPictureFilename: String? = nil,
displayPictureEncryptionKey: Data? = nil,
lastDisplayPictureUpdate: TimeInterval = 0,
groupIdentityPrivateKey: Data? = nil,
tag: Data? = nil,
subkey: Data? = nil,
approved: Bool
) {
self.threadId = threadId
self.name = name
self.formationTimestamp = formationTimestamp
self.displayPictureUrl = displayPictureUrl
self.displayPictureFilename = displayPictureFilename
self.displayPictureEncryptionKey = displayPictureEncryptionKey
self.lastDisplayPictureUpdate = lastDisplayPictureUpdate
self.groupIdentityPrivateKey = groupIdentityPrivateKey
self.tag = tag
self.subkey = subkey
self.approved = approved
}
}
@ -135,7 +190,8 @@ public extension ClosedGroup {
_ db: Database? = nil,
threadIds: [String],
removeGroupData: Bool,
calledFromConfigHandling: Bool
calledFromConfigHandling: Bool,
using dependencies: Dependencies = Dependencies()
) throws {
guard !threadIds.isEmpty else { return }
guard let db: Database = db else {
@ -156,11 +212,12 @@ public extension ClosedGroup {
threadIds.forEach { threadId in
ClosedGroupPoller.shared.stopPolling(for: threadId)
PushNotificationAPI
.unsubscribeFromLegacyGroup(
try? PushNotificationAPI
.preparedUnsubscribeFromLegacyGroup(
legacyGroupId: threadId,
currentUserPublicKey: userPublicKey
)
.send(using: dependencies)
.sinkUntilComplete()
}

View File

@ -21,6 +21,10 @@ public struct ConfigDump: Codable, Equatable, Hashable, FetchableRecord, Persist
case contacts
case convoInfoVolatile
case userGroups
case groupInfo
case groupMembers
case groupKeys
}
/// The type of config this dump is for
@ -63,6 +67,10 @@ public extension ConfigDump.Variant {
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .userGroups: return .userGroups
case .groupInfo: return .groupInfo
case .groupMembers: return .groupMembers
case .groupKeys: return .groupKeys
}
}
@ -72,6 +80,10 @@ public extension ConfigDump.Variant {
case .contacts: return SnodeAPI.Namespace.configContacts
case .convoInfoVolatile: return SnodeAPI.Namespace.configConvoInfoVolatile
case .userGroups: return SnodeAPI.Namespace.configUserGroups
case .groupInfo: return SnodeAPI.Namespace.configGroupInfo
case .groupMembers: return SnodeAPI.Namespace.configGroupMembers
case .groupKeys: return SnodeAPI.Namespace.configGroupKeys
}
}
@ -82,8 +94,8 @@ public extension ConfigDump.Variant {
/// processed (without this we would have to wait until the next poll for it to be processed correctly)
var processingOrder: Int {
switch self {
case .userProfile, .contacts: return 0
case .userGroups: return 1
case .userProfile, .contacts, .groupKeys: return 0
case .userGroups, .groupInfo, .groupMembers: return 1
case .convoInfoVolatile: return 2
}
}

View File

@ -95,7 +95,7 @@ public enum AttachmentDownloadJob: JobExecutor {
else { throw AttachmentDownloadError.invalidUrl }
return Storage.shared
.readPublisher { db -> OpenGroupAPI.PreparedSendData<Data>? in
.readPublisher { db -> HTTP.PreparedRequest<Data>? in
try OpenGroup.fetchOne(db, id: threadId)
.map { openGroup in
try OpenGroupAPI
@ -107,8 +107,8 @@ public enum AttachmentDownloadJob: JobExecutor {
)
}
}
.flatMap { maybePreparedSendData -> AnyPublisher<Data, Error> in
guard let preparedSendData: OpenGroupAPI.PreparedSendData<Data> = maybePreparedSendData else {
.flatMap { maybePreparedRequest -> AnyPublisher<Data, Error> in
guard let preparedRequest: HTTP.PreparedRequest<Data> = maybePreparedRequest else {
return FileServerAPI
.download(
fileId,
@ -117,8 +117,7 @@ public enum AttachmentDownloadJob: JobExecutor {
.eraseToAnyPublisher()
}
return OpenGroupAPI
.send(data: preparedSendData)
return preparedRequest.send(using: dependencies)
.map { _, data in data }
.eraseToAnyPublisher()
}

View File

@ -92,7 +92,7 @@ public enum ConfigurationSyncJob: JobExecutor {
)
}
}
.flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher<HTTP.BatchResponse, Error> in
.flatMap { (changes: [MessageSender.PreparedSendData]) -> AnyPublisher<(ResponseInfoType, HTTP.BatchResponse), Error> in
SnodeAPI
.sendConfigMessages(
changes.compactMap { change in
@ -109,11 +109,11 @@ public enum ConfigurationSyncJob: JobExecutor {
}
.subscribe(on: queue)
.receive(on: queue)
.map { (response: HTTP.BatchResponse) -> [ConfigDump] in
.map { (_: ResponseInfoType, response: HTTP.BatchResponse) -> [ConfigDump] in
/// The number of responses returned might not match the number of changes sent but they will be returned
/// in the same order, this means we can just `zip` the two arrays as it will take the smaller of the two and
/// correctly align the response to the change
zip(response.responses, pendingConfigChanges)
zip(response, pendingConfigChanges)
.compactMap { (subResponse: Decodable, change: SessionUtil.OutgoingConfResult) in
/// If the request wasn't successful then just ignore it (the next time we sync this config we will try
/// to send the changes again)
@ -236,12 +236,15 @@ public extension ConfigurationSyncJob {
)
}
static func run(using dependencies: Dependencies = Dependencies()) -> AnyPublisher<Void, Error> {
static func run(
publicKey: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
// Trigger the job emitting the result when completed
return Deferred {
Future { resolver in
ConfigurationSyncJob.run(
Job(variant: .configurationSync),
Job(variant: .configurationSync, threadId: publicKey),
queue: .global(qos: .userInitiated),
success: { _, _, _ in resolver(Result.success(())) },
failure: { _, error, _, _ in resolver(Result.failure(error ?? HTTPError.generic)) },

View File

@ -0,0 +1,72 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import SessionUtilitiesKit
import SessionSnodeKit
public enum GroupInviteMemberJob: JobExecutor {
public static var maxFailureCount: Int = 1
public static var requiresThreadId: Bool = true
public static var requiresInteractionId: Bool = false
public static func run(
_ job: Job,
queue: DispatchQueue,
success: @escaping (Job, Bool, Dependencies) -> (),
failure: @escaping (Job, Error?, Bool, Dependencies) -> (),
deferred: @escaping (Job, Dependencies) -> (),
using dependencies: Dependencies
) {
guard
let threadId: String = job.threadId,
let detailsData: Data = job.details,
let currentInfo: (groupName: String, adminProfile: Profile) = dependencies.storage.read({ db in
let maybeGroupName: String? = try ClosedGroup
.filter(id: threadId)
.select(.name)
.asRequest(of: String.self)
.fetchOne(db)
guard let groupName: String = maybeGroupName else { throw StorageError.objectNotFound }
return (groupName, Profile.fetchOrCreateCurrentUser(db, using: dependencies))
}),
let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData)
else {
SNLog("[InviteGroupMemberJob] Failing due to missing details")
failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
return
}
let sentTimestamp: Int64 = SnodeAPI.currentOffsetTimestampMs()
let message: GroupUpdateInviteMessage = GroupUpdateInviteMessage(
groupIdentityPublicKey: Data(hex: threadId),
groupName: currentInfo.groupName,
memberSubkey: details.memberSubkey,
memberTag: details.memberTag,
profile: VisibleMessage.VMProfile.init(
profile: currentInfo.adminProfile,
blocksCommunityMessageRequests: nil
),
sentTimestamp: UInt64(sentTimestamp)
)
// TODO: Need to actually send the invite to the recipient
// TODO: Need to batch errors together and send a toast indicating invitation failures
// SnodeAPI
// .SendMessageRequest(message: <#T##SnodeMessage#>, namespace: <#T##SnodeAPI.Namespace#>, subkey: <#T##String?#>, timestampMs: <#T##UInt64#>, ed25519PublicKey: <#T##[UInt8]#>, ed25519SecretKey: <#T##[UInt8]#>)
}
}
// MARK: - GroupInviteMemberJob.Details
extension GroupInviteMemberJob {
public struct Details: Codable {
public let memberSubkey: Data
public let memberTag: Data
}
}

View File

@ -27,12 +27,16 @@ public enum NotifyPushServerJob: JobExecutor {
return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies)
}
PushNotificationAPI
.legacyNotify(
recipient: details.message.recipient,
with: details.message.data,
maxRetryCount: 4
)
dependencies.storage
.readPublisher(using: dependencies) { db in
try PushNotificationAPI.preparedLegacyNotify(
recipient: details.message.recipient,
with: details.message.data,
maxRetryCount: 4,
using: dependencies
)
}
.flatMap { $0.send(using: dependencies) }
.subscribe(on: queue)
.receive(on: queue)
.sinkUntilComplete(

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,128 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
public final class GroupUpdateInviteMessage: ControlMessage {
private enum CodingKeys: String, CodingKey {
case groupIdentityPublicKey
case groupName
case memberSubkey
case memberTag
case profile
}
public var groupIdentityPublicKey: Data
public var groupName: String
public var memberSubkey: Data
public var memberTag: Data
public var profile: VisibleMessage.VMProfile?
// MARK: - Initialization
public init(
groupIdentityPublicKey: Data,
groupName: String,
memberSubkey: Data,
memberTag: Data,
profile: VisibleMessage.VMProfile? = nil,
sentTimestamp: UInt64? = nil
) {
self.groupIdentityPublicKey = groupIdentityPublicKey
self.groupName = groupName
self.memberSubkey = memberSubkey
self.memberTag = memberTag
self.profile = profile
super.init(
sentTimestamp: sentTimestamp
)
}
// MARK: - Codable
required init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
groupIdentityPublicKey = try container.decode(Data.self, forKey: .groupIdentityPublicKey)
groupName = try container.decode(String.self, forKey: .groupName)
memberSubkey = try container.decode(Data.self, forKey: .memberSubkey)
memberTag = try container.decode(Data.self, forKey: .memberTag)
profile = try? container.decode(VisibleMessage.VMProfile.self, forKey: .profile)
try super.init(from: decoder)
}
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(groupIdentityPublicKey, forKey: .groupIdentityPublicKey)
try container.encode(groupName, forKey: .groupName)
try container.encode(memberSubkey, forKey: .memberSubkey)
try container.encode(memberTag, forKey: .memberTag)
try container.encodeIfPresent(profile, forKey: .profile)
}
// MARK: - Proto Conversion
public override class func fromProto(_ proto: SNProtoContent, sender: String) -> GroupUpdateInviteMessage? {
guard let groupInviteMessage = proto.dataMessage?.groupUpdateMessage?.inviteMessage else { return nil }
return GroupUpdateInviteMessage(
groupIdentityPublicKey: groupInviteMessage.groupIdentityPublicKey,
groupName: groupInviteMessage.name,
memberSubkey: groupInviteMessage.memberSubkey,
memberTag: groupInviteMessage.memberTag,
profile: VisibleMessage.VMProfile.fromProto(groupInviteMessage)
)
}
public override func toProto(_ db: Database, threadId: String) -> SNProtoContent? {
do {
let inviteMessageBuilder: SNProtoGroupUpdateInviteMessage.SNProtoGroupUpdateInviteMessageBuilder
// Profile
if let profile = profile, let profileProto: SNProtoGroupUpdateInviteMessage = profile.toProto(groupIdentityPublicKey: groupIdentityPublicKey, name: groupName, memberSubkey: memberSubkey, memberTag: memberTag) {
inviteMessageBuilder = profileProto.asBuilder()
}
else {
inviteMessageBuilder = SNProtoGroupUpdateInviteMessage.builder(
groupIdentityPublicKey: groupIdentityPublicKey,
name: groupName,
memberSubkey: memberSubkey,
memberTag: memberTag
)
}
let groupUpdateMessage = SNProtoGroupUpdateMessage.builder()
groupUpdateMessage.setInviteMessage(try inviteMessageBuilder.build())
let dataMessage = SNProtoDataMessage.builder()
dataMessage.setGroupUpdateMessage(try groupUpdateMessage.build())
let contentProto = SNProtoContent.builder()
contentProto.setDataMessage(try dataMessage.build())
return try contentProto.build()
} catch {
SNLog("Couldn't construct data extraction notification proto from: \(self).")
return nil
}
}
// MARK: - Description
public var description: String {
"""
GroupUpdateInviteMessage(
groupIdentityPublicKey: \(groupIdentityPublicKey),
groupName: \(groupName),
memberSubkey: \(memberSubkey.toHexString()),
memberTag: \(memberTag.toHexString()),
profile: \(profile?.description ?? "null")
)
"""
}
}

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -0,0 +1,3 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation

View File

@ -26,6 +26,10 @@ public final class SharedConfigMessage: ControlMessage {
case contacts
case convoInfoVolatile
case userGroups
case groupInfo
case groupMembers
case groupKeys
public var description: String {
switch self {
@ -33,6 +37,10 @@ public final class SharedConfigMessage: ControlMessage {
case .contacts: return "contacts"
case .convoInfoVolatile: return "convoInfoVolatile"
case .userGroups: return "userGroups"
case .groupInfo: return "groupInfo"
case .groupMembers: return "groupMembers"
case .groupKeys: return "groupKeys"
}
}
}
@ -86,6 +94,10 @@ public final class SharedConfigMessage: ControlMessage {
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .userGroups: return .userGroups
case .groupInfo: return .groupInfo
case .groupMembers: return .groupMembers
case .groupKeys: return .groupKeys
}
}(),
seqNo: sharedConfigMessage.seqno,
@ -102,6 +114,10 @@ public final class SharedConfigMessage: ControlMessage {
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .userGroups: return .userGroups
case .groupInfo: return .groupInfo
case .groupMembers: return .groupMembers
case .groupKeys: return .groupKeys
}
}(),
seqno: self.seqNo,
@ -139,6 +155,10 @@ public extension SharedConfigMessage.Kind {
case .contacts: return .contacts
case .convoInfoVolatile: return .convoInfoVolatile
case .userGroups: return .userGroups
case .groupInfo: return .groupInfo
case .groupMembers: return .groupMembers
case .groupKeys: return .groupKeys
}
}
}

View File

@ -588,7 +588,7 @@ public extension Message {
case let closedGroupControlMessage as ClosedGroupControlMessage:
switch closedGroupControlMessage.kind {
case .encryptionKeyPair:
try MessageReceiver.handleClosedGroupControlMessage(
try MessageReceiver.handleLegacyClosedGroupControlMessage(
db,
threadId: threadId,
threadVariant: threadVariant,

View File

@ -70,6 +70,8 @@ public extension VisibleMessage {
}
}
// MARK: - MessageRequestResponse
public static func fromProto(_ proto: SNProtoMessageRequestResponse) -> VMProfile? {
guard
let profileProto = proto.profile,
@ -107,6 +109,54 @@ public extension VisibleMessage {
}
}
// MARK: - GroupUpdateInviteMessage
public static func fromProto(_ proto: SNProtoGroupUpdateInviteMessage) -> VMProfile? {
guard
let profileProto = proto.profile,
let displayName = profileProto.displayName
else { return nil }
return VMProfile(
displayName: displayName,
profileKey: proto.profileKey,
profilePictureUrl: profileProto.profilePicture
)
}
public func toProto(
groupIdentityPublicKey: Data,
name: String,
memberSubkey: Data,
memberTag: Data
) -> SNProtoGroupUpdateInviteMessage? {
guard let displayName = displayName else {
SNLog("Couldn't construct profile proto from: \(self).")
return nil
}
let groupUpdateProto = SNProtoGroupUpdateInviteMessage.builder(
groupIdentityPublicKey: groupIdentityPublicKey,
name: name,
memberSubkey: memberSubkey,
memberTag: memberTag
)
let profileProto = SNProtoLokiProfile.builder()
profileProto.setDisplayName(displayName)
if let profileKey = profileKey, let profilePictureUrl = profilePictureUrl {
groupUpdateProto.setProfileKey(profileKey)
profileProto.setProfilePicture(profilePictureUrl)
}
do {
groupUpdateProto.setProfile(try profileProto.build())
return try groupUpdateProto.build()
} catch {
SNLog("Couldn't construct profile proto from: \(self).")
return nil
}
}
// MARK: Description
public var description: String {

View File

@ -1,74 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
public extension OpenGroupAPI {
internal struct BatchRequest: Encodable {
let requests: [Child]
init(requests: [ErasedPreparedSendData]) {
self.requests = requests.map { Child(request: $0) }
}
// MARK: - Encodable
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(requests)
}
// MARK: - BatchRequest.Child
struct Child: Encodable {
enum CodingKeys: String, CodingKey {
case method
case path
case headers
case json
case b64
case bytes
}
let request: ErasedPreparedSendData
func encode(to encoder: Encoder) throws {
try request.encodeForBatchRequest(to: encoder)
}
}
}
struct BatchResponse: Decodable {
let info: ResponseInfoType
let data: [Endpoint: Decodable]
public subscript(position: Endpoint) -> Decodable? {
get { return data[position] }
}
public var count: Int { data.count }
public var keys: Dictionary<Endpoint, Decodable>.Keys { data.keys }
public var values: Dictionary<Endpoint, Decodable>.Values { data.values }
// MARK: - Initialization
internal init(
info: ResponseInfoType,
data: [Endpoint: Decodable]
) {
self.info = info
self.data = data
}
public init(from decoder: Decoder) throws {
#if DEBUG
preconditionFailure("The `OpenGroupAPI.BatchResponse` type cannot be decoded directly, this is simply here to allow for `PreparedSendData<OpenGroupAPI.BatchResponse>` support")
#else
info = HTTP.ResponseInfo(code: 0, headers: [:])
data = [:]
#endif
}
}
}

View File

@ -32,7 +32,7 @@ public enum OpenGroupAPI {
hasPerformedInitialPoll: Bool,
timeSinceLastPoll: TimeInterval,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
) throws -> HTTP.PreparedRequest<HTTP.BatchResponseMap<Endpoint>> {
let lastInboxMessageId: Int64 = (try? OpenGroup
.select(.inboxLatestMessageId)
.filter(OpenGroup.Columns.server == server)
@ -58,7 +58,7 @@ public enum OpenGroupAPI {
.fetchAll(db))
.defaulting(to: [])
let preparedRequests: [ErasedPreparedSendData] = [
let preparedRequests: [any ErasedPreparedRequest] = [
try preparedCapabilities(
db,
server: server,
@ -67,7 +67,7 @@ public enum OpenGroupAPI {
].appending(
// Per-room requests
contentsOf: try openGroupRooms
.flatMap { openGroup -> [ErasedPreparedSendData] in
.flatMap { openGroup -> [any ErasedPreparedRequest] in
let shouldRetrieveRecentMessages: Bool = (
openGroup.sequenceNumber == 0 || (
// If it's the first poll for this launch and it's been longer than
@ -126,12 +126,14 @@ public enum OpenGroupAPI {
)
)
return try OpenGroupAPI.preparedBatch(
db,
server: server,
requests: preparedRequests,
using: dependencies
)
return try OpenGroupAPI
.preparedBatch(
db,
server: server,
requests: preparedRequests,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Submits multiple requests wrapped up in a single request, runs them all, then returns the result of each one
@ -144,21 +146,22 @@ public enum OpenGroupAPI {
private static func preparedBatch(
_ db: Database,
server: String,
requests: [ErasedPreparedSendData],
requests: [any ErasedPreparedRequest],
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
) throws -> HTTP.PreparedRequest<HTTP.BatchResponseMap<Endpoint>> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
server: server,
endpoint: .batch,
body: BatchRequest(requests: requests)
body: HTTP.BatchRequest(requests: requests)
),
responseType: BatchResponse.self,
responseType: HTTP.BatchResponseMap<Endpoint>.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// This is like `/batch`, except that it guarantees to perform requests sequentially in the order provided and will stop processing requests
@ -174,21 +177,22 @@ public enum OpenGroupAPI {
private static func preparedSequence(
_ db: Database,
server: String,
requests: [ErasedPreparedSendData],
requests: [any ErasedPreparedRequest],
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
) throws -> HTTP.PreparedRequest<HTTP.BatchResponseMap<Endpoint>> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
server: server,
endpoint: Endpoint.sequence,
body: BatchRequest(requests: requests)
endpoint: .sequence,
body: HTTP.BatchRequest(requests: requests)
),
responseType: BatchResponse.self,
responseType: HTTP.BatchResponseMap<Endpoint>.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Capabilities
@ -205,9 +209,9 @@ public enum OpenGroupAPI {
server: String,
forceBlinded: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Capabilities> {
) throws -> HTTP.PreparedRequest<Capabilities> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -217,6 +221,7 @@ public enum OpenGroupAPI {
forceBlinded: forceBlinded,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Room
@ -228,9 +233,9 @@ public enum OpenGroupAPI {
_ db: Database,
server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Room]> {
) throws -> HTTP.PreparedRequest<[Room]> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -239,6 +244,7 @@ public enum OpenGroupAPI {
responseType: [Room].self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Returns the details of a single room
@ -247,9 +253,9 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Room> {
) throws -> HTTP.PreparedRequest<Room> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -258,6 +264,7 @@ public enum OpenGroupAPI {
responseType: Room.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Polls a room for metadata updates
@ -270,9 +277,9 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<RoomPollInfo> {
) throws -> HTTP.PreparedRequest<RoomPollInfo> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -281,6 +288,7 @@ public enum OpenGroupAPI {
responseType: RoomPollInfo.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
public typealias CapabilitiesAndRoomResponse = (
@ -295,7 +303,7 @@ public enum OpenGroupAPI {
for roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<CapabilitiesAndRoomResponse> {
) throws -> HTTP.PreparedRequest<CapabilitiesAndRoomResponse> {
return try OpenGroupAPI
.preparedSequence(
db,
@ -308,7 +316,8 @@ public enum OpenGroupAPI {
],
using: dependencies
)
.map { (info: ResponseInfoType, response: BatchResponse) -> CapabilitiesAndRoomResponse in
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
.map { (info: ResponseInfoType, response: HTTP.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomResponse in
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (response[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
let maybeRoomResponse: Decodable? = response.data
.first(where: { key, _ in
@ -321,9 +330,9 @@ public enum OpenGroupAPI {
let maybeRoom: HTTP.BatchSubResponse<Room>? = (maybeRoomResponse as? HTTP.BatchSubResponse<Room>)
guard
let capabilitiesInfo: ResponseInfoType = maybeCapabilities?.responseInfo,
let capabilitiesInfo: ResponseInfoType = maybeCapabilities,
let capabilities: Capabilities = maybeCapabilities?.body,
let roomInfo: ResponseInfoType = maybeRoom?.responseInfo,
let roomInfo: ResponseInfoType = maybeRoom,
let room: Room = maybeRoom?.body
else { throw HTTPError.parsingFailed }
@ -345,7 +354,7 @@ public enum OpenGroupAPI {
_ db: Database,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<CapabilitiesAndRoomsResponse> {
) throws -> HTTP.PreparedRequest<CapabilitiesAndRoomsResponse> {
return try OpenGroupAPI
.preparedSequence(
db,
@ -358,7 +367,8 @@ public enum OpenGroupAPI {
],
using: dependencies
)
.map { (info: ResponseInfoType, response: BatchResponse) -> CapabilitiesAndRoomsResponse in
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
.map { (info: ResponseInfoType, response: HTTP.BatchResponseMap<Endpoint>) -> CapabilitiesAndRoomsResponse in
let maybeCapabilities: HTTP.BatchSubResponse<Capabilities>? = (response[.capabilities] as? HTTP.BatchSubResponse<Capabilities>)
let maybeRooms: HTTP.BatchSubResponse<[Room]>? = response.data
.first(where: { key, _ in
@ -370,9 +380,9 @@ public enum OpenGroupAPI {
.map { _, value in value as? HTTP.BatchSubResponse<[Room]> }
guard
let capabilitiesInfo: ResponseInfoType = maybeCapabilities?.responseInfo,
let capabilitiesInfo: ResponseInfoType = maybeCapabilities,
let capabilities: Capabilities = maybeCapabilities?.body,
let roomsInfo: ResponseInfoType = maybeRooms?.responseInfo,
let roomsInfo: ResponseInfoType = maybeRooms,
let rooms: [Room] = maybeRooms?.body
else { throw HTTPError.parsingFailed }
@ -395,13 +405,17 @@ public enum OpenGroupAPI {
whisperMods: Bool,
fileIds: [String]?,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Message> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
throw OpenGroupAPIError.signingFailed
}
) throws -> HTTP.PreparedRequest<Message> {
let signResult: (publicKey: String, signature: Bytes) = try sign(
db,
messageBytes: plaintext.bytes,
for: server,
fallbackSigningType: .standard,
using: dependencies
)
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -418,6 +432,7 @@ public enum OpenGroupAPI {
responseType: Message.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Returns a single message by ID
@ -427,9 +442,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Message> {
) throws -> HTTP.PreparedRequest<Message> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -438,6 +453,7 @@ public enum OpenGroupAPI {
responseType: Message.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Edits a message, replacing its existing content with new content and a new signature
@ -451,13 +467,17 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: plaintext.bytes, for: server, fallbackSigningType: .standard, using: dependencies) else {
throw OpenGroupAPIError.signingFailed
}
) throws -> HTTP.PreparedRequest<NoResponse> {
let signResult: (publicKey: String, signature: Bytes) = try sign(
db,
messageBytes: plaintext.bytes,
for: server,
fallbackSigningType: .standard,
using: dependencies
)
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .put,
@ -472,6 +492,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Remove a message by its message id
@ -481,9 +502,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .delete,
@ -493,6 +514,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Retrieves recent messages posted to this room
@ -505,9 +527,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -520,6 +542,7 @@ public enum OpenGroupAPI {
responseType: [Failable<Message>].self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Retrieves messages from the room preceding a given id.
@ -534,9 +557,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -549,6 +572,7 @@ public enum OpenGroupAPI {
responseType: [Failable<Message>].self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Retrieves message updates from a room. This is the main message polling endpoint in SOGS.
@ -563,9 +587,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[Failable<Message>]> {
) throws -> HTTP.PreparedRequest<[Failable<Message>]> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -578,6 +602,7 @@ public enum OpenGroupAPI {
responseType: [Failable<Message>].self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Deletes all messages from a given sessionId within the provided rooms (or globally) on a server
@ -599,9 +624,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .delete,
@ -611,6 +636,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Reactions
@ -623,7 +649,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
@ -631,7 +657,7 @@ public enum OpenGroupAPI {
}
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .get,
@ -641,6 +667,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Adds a reaction to the given message in this room. The user must have read access in the room.
@ -654,7 +681,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionAddResponse> {
) throws -> HTTP.PreparedRequest<ReactionAddResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
@ -662,7 +689,7 @@ public enum OpenGroupAPI {
}
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .put,
@ -672,6 +699,7 @@ public enum OpenGroupAPI {
responseType: ReactionAddResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Removes a reaction from a post this room. The user must have read access in the room. This only removes the user's own reaction
@ -683,7 +711,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionRemoveResponse> {
) throws -> HTTP.PreparedRequest<ReactionRemoveResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
@ -691,7 +719,7 @@ public enum OpenGroupAPI {
}
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .delete,
@ -701,6 +729,7 @@ public enum OpenGroupAPI {
responseType: ReactionRemoveResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Removes all reactions of all users from a post in this room. The calling must have moderator permissions in the room. This endpoint
@ -713,7 +742,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<ReactionRemoveAllResponse> {
) throws -> HTTP.PreparedRequest<ReactionRemoveAllResponse> {
/// URL(String:) won't convert raw emojis, so need to do a little encoding here.
/// The raw emoji will come back when calling url.path
guard let encodedEmoji: String = emoji.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) else {
@ -721,7 +750,7 @@ public enum OpenGroupAPI {
}
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .delete,
@ -731,6 +760,7 @@ public enum OpenGroupAPI {
responseType: ReactionRemoveAllResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Pinning
@ -751,9 +781,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .post,
@ -763,6 +793,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Remove a message from this room's pinned message list
@ -774,9 +805,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .post,
@ -786,6 +817,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Removes _all_ pinned messages from this room
@ -796,9 +828,9 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .post,
@ -808,6 +840,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Files
@ -825,9 +858,9 @@ public enum OpenGroupAPI {
to roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<FileUploadResponse> {
) throws -> HTTP.PreparedRequest<FileUploadResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -845,6 +878,7 @@ public enum OpenGroupAPI {
timeout: FileServerAPI.fileUploadTimeout,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Retrieves a file uploaded to the room.
@ -857,9 +891,9 @@ public enum OpenGroupAPI {
from roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<Data> {
) throws -> HTTP.PreparedRequest<Data> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -869,6 +903,7 @@ public enum OpenGroupAPI {
timeout: FileServerAPI.fileDownloadTimeout,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Inbox/Outbox (Message Requests)
@ -880,9 +915,9 @@ public enum OpenGroupAPI {
_ db: Database,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
) throws -> HTTP.PreparedRequest<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -891,6 +926,7 @@ public enum OpenGroupAPI {
responseType: [DirectMessage]?.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages
@ -901,9 +937,9 @@ public enum OpenGroupAPI {
id: Int64,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
) throws -> HTTP.PreparedRequest<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -912,6 +948,7 @@ public enum OpenGroupAPI {
responseType: [DirectMessage]?.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Remove all message requests from inbox, this methrod will return the number of messages deleted
@ -919,9 +956,9 @@ public enum OpenGroupAPI {
_ db: Database,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<DeleteInboxResponse> {
) throws -> HTTP.PreparedRequest<DeleteInboxResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
method: .delete,
@ -931,6 +968,7 @@ public enum OpenGroupAPI {
responseType: DeleteInboxResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Delivers a direct message to a user via their blinded Session ID
@ -942,9 +980,9 @@ public enum OpenGroupAPI {
toInboxFor blindedSessionId: String,
on server: String,
using dependencies: Dependencies
) throws -> PreparedSendData<SendDirectMessageResponse> {
) throws -> HTTP.PreparedRequest<SendDirectMessageResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -957,6 +995,7 @@ public enum OpenGroupAPI {
responseType: SendDirectMessageResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Retrieves all of the user's sent DMs (up to limit)
@ -966,9 +1005,9 @@ public enum OpenGroupAPI {
_ db: Database,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
) throws -> HTTP.PreparedRequest<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -977,6 +1016,7 @@ public enum OpenGroupAPI {
responseType: [DirectMessage]?.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages
@ -987,9 +1027,9 @@ public enum OpenGroupAPI {
id: Int64,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<[DirectMessage]?> {
) throws -> HTTP.PreparedRequest<[DirectMessage]?> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request<NoBody, Endpoint>(
server: server,
@ -998,6 +1038,7 @@ public enum OpenGroupAPI {
responseType: [DirectMessage]?.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Users
@ -1040,9 +1081,9 @@ public enum OpenGroupAPI {
from roomTokens: [String]? = nil,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -1057,6 +1098,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Removes a user ban from specific rooms, or from the server globally
@ -1089,9 +1131,9 @@ public enum OpenGroupAPI {
from roomTokens: [String]?,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -1105,6 +1147,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// Appoints or removes a moderator or admin
@ -1167,13 +1210,13 @@ public enum OpenGroupAPI {
for roomTokens: [String]?,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<NoResponse> {
) throws -> HTTP.PreparedRequest<NoResponse> {
guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else {
throw HTTPError.generic
}
return try OpenGroupAPI
.prepareSendData(
.prepareRequest(
db,
request: Request(
method: .post,
@ -1190,6 +1233,7 @@ public enum OpenGroupAPI {
responseType: NoResponse.self,
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
/// This is a convenience method which constructs a `/sequence` of the `userBan` and `userDeleteMessages` requests, refer to those
@ -1200,7 +1244,7 @@ public enum OpenGroupAPI {
in roomToken: String,
on server: String,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<BatchResponse> {
) throws -> HTTP.PreparedRequest<HTTP.BatchResponseMap<Endpoint>> {
return try OpenGroupAPI
.preparedSequence(
db,
@ -1223,6 +1267,7 @@ public enum OpenGroupAPI {
],
using: dependencies
)
.signed(db, with: OpenGroupAPI.signRequest, using: dependencies)
}
// MARK: - Authentication
@ -1235,7 +1280,7 @@ public enum OpenGroupAPI {
fallbackSigningType signingType: SessionId.Prefix,
forceBlinded: Bool = false,
using dependencies: Dependencies
) -> (publicKey: String, signature: Bytes)? {
) throws -> (publicKey: String, signature: Bytes) {
guard
let userEdKeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db),
let serverPublicKey: String = try? OpenGroup
@ -1243,7 +1288,7 @@ public enum OpenGroupAPI {
.filter(OpenGroup.Columns.server == serverName.lowercased())
.asRequest(of: String.self)
.fetchOne(db)
else { return nil }
else { throw OpenGroupAPIError.signingFailed }
let capabilities: Set<Capability.Variant> = (try? Capability
.select(.variant)
@ -1261,7 +1306,7 @@ public enum OpenGroupAPI {
let signatureResult: Bytes = try? dependencies.crypto.perform(
.sogsSignature(message: messageBytes, secretKey: userEdKeyPair.secretKey, blindedSecretKey: blindedKeyPair.secretKey, blindedPublicKey: blindedKeyPair.publicKey)
)
else { return nil }
else { throw OpenGroupAPIError.signingFailed }
return (
publicKey: SessionId(.blinded15, publicKey: blindedKeyPair.publicKey).hexString,
@ -1276,7 +1321,7 @@ public enum OpenGroupAPI {
let signatureResult: Bytes = try? dependencies.crypto.perform(
.signature(message: messageBytes, secretKey: userEdKeyPair.secretKey)
)
else { return nil }
else { throw OpenGroupAPIError.signingFailed }
return (
publicKey: SessionId(.unblinded, publicKey: userEdKeyPair.publicKey).hexString,
@ -1290,7 +1335,7 @@ public enum OpenGroupAPI {
let signatureResult: Bytes = try? dependencies.crypto.perform(
.signEd25519(data: messageBytes, keyPair: userKeyPair)
)
else { return nil }
else { throw OpenGroupAPIError.signingFailed }
return (
publicKey: SessionId(.standard, publicKey: userKeyPair.publicKey).hexString,
@ -1300,20 +1345,24 @@ public enum OpenGroupAPI {
}
/// Sign a request to be sent to SOGS (handles both un-blinded and blinded signing based on the server capabilities)
private static func sign(
private static func signRequest<R>(
_ db: Database,
request: URLRequest,
for serverName: String,
with serverPublicKey: String,
forceBlinded: Bool = false,
preparedRequest: HTTP.PreparedRequest<R>,
using dependencies: Dependencies = Dependencies()
) -> URLRequest? {
guard let url: URL = request.url else { return nil }
) throws -> URLRequest {
guard
let url: URL = preparedRequest.request.url,
let serverPublicKey: String = try? OpenGroup
.select(.publicKey)
.filter(OpenGroup.Columns.server == preparedRequest.server.lowercased())
.asRequest(of: String.self)
.fetchOne(db)
else { throw OpenGroupAPIError.signingFailed }
var updatedRequest: URLRequest = request
var updatedRequest: URLRequest = preparedRequest.request
let path: String = url.path
.appending(url.query.map { value in "?\(value)" })
let method: String = (request.httpMethod ?? "GET")
let method: String = preparedRequest.method.rawValue
let timestamp: Int = Int(floor(dependencies.dateNow.timeIntervalSince1970))
let serverPublicKeyData: Data = Data(hex: serverPublicKey)
@ -1321,11 +1370,11 @@ public enum OpenGroupAPI {
!serverPublicKeyData.isEmpty,
let nonce: Data = (try? dependencies.crypto.perform(.generateNonce16())).map({ Data($0) }),
let timestampBytes: Bytes = "\(timestamp)".data(using: .ascii)?.bytes
else { return nil }
else { throw OpenGroupAPIError.signingFailed }
/// Get a hash of any body content
let bodyHash: Bytes? = {
guard let body: Data = request.httpBody else { return nil }
guard let body: Data = preparedRequest.request.httpBody else { return nil }
return try? dependencies.crypto.perform(.hash(message: body.bytes, outputLength: 64))
}()
@ -1346,11 +1395,16 @@ public enum OpenGroupAPI {
.appending(contentsOf: bodyHash ?? [])
/// Sign the above message
guard let signResult: (publicKey: String, signature: Bytes) = sign(db, messageBytes: messageBytes, for: serverName, fallbackSigningType: .unblinded, forceBlinded: forceBlinded, using: dependencies) else {
return nil
}
let signResult: (publicKey: String, signature: Bytes) = try sign(
db,
messageBytes: messageBytes,
for: preparedRequest.server,
fallbackSigningType: .unblinded,
forceBlinded: ((preparedRequest.metadata[.forceBlinded] as? Bool) == true),
using: dependencies
)
updatedRequest.allHTTPHeaderFields = (request.allHTTPHeaderFields ?? [:])
updatedRequest.allHTTPHeaderFields = (preparedRequest.request.allHTTPHeaderFields ?? [:])
.updated(with: [
HTTPHeader.sogsPubKey: signResult.publicKey,
HTTPHeader.sogsTimestamp: "\(timestamp)",
@ -1363,18 +1417,17 @@ public enum OpenGroupAPI {
// MARK: - Convenience
/// Takes the reuqest information and generates a signed `PreparedSendData<R>` pbject which is ready for sending to the API, this
/// Takes the reuqest information and generates a `PreparedRequest<R, Endpoint>` pbject which is ready for sending to the API, this
/// method is mainly here so we can separate the preparation of a request, which requires access to the database for signing, from the
/// actual sending of the reuqest to ensure we don't run into any unexpected blocking of the database write thread
private static func prepareSendData<T: Encodable, R: Decodable>(
private static func prepareRequest<T: Encodable, R: Decodable>(
_ db: Database,
request: Request<T, Endpoint>,
responseType: R.Type,
forceBlinded: Bool = false,
timeout: TimeInterval = HTTP.defaultTimeout,
using dependencies: Dependencies = Dependencies()
) throws -> PreparedSendData<R> {
let urlRequest: URLRequest = try request.generateUrlRequest()
) throws -> HTTP.PreparedRequest<R> {
let maybePublicKey: String? = try? OpenGroup
.select(.publicKey)
.filter(OpenGroup.Columns.server == request.server.lowercased())
@ -1383,40 +1436,13 @@ public enum OpenGroupAPI {
guard let publicKey: String = maybePublicKey else { throw OpenGroupAPIError.noPublicKey }
// Attempt to sign the request with the new auth
guard let signedRequest: URLRequest = sign(db, request: urlRequest, for: request.server, with: publicKey, forceBlinded: forceBlinded, using: dependencies) else {
throw OpenGroupAPIError.signingFailed
}
return PreparedSendData(
return HTTP.PreparedRequest(
request: request,
urlRequest: signedRequest,
urlRequest: try request.generateUrlRequest(),
publicKey: publicKey,
responseType: responseType,
metadata: [.forceBlinded: forceBlinded],
timeout: timeout
)
}
/// This method takes in the `PreparedSendData<R>` and actually sends it to the API
public static func send<R>(
data: PreparedSendData<R>?,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<(ResponseInfoType, R), Error> {
guard let validData: PreparedSendData<R> = data else {
return Fail(error: OpenGroupAPIError.invalidPreparedData)
.eraseToAnyPublisher()
}
return dependencies.network
.send(
.onionRequest(
validData.request,
to: validData.server,
with: validData.publicKey,
timeout: validData.timeout
)
)
.decoded(with: validData, using: dependencies)
.eraseToAnyPublisher()
}
}

View File

@ -259,7 +259,7 @@ public final class OpenGroupManager {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.flatMap { info, response -> Future<Void, Error> in
Future<Void, Error> { resolver in
dependencies.storage.write { db in
@ -993,14 +993,14 @@ public final class OpenGroupManager {
// Try to retrieve the default rooms 8 times
let publisher: AnyPublisher<[DefaultRoomInfo], Error> = dependencies.storage
.readPublisher { db -> OpenGroupAPI.PreparedSendData<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
.readPublisher { db -> HTTP.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse> in
try OpenGroupAPI.preparedCapabilitiesAndRooms(
db,
on: OpenGroupAPI.defaultServer,
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.subscribe(on: OpenGroupAPI.workQueue, using: dependencies)
.receive(on: OpenGroupAPI.workQueue, using: dependencies)
.retry(8, using: dependencies)
@ -1129,7 +1129,7 @@ public final class OpenGroupManager {
DispatchQueue.global(qos: .background).async(using: dependencies) {
// Hold on to the publisher until it has completed at least once
dependencies.storage
.readPublisher { db -> (Data?, OpenGroupAPI.PreparedSendData<Data>?) in
.readPublisher { db -> (Data?, HTTP.PreparedRequest<Data>?) in
if canUseExistingImage {
let maybeExistingData: Data? = try? OpenGroup
.select(.imageData)
@ -1161,8 +1161,8 @@ public final class OpenGroupManager {
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
case (_, .some(let sendData)):
return OpenGroupAPI.send(data: sendData, using: dependencies)
case (_, .some(let preparedRequest)):
return preparedRequest.send(using: dependencies)
.map { _, imageData in imageData }
.eraseToAnyPublisher()

View File

@ -7,7 +7,6 @@ public enum OpenGroupAPIError: LocalizedError {
case signingFailed
case noPublicKey
case invalidEmoji
case invalidPreparedData
case invalidPoll
public var errorDescription: String? {
@ -16,7 +15,6 @@ public enum OpenGroupAPIError: LocalizedError {
case .signingFailed: return "Couldn't sign message."
case .noPublicKey: return "Couldn't find server public key."
case .invalidEmoji: return "The emoji is invalid."
case .invalidPreparedData: return "Invalid PreparedSendData provided."
case .invalidPoll: return "Poller in invalid state."
}
}

View File

@ -0,0 +1,8 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
public extension HTTPRequestMetadata {
static let forceBlinded: HTTPRequestMetadata = "forceBlinded"
}

View File

@ -1,243 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
// MARK: - ErasedPreparedSendData
public protocol ErasedPreparedSendData {
var endpoint: OpenGroupAPI.Endpoint { get }
var batchResponseTypes: [Decodable.Type] { get }
func encodeForBatchRequest(to encoder: Encoder) throws
}
// MARK: - PreparedSendData<R>
public extension OpenGroupAPI {
struct PreparedSendData<R>: ErasedPreparedSendData {
internal let request: URLRequest
internal let server: String
internal let publicKey: String
internal let originalType: Decodable.Type
internal let responseType: R.Type
internal let timeout: TimeInterval
fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R)
// The following types are needed for `BatchRequest` handling
private let method: HTTPMethod
private let path: String
public let endpoint: Endpoint
internal let batchEndpoints: [Endpoint]
public let batchResponseTypes: [Decodable.Type]
/// The `jsonBodyEncoder` is used to simplify the encoding for `BatchRequest`
private let jsonBodyEncoder: ((inout KeyedEncodingContainer<BatchRequest.Child.CodingKeys>, BatchRequest.Child.CodingKeys) throws -> ())?
private let b64: String?
private let bytes: [UInt8]?
internal init<T: Encodable>(
request: Request<T, Endpoint>,
urlRequest: URLRequest,
publicKey: String,
responseType: R.Type,
timeout: TimeInterval
) where R: Decodable {
self.request = urlRequest
self.server = request.server
self.publicKey = publicKey
self.originalType = responseType
self.responseType = responseType
self.timeout = timeout
self.responseConverter = { _, response in
guard let validResponse: R = response as? R else { throw HTTPError.invalidResponse }
return validResponse
}
// The following data is needed in this type for handling batch requests
self.method = request.method
self.endpoint = request.endpoint
self.path = request.urlPathAndParamsString
self.batchEndpoints = ((request.body as? BatchRequest)?
.requests
.map { $0.request.endpoint })
.defaulting(to: [])
self.batchResponseTypes = ((request.body as? BatchRequest)?
.requests
.flatMap { $0.request.batchResponseTypes })
.defaulting(to: [HTTP.BatchSubResponse<R>.self])
// Note: Need to differentiate between JSON, b64 string and bytes body values to ensure
// they are encoded correctly so the server knows how to handle them
switch request.body {
case let bodyString as String:
self.jsonBodyEncoder = nil
self.b64 = bodyString
self.bytes = nil
case let bodyBytes as [UInt8]:
self.jsonBodyEncoder = nil
self.b64 = nil
self.bytes = bodyBytes
default:
self.jsonBodyEncoder = { [body = request.body] container, key in
try container.encodeIfPresent(body, forKey: key)
}
self.b64 = nil
self.bytes = nil
}
}
private init<U: Decodable>(
request: URLRequest,
server: String,
publicKey: String,
originalType: U.Type,
responseType: R.Type,
timeout: TimeInterval,
responseConverter: @escaping (ResponseInfoType, Any) throws -> R,
method: HTTPMethod,
endpoint: Endpoint,
path: String,
batchEndpoints: [Endpoint],
batchResponseTypes: [Decodable.Type],
jsonBodyEncoder: ((inout KeyedEncodingContainer<BatchRequest.Child.CodingKeys>, BatchRequest.Child.CodingKeys) throws -> ())?,
b64: String?,
bytes: [UInt8]?
) {
self.request = request
self.server = server
self.publicKey = publicKey
self.originalType = originalType
self.responseType = responseType
self.timeout = timeout
self.responseConverter = responseConverter
// The following data is needed in this type for handling batch requests
self.method = method
self.endpoint = endpoint
self.path = path
self.batchEndpoints = batchEndpoints
self.batchResponseTypes = batchResponseTypes
self.jsonBodyEncoder = jsonBodyEncoder
self.b64 = b64
self.bytes = bytes
}
// MARK: - ErasedPreparedSendData
public func encodeForBatchRequest(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<BatchRequest.Child.CodingKeys> = encoder.container(keyedBy: BatchRequest.Child.CodingKeys.self)
// Exclude request signature headers (not used for sub-requests)
let batchRequestHeaders: [String: String] = (request.allHTTPHeaderFields ?? [:])
.filter { key, _ in
key.lowercased() != HTTPHeader.sogsPubKey.lowercased() &&
key.lowercased() != HTTPHeader.sogsTimestamp.lowercased() &&
key.lowercased() != HTTPHeader.sogsNonce.lowercased() &&
key.lowercased() != HTTPHeader.sogsSignature.lowercased()
}
if !batchRequestHeaders.isEmpty {
try container.encode(batchRequestHeaders, forKey: .headers)
}
try container.encode(method, forKey: .method)
try container.encode(path, forKey: .path)
try jsonBodyEncoder?(&container, .json)
try container.encodeIfPresent(b64, forKey: .b64)
try container.encodeIfPresent(bytes, forKey: .bytes)
}
}
}
public extension OpenGroupAPI.PreparedSendData {
func map<O>(transform: @escaping (ResponseInfoType, R) throws -> O) -> OpenGroupAPI.PreparedSendData<O> {
return OpenGroupAPI.PreparedSendData(
request: request,
server: server,
publicKey: publicKey,
originalType: originalType,
responseType: O.self,
timeout: timeout,
responseConverter: { info, response in
let validResponse: R = try responseConverter(info, response)
return try transform(info, validResponse)
},
method: method,
endpoint: endpoint,
path: path,
batchEndpoints: batchEndpoints,
batchResponseTypes: batchResponseTypes,
jsonBodyEncoder: jsonBodyEncoder,
b64: b64,
bytes: bytes
)
}
}
// MARK: - Convenience
public extension Publisher where Output == (ResponseInfoType, Data?), Failure == Error {
func decoded<R>(
with preparedData: OpenGroupAPI.PreparedSendData<R>,
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, R), Error> {
self
.tryMap { responseInfo, maybeData -> (ResponseInfoType, R) in
// Depending on the 'originalType' we need to process the response differently
let targetData: Any = try {
switch preparedData.originalType {
case is OpenGroupAPI.BatchResponse.Type:
let responses: [Decodable] = try HTTP.BatchResponse.decodingResponses(
from: maybeData,
as: preparedData.batchResponseTypes,
requireAllResults: true,
using: dependencies
)
return OpenGroupAPI.BatchResponse(
info: responseInfo,
data: Swift.zip(preparedData.batchEndpoints, responses)
.reduce(into: [:]) { result, next in
result[next.0] = next.1
}
)
case is NoResponse.Type: return NoResponse()
case is Optional<Data>.Type: return maybeData as Any
case is Data.Type: return try maybeData ?? { throw HTTPError.parsingFailed }()
case is _OptionalProtocol.Type:
guard let data: Data = maybeData else { return maybeData as Any }
return try preparedData.originalType.decoded(from: data, using: dependencies)
default:
guard let data: Data = maybeData else { throw HTTPError.parsingFailed }
return try preparedData.originalType.decoded(from: data, using: dependencies)
}
}()
// Generate and return the converted data
let convertedData: R = try preparedData.responseConverter(responseInfo, targetData)
return (responseInfo, convertedData)
}
.eraseToAnyPublisher()
}
}
// MARK: - _OptionalProtocol
/// This protocol should only be used within this file and is used to distinguish between `Any.Type` and `Optional<Any>.Type` as
/// it seems that `is Optional<Any>.Type` doesn't work nicely but this protocol works nicely as long as the case is under any explicit
/// `Optional<T>` handling that we need
private protocol _OptionalProtocol {}
extension Optional: _OptionalProtocol {}

View File

@ -59,6 +59,11 @@ extension OpenGroupAPI {
case userUnban(String)
case userModerator(String)
public static var batchRequestVariant: HTTP.BatchRequest.Child.Variant = .sogs
public static var excludedSubRequestHeaders: [HTTPHeader] = [
.sogsPubKey, .sogsTimestamp, .sogsNonce, .sogsSignature
]
public var path: String {
switch self {
// Utility

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -218,6 +218,13 @@ extension WebSocketProtos_WebSocketMessage.TypeEnum: CaseIterable {
#endif // swift(>=4.2)
#if swift(>=5.5) && canImport(_Concurrency)
extension WebSocketProtos_WebSocketRequestMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketResponseMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketMessage: @unchecked Sendable {}
extension WebSocketProtos_WebSocketMessage.TypeEnum: @unchecked Sendable {}
#endif // swift(>=5.5) && canImport(_Concurrency)
// MARK: - Code below here is support for the SwiftProtobuf runtime.
fileprivate let _protobuf_package = "WebSocketProtos"
@ -249,18 +256,22 @@ extension WebSocketProtos_WebSocketRequestMessage: SwiftProtobuf.Message, SwiftP
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._verb {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._verb {
try visitor.visitSingularStringField(value: v, fieldNumber: 1)
}
if let v = self._path {
} }()
try { if let v = self._path {
try visitor.visitSingularStringField(value: v, fieldNumber: 2)
}
if let v = self._body {
} }()
try { if let v = self._body {
try visitor.visitSingularBytesField(value: v, fieldNumber: 3)
}
if let v = self._requestID {
} }()
try { if let v = self._requestID {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 4)
}
} }()
if !self.headers.isEmpty {
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
}
@ -305,18 +316,22 @@ extension WebSocketProtos_WebSocketResponseMessage: SwiftProtobuf.Message, Swift
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._requestID {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._requestID {
try visitor.visitSingularUInt64Field(value: v, fieldNumber: 1)
}
if let v = self._status {
} }()
try { if let v = self._status {
try visitor.visitSingularUInt32Field(value: v, fieldNumber: 2)
}
if let v = self._message {
} }()
try { if let v = self._message {
try visitor.visitSingularStringField(value: v, fieldNumber: 3)
}
if let v = self._body {
} }()
try { if let v = self._body {
try visitor.visitSingularBytesField(value: v, fieldNumber: 4)
}
} }()
if !self.headers.isEmpty {
try visitor.visitRepeatedStringField(value: self.headers, fieldNumber: 5)
}
@ -357,15 +372,19 @@ extension WebSocketProtos_WebSocketMessage: SwiftProtobuf.Message, SwiftProtobuf
}
func traverse<V: SwiftProtobuf.Visitor>(visitor: inout V) throws {
if let v = self._type {
// The use of inline closures is to circumvent an issue where the compiler
// allocates stack space for every if/case branch local when no optimizations
// are enabled. https://github.com/apple/swift-protobuf/issues/1034 and
// https://github.com/apple/swift-protobuf/issues/1182
try { if let v = self._type {
try visitor.visitSingularEnumField(value: v, fieldNumber: 1)
}
if let v = self._request {
} }()
try { if let v = self._request {
try visitor.visitSingularMessageField(value: v, fieldNumber: 2)
}
if let v = self._response {
} }()
try { if let v = self._response {
try visitor.visitSingularMessageField(value: v, fieldNumber: 3)
}
} }()
try unknownFields.traverse(visitor: &visitor)
}

View File

@ -217,6 +217,7 @@ message DataMessage {
optional ClosedGroupControlMessage closedGroupControlMessage = 104;
optional string syncTarget = 105;
optional bool blocksCommunityMessageRequests = 106;
optional GroupUpdateMessage groupUpdateMessage = 120;
}
message ConfigurationMessage {
@ -288,7 +289,6 @@ message SharedConfigMessage {
USER_PROFILE = 1;
CONTACTS = 2;
CONVO_INFO_VOLATILE = 3;
USER_GROUPS = 4;
}
// @required
@ -298,3 +298,89 @@ message SharedConfigMessage {
// @required
required bytes data = 3;
}
// Group Update Messages
message GroupUpdateMessage {
optional GroupUpdateInviteMessage inviteMessage = 31;
optional GroupUpdateDeleteMessage deleteMessage = 32;
optional GroupUpdateInfoChangeMessage infoChangeMessage = 33;
optional GroupUpdateMemberChangeMessage memberChangeMessage = 34;
optional GroupUpdatePromoteMessage promoteMessage = 35;
optional GroupUpdateMemberLeftMessage memberLeftMessage = 36;
optional GroupUpdateInviteResponseMessage inviteResponse = 37;
optional GroupUpdatePromotionResponseMessage promotionResponse = 38;
optional GroupUpdateDeleteMemberContentMessage deleteMemberContent = 39;
}
message GroupUpdateInviteMessage {
// @required
required bytes groupIdentityPublicKey = 1;
// @required
required string name = 2;
// @required
required bytes memberSubkey = 3;
// @required
required bytes memberTag = 4;
optional bytes profileKey = 5;
optional LokiProfile profile = 6;
}
message GroupUpdateDeleteMessage {
// @required
required bytes groupIdentityPublicKey = 1;
// @required
required bytes encryptedMemberSubkey = 2;
}
message GroupUpdateInfoChangeMessage {
enum Type {
NAME = 1;
AVATAR = 2;
DISAPPEARING_MESSAGES = 3;
}
// @required
required Type type = 1;
optional string updatedName = 2;
optional uint32 updatedExpiration = 3;
}
message GroupUpdateMemberChangeMessage {
enum Type {
ADDED = 1;
REMOVED = 2;
PROMOTED = 3;
}
// @required
required Type type = 1;
repeated bytes memberPublicKeys = 2;
}
message GroupUpdatePromoteMessage {
// @required
required bytes memberPublicKey = 1;
// @required
required bytes encryptedGroupIdentityPrivateKey = 2;
}
message GroupUpdateMemberLeftMessage {
// the pubkey of the member left is included as part of the closed group encryption logic (senderIdentity on desktop)
}
message GroupUpdateInviteResponseMessage {
// @required
required bool isApproved = 1; // Whether the request was approved
optional bytes profileKey = 2;
optional LokiProfile profile = 3;
}
message GroupUpdatePromotionResponseMessage {
// @required
required bytes encryptedMemberPublicKey = 1;
}
message GroupUpdateDeleteMemberContentMessage {
repeated bytes memberPublicKeys = 2;
}

View File

@ -92,7 +92,7 @@ extension MessageReceiver {
try SessionUtil
.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
disappearingConfig: remoteConfig
)
@ -237,10 +237,13 @@ extension MessageReceiver {
try SessionUtil
.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
disappearingConfig: remoteConfig
)
case .group:
preconditionFailure()
default: break
}
}

View File

@ -0,0 +1,58 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import Sodium
import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver {
// MARK: - Specific Handling
private static func handleNewClosedGroup(
_ db: Database,
message: ClosedGroupControlMessage,
using dependencies: Dependencies
) throws {
}
internal static func handleNewGroup(
_ db: Database,
groupIdentityPublicKey: String,
groupIdentityPrivateKey: Data?,
name: String,
tag: Data?,
subkey: Data?,
created: Int64,
approved: Bool,
calledFromConfigHandling: Bool,
using dependencies: Dependencies
) throws {
// Create the group
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupIdentityPublicKey, variant: .group, shouldBeVisible: true)
let closedGroup: ClosedGroup = try ClosedGroup(
threadId: groupIdentityPublicKey,
name: name,
formationTimestamp: TimeInterval(created),
groupIdentityPrivateKey: groupIdentityPrivateKey,
tag: tag,
subkey: subkey,
approved: approved
).saved(db)
// Only start polling and subscribe for PNs if the user has approved the group
guard approved else { return }
// Start polling
ClosedGroupPoller.shared.startIfNeeded(for: groupIdentityPublicKey, using: dependencies)
// Resubscribe for group push notifications
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
}
}

View File

@ -8,7 +8,7 @@ import SessionUtilitiesKit
import SessionSnodeKit
extension MessageReceiver {
public static func handleClosedGroupControlMessage(
public static func handleLegacyClosedGroupControlMessage(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
@ -16,7 +16,7 @@ extension MessageReceiver {
using dependencies: Dependencies = Dependencies()
) throws {
switch message.kind {
case .new: try handleNewClosedGroup(db, message: message, using: dependencies)
case .new: try handleNewLegacyClosedGroup(db, message: message, using: dependencies)
case .encryptionKeyPair:
try handleClosedGroupEncryptionKeyPair(
@ -66,7 +66,7 @@ extension MessageReceiver {
// MARK: - Specific Handling
private static func handleNewClosedGroup(
private static func handleNewLegacyClosedGroup(
_ db: Database,
message: ClosedGroupControlMessage,
using dependencies: Dependencies
@ -108,7 +108,7 @@ extension MessageReceiver {
return
}
try handleNewClosedGroup(
try handleNewLegacyClosedGroup(
db,
groupPublicKey: publicKeyAsData.toHexString(),
name: name,
@ -122,7 +122,7 @@ extension MessageReceiver {
)
}
internal static func handleNewClosedGroup(
internal static func handleNewLegacyClosedGroup(
_ db: Database,
groupPublicKey: String,
name: String,
@ -157,7 +157,8 @@ extension MessageReceiver {
let closedGroup: ClosedGroup = try ClosedGroup(
threadId: groupPublicKey,
name: name,
formationTimestamp: (TimeInterval(formationTimestampMs) / 1000)
formationTimestamp: (TimeInterval(formationTimestampMs) / 1000),
approved: true // Legacy groups are always approved
).saved(db)
// Clear the zombie list if the group wasn't active (ie. had no keys)
@ -217,7 +218,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.add(
db,
groupPublicKey: groupPublicKey,
legacyGroupPublicKey: groupPublicKey,
name: name,
latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey),
latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey),
@ -234,8 +235,8 @@ extension MessageReceiver {
// Resubscribe for group push notifications
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
PushNotificationAPI
.subscribeToLegacyGroups(
try? PushNotificationAPI
.preparedSubscribeToLegacyGroups(
currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: try ClosedGroup
.select(.threadId)
@ -247,7 +248,9 @@ extension MessageReceiver {
.asRequest(of: String.self)
.fetchSet(db)
.inserting(groupPublicKey) // Insert the new key just to be sure
)
)?
.send(using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .default), using: dependencies)
.sinkUntilComplete()
}
@ -315,7 +318,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: groupPublicKey,
legacyGroupPublicKey: groupPublicKey,
latestKeyPair: keyPair
)
}
@ -352,7 +355,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
name: name
)
@ -395,7 +398,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
members: allMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
@ -502,7 +505,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
members: allMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
@ -554,6 +557,8 @@ extension MessageReceiver {
case .memberLeft = messageKind
else { return }
// TODO: [GROUPS REBUILD] If the current user is an admin then we need to actually remove the member from the group.
try processIfValid(
db,
threadId: threadId,
@ -577,7 +582,7 @@ extension MessageReceiver {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: threadId,
legacyGroupPublicKey: threadId,
members: allMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }
@ -660,6 +665,7 @@ extension MessageReceiver {
try legacyGroupChanges(sender, closedGroup, allMembers)
case .group:
// TODO: [GROUPS REBUILD] Need to check if the user has access to historic messages
break
default: return // Ignore as invalid

View File

@ -0,0 +1,123 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import Sodium
import SessionUtilitiesKit
import SessionSnodeKit
extension MessageSender {
private typealias PreparedGroupData = (
thread: SessionThread,
group: ClosedGroup,
members: [GroupMember],
preparedNotificationsSubscription: HTTP.PreparedRequest<PushNotificationAPI.SubscribeResponse>,
currentUserPublicKey: String
)
public static func createGroup(
name: String,
displayPicture: SignalAttachment?,
members: Set<String>,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<SessionThread, Error> {
Just(())
.setFailureType(to: Error.self)
.flatMap { _ -> AnyPublisher<(url: String, filename: String, encryptionKey: Data)?, Error> in
guard let displayPicture: SignalAttachment = displayPicture else {
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// TODO: Upload group image first
return Just(nil)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.map { displayPictureInfo -> PreparedGroupData? in
dependencies.storage.write { db -> PreparedGroupData in
// Create and cache the libSession entries
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let groupData: (identityKeyPair: KeyPair, group: ClosedGroup, members: [GroupMember]) = try SessionUtil.createGroup(
db,
name: name,
displayPictureUrl: displayPictureInfo?.url,
displayPictureFilename: displayPictureInfo?.filename,
displayPictureEncryptionKey: displayPictureInfo?.encryptionKey,
members: members,
admins: [currentUserPublicKey],
using: dependencies
)
let preparedNotificationSubscription = try PushNotificationAPI
.preparedSubscribe(
publicKey: groupData.group.id,
subkey: nil,
ed25519KeyPair: groupData.identityKeyPair,
using: dependencies
)
// Save the relevant objects to the database
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupData.group.id, variant: .group, shouldBeVisible: true)
try groupData.group.insert(db)
try groupData.members.forEach { try $0.insert(db) }
return (
thread,
groupData.group,
groupData.members,
preparedNotificationSubscription,
currentUserPublicKey
)
}
}
.tryFlatMap { maybePreparedData -> AnyPublisher<PreparedGroupData, Error> in
guard let preparedData: PreparedGroupData = maybePreparedData else {
throw StorageError.failedToSave
}
return ConfigurationSyncJob
.run(publicKey: preparedData.group.id, using: dependencies)
.map { _ in preparedData }
.eraseToAnyPublisher()
}
.handleEvents(
receiveOutput: { _, group, members, preparedNotificationSubscription, currentUserPublicKey in
// Start polling
ClosedGroupPoller.shared.startIfNeeded(for: group.id, using: dependencies)
// Subscribe for push notifications
preparedNotificationSubscription
.send(using: dependencies)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
// Save jobs for sending group member invitations
dependencies.storage.write { db in
members
.filter { $0.profileId != currentUserPublicKey }
.forEach { member in
dependencies.jobRunner.add(
db,
job: Job(
variant: .groupInviteMemberJob,
details: GroupInviteMemberJob.Details(
memberSubkey: Data(),
memberTag: Data()
)
),
canStartJob: true,
using: dependencies
)
// Send admin keys to any admins
guard member.role == .admin else { return }
}
}
}
)
.map { thread, _, _, _, _ in thread }
.eraseToAnyPublisher()
}
}

View File

@ -10,7 +10,7 @@ import SessionSnodeKit
extension MessageSender {
public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:])
public static func createClosedGroup(
public static func createLegacyClosedGroup(
name: String,
members: Set<String>,
using dependencies: Dependencies = Dependencies()
@ -24,7 +24,7 @@ extension MessageSender {
else { throw MessageSenderError.noKeyPair }
// Includes the 'SessionId.Prefix.standard' prefix
let groupPublicKey: String = groupKeyPair.hexEncodedPublicKey
let legacyGroupPublicKey: String = groupKeyPair.hexEncodedPublicKey
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
var members: Set<String> = members
@ -37,17 +37,18 @@ extension MessageSender {
// Create the relevant objects in the database
let thread: SessionThread = try SessionThread
.fetchOrCreate(db, id: groupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
.fetchOrCreate(db, id: legacyGroupPublicKey, variant: .legacyGroup, shouldBeVisible: true)
try ClosedGroup(
threadId: groupPublicKey,
threadId: legacyGroupPublicKey,
name: name,
formationTimestamp: formationTimestamp
formationTimestamp: formationTimestamp,
approved: true // Legacy groups are always approved
).insert(db)
// Store the key pair
let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000)
try ClosedGroupKeyPair(
threadId: groupPublicKey,
threadId: legacyGroupPublicKey,
publicKey: Data(encryptionKeyPair.publicKey),
secretKey: Data(encryptionKeyPair.secretKey),
receivedTimestamp: latestKeyPairReceivedTimestamp
@ -56,7 +57,7 @@ extension MessageSender {
// Create the member objects
try admins.forEach { adminId in
try GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: adminId,
role: .admin,
isHidden: false
@ -65,7 +66,7 @@ extension MessageSender {
try members.forEach { memberId in
try GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
@ -75,12 +76,12 @@ extension MessageSender {
// Update libSession
try SessionUtil.add(
db,
groupPublicKey: groupPublicKey,
legacyGroupPublicKey: legacyGroupPublicKey,
name: name,
latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey),
latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey),
latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp,
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey),
disappearingConfig: DisappearingMessagesConfiguration.defaultWith(legacyGroupPublicKey),
members: members,
admins: admins
)
@ -91,7 +92,7 @@ extension MessageSender {
db,
message: ClosedGroupControlMessage(
kind: .new(
publicKey: Data(hex: groupPublicKey),
publicKey: Data(hex: legacyGroupPublicKey),
name: name,
encryptionKeyPair: encryptionKeyPair,
members: membersAsData,
@ -117,7 +118,7 @@ extension MessageSender {
)
.asRequest(of: String.self)
.fetchSet(db)
.inserting(groupPublicKey) // Insert the new key just to be sure
.inserting(legacyGroupPublicKey) // Insert the new key just to be sure
return (userPublicKey, thread, memberSendData, allActiveLegacyGroupIds)
}
@ -129,10 +130,14 @@ extension MessageSender {
.map { MessageSender.sendImmediate(data: $0, using: dependencies) }
.appending(
// Resubscribe to all legacy groups
PushNotificationAPI.subscribeToLegacyGroups(
currentUserPublicKey: userPublicKey,
legacyGroupIds: allActiveLegacyGroupIds
)
try? PushNotificationAPI
.preparedSubscribeToLegacyGroups(
currentUserPublicKey: userPublicKey,
legacyGroupIds: allActiveLegacyGroupIds
)?
.send(using: dependencies)
.map { _ in () }
.eraseToAnyPublisher()
)
)
.collect()
@ -235,7 +240,7 @@ extension MessageSender {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
legacyGroupPublicKey: closedGroup.threadId,
latestKeyPair: newKeyPair,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
@ -261,7 +266,7 @@ extension MessageSender {
}
public static func update(
groupPublicKey: String,
legacyGroupPublicKey: String,
with members: Set<String>,
name: String,
using dependencies: Dependencies = Dependencies()
@ -271,11 +276,11 @@ extension MessageSender {
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
// Get the group, check preconditions & prepare
guard (try? SessionThread.exists(db, id: groupPublicKey)) == true else {
guard (try? SessionThread.exists(db, id: legacyGroupPublicKey)) == true else {
SNLog("Can't update nonexistent closed group.")
throw MessageSenderError.noThread
}
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: groupPublicKey) else {
guard let closedGroup: ClosedGroup = try? ClosedGroup.fetchOne(db, id: legacyGroupPublicKey) else {
throw MessageSenderError.invalidClosedGroupUpdate
}
@ -288,7 +293,7 @@ extension MessageSender {
// Notify the user
let interaction: Interaction = try Interaction(
threadId: groupPublicKey,
threadId: legacyGroupPublicKey,
authorId: userPublicKey,
variant: .infoClosedGroupUpdated,
body: ClosedGroupControlMessage.Kind
@ -304,7 +309,7 @@ extension MessageSender {
db,
message: ClosedGroupControlMessage(kind: .nameChange(name: name)),
interactionId: interactionId,
threadId: groupPublicKey,
threadId: legacyGroupPublicKey,
threadVariant: .legacyGroup,
using: dependencies
)
@ -312,7 +317,7 @@ extension MessageSender {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
legacyGroupPublicKey: closedGroup.threadId,
name: name
)
}
@ -416,7 +421,7 @@ extension MessageSender {
// Update libSession
try? SessionUtil.update(
db,
groupPublicKey: closedGroup.threadId,
legacyGroupPublicKey: closedGroup.threadId,
members: allGroupMembers
.filter { $0.role == .standard || $0.role == .zombie }
.map { $0.profileId }

View File

@ -167,7 +167,12 @@ public enum MessageReceiver {
// Extract the proper threadId for the message
let (threadId, threadVariant): (String, SessionThread.Variant) = {
if let groupPublicKey: String = groupPublicKey { return (groupPublicKey, .legacyGroup) }
if let groupPublicKey: String = groupPublicKey {
switch SessionId.Prefix(from: groupPublicKey) {
case .group: return (groupPublicKey, .group)
default: return (groupPublicKey, .legacyGroup)
}
}
if let openGroupId: String = openGroupId { return (openGroupId, .community) }
switch message {
@ -236,7 +241,7 @@ public enum MessageReceiver {
)
case let message as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(
try MessageReceiver.handleLegacyClosedGroupControlMessage(
db,
threadId: threadId,
threadVariant: threadVariant,

View File

@ -159,9 +159,9 @@ extension MessageSender {
try? OpenGroup.fetchOne(db, id: threadId)
)
}
.flatMap { attachments, openGroup -> AnyPublisher<[String?], Error> in
.flatMap { attachments, openGroup -> AnyPublisher<[String], Error> in
guard !attachments.isEmpty else {
return Just<[String?]>([])
return Just<[String]>([])
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
@ -169,7 +169,7 @@ extension MessageSender {
return Publishers
.MergeMany(
attachments
.map { attachment -> AnyPublisher<String?, Error> in
.map { attachment -> AnyPublisher<String, Error> in
attachment
.upload(
to: (
@ -183,11 +183,9 @@ extension MessageSender {
.collect()
.eraseToAnyPublisher()
}
.map { results -> PreparedSendData in
.map { fileIds -> PreparedSendData in
// Once the attachments are processed then update the PreparedSendData with
// the fileIds associated to the message
let fileIds: [String] = results.compactMap { result -> String? in result }
return preparedSendData.with(fileIds: fileIds)
}
.eraseToAnyPublisher()

View File

@ -793,7 +793,7 @@ public final class MessageSender {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in
let serverTimestampMs: UInt64? = responseData.posted.map { UInt64(floor($0 * 1000)) }
let updatedMessage: Message = message
@ -858,7 +858,7 @@ public final class MessageSender {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.flatMap { (responseInfo, responseData) -> AnyPublisher<Void, Error> in
let updatedMessage: Message = message
updatedMessage.openGroupServerMessageId = UInt64(responseData.id)

View File

@ -2,7 +2,7 @@
import Foundation
extension PushNotificationAPI {
public extension PushNotificationAPI {
struct LegacyPushServerResponse: Codable {
let code: Int
let message: String?

View File

@ -27,7 +27,7 @@ public struct PushNotificationAPIRequest<T: Encodable>: Encodable {
public func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(endpoint.rawValue, forKey: .method)
try container.encode(endpoint.path, forKey: .method)
try container.encode(body, forKey: .body)
}
}

View File

@ -2,7 +2,7 @@
import Foundation
extension PushNotificationAPI {
public extension PushNotificationAPI {
struct SubscribeResponse: Codable {
/// Flag indicating the success of the registration
let success: Bool?

View File

@ -2,7 +2,7 @@
import Foundation
extension PushNotificationAPI {
public extension PushNotificationAPI {
struct UnsubscribeResponse: Codable {
/// Flag indicating the success of the registration
let success: Bool?

View File

@ -20,13 +20,17 @@ public enum PushNotificationAPI {
public static let legacyServer = "https://live.apns.getsession.org"
public static let legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
// MARK: - Requests
// MARK: - Batch Requests
public static func subscribe(
public static func subscribeAll(
token: Data,
isForcedUpdate: Bool,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
typealias SubscribeAllPreparedRequests = (
HTTP.PreparedRequest<PushNotificationAPI.SubscribeResponse>,
HTTP.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>?
)
let hexEncodedToken: String = token.toHexString()
let oldToken: String? = dependencies.standardUserDefaults[.deviceToken]
let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload]
@ -39,94 +43,66 @@ public enum PushNotificationAPI {
.eraseToAnyPublisher()
}
guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies) else {
SNLog("Unable to retrieve PN encryption key.")
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
// TODO: Need to generate requests for each updated group as well
return dependencies.storage
.readPublisher(using: dependencies) { db -> (SubscribeRequest, String, Set<String>) in
.readPublisher(using: dependencies) { db -> SubscribeAllPreparedRequests in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair
}
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let request: SubscribeRequest = SubscribeRequest(
pubkey: currentUserPublicKey,
namespaces: [.default],
// Note: Unfortunately we always need the message content because without the content
// control messages can't be distinguished from visible messages which results in the
// 'generic' notification being shown when receiving things like typing indicator updates
includeMessageData: true,
serviceInfo: SubscribeRequest.ServiceInfo(
token: hexEncodedToken
),
notificationsEncryptionKey: notificationsEncryptionKey,
subkey: nil,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: userED25519KeyPair.publicKey,
ed25519SecretKey: userED25519KeyPair.secretKey
)
let preparedUserRequest = try PushNotificationAPI
.preparedSubscribe(
publicKey: currentUserPublicKey,
subkey: nil,
ed25519KeyPair: userED25519KeyPair,
using: dependencies
)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else { return }
dependencies.standardUserDefaults[.deviceToken] = hexEncodedToken
dependencies.standardUserDefaults[.lastDeviceTokenUpload] = now
dependencies.standardUserDefaults[.isUsingFullAPNs] = true
}
)
let preparedLegacyGroupRequest = try PushNotificationAPI
.preparedSubscribeToLegacyGroups(
forced: true,
token: hexEncodedToken,
currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db),
using: dependencies
)
return (
request,
currentUserPublicKey,
try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.joining(
required: ClosedGroup.members
.filter(GroupMember.Columns.profileId == currentUserPublicKey)
)
.asRequest(of: String.self)
.fetchSet(db)
preparedUserRequest,
preparedLegacyGroupRequest
)
}
.flatMap { request, currentUserPublicKey, legacyGroupIds -> AnyPublisher<Void, Error> in
.flatMap { userRequest, legacyGroupRequest -> AnyPublisher<Void, Error> in
Publishers
.MergeMany(
[
PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
endpoint: .subscribe,
body: request
),
using: dependencies
)
.decoded(as: SubscribeResponse.self, using: dependencies)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't subscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
dependencies.standardUserDefaults[.deviceToken] = hexEncodedToken
dependencies.standardUserDefaults[.lastDeviceTokenUpload] = now
dependencies.standardUserDefaults[.isUsingFullAPNs] = true
},
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Couldn't subscribe for push notifications.")
}
}
)
.map { _ in () }
userRequest
.send(using: dependencies)
.map { _, _ in () }
.eraseToAnyPublisher(),
// FIXME: Remove this once legacy groups are deprecated
PushNotificationAPI.subscribeToLegacyGroups(
forced: true,
token: hexEncodedToken,
currentUserPublicKey: currentUserPublicKey,
legacyGroupIds: legacyGroupIds,
using: dependencies
)
]
legacyGroupRequest?
.send(using: dependencies)
.map { _, _ in () }
.eraseToAnyPublisher()
].compactMap { $0 }
)
.collect()
.map { _ in () }
@ -135,116 +111,193 @@ public enum PushNotificationAPI {
.eraseToAnyPublisher()
}
public static func unsubscribe(
public static func unsubscribeAll(
token: Data,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
let hexEncodedToken: String = token.toHexString()
typealias UnsubscribeAllPreparedRequests = (
HTTP.PreparedRequest<PushNotificationAPI.UnsubscribeResponse>,
[HTTP.PreparedRequest<PushNotificationAPI.LegacyPushServerResponse>]
)
// FIXME: Remove this once legacy groups are deprecated
/// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case)
dependencies.storage
.readPublisher(using: dependencies) { db -> (String, Set<String>) in
(
getUserHexEncodedPublicKey(db, using: dependencies),
try ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.asRequest(of: String.self)
.fetchSet(db)
)
}
.flatMap { currentUserPublicKey, legacyGroupIds in
Publishers
.MergeMany(
legacyGroupIds
.map { legacyGroupId -> AnyPublisher<Void, Error> in
PushNotificationAPI
.unsubscribeFromLegacyGroup(
legacyGroupId: legacyGroupId,
currentUserPublicKey: currentUserPublicKey,
using: dependencies
)
}
)
.collect()
.eraseToAnyPublisher()
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete()
// TODO: Need to generate requests for each updated group as well
return dependencies.storage
.readPublisher(using: dependencies) { db -> UnsubscribeRequest in
.readPublisher(using: dependencies) { db -> UnsubscribeAllPreparedRequests in
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
throw SnodeAPIError.noKeyPair
}
return UnsubscribeRequest(
pubkey: getUserHexEncodedPublicKey(db, using: dependencies),
serviceInfo: UnsubscribeRequest.ServiceInfo(
token: hexEncodedToken
),
subkey: nil,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: userED25519KeyPair.publicKey,
ed25519SecretKey: userED25519KeyPair.secretKey
)
}
.flatMap { request -> AnyPublisher<Void, Error> in
PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
endpoint: .unsubscribe,
body: request
),
using: dependencies
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let preparedUserRequest = try PushNotificationAPI
.preparedUnsubscribe(
token: token,
publicKey: currentUserPublicKey,
subkey: nil,
ed25519KeyPair: userED25519KeyPair
)
.decoded(as: UnsubscribeResponse.self, using: dependencies)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't unsubscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
guard response.success == true else { return }
dependencies.standardUserDefaults[.deviceToken] = nil
},
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Couldn't unsubscribe for push notifications.")
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
// FIXME: Remove this once legacy groups are deprecated
let preparedLegacyUnsubscribeRequests = (try? ClosedGroup
.select(.threadId)
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
.asRequest(of: String.self)
.fetchSet(db))
.defaulting(to: [])
.compactMap { legacyGroupId in
try? PushNotificationAPI.preparedUnsubscribeFromLegacyGroup(
legacyGroupId: legacyGroupId,
currentUserPublicKey: currentUserPublicKey
)
}
return (preparedUserRequest, preparedLegacyUnsubscribeRequests)
}
.flatMap { preparedUserRequest, preparedLegacyUnsubscribeRequests in
// FIXME: Remove this once legacy groups are deprecated
/// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case)
Publishers
.MergeMany(preparedLegacyUnsubscribeRequests.map { $0.send(using: dependencies) })
.collect()
.subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.receive(on: DispatchQueue.global(qos: .userInitiated), using: dependencies)
.sinkUntilComplete()
return preparedUserRequest.send(using: dependencies)
}
.map { _ in () }
.eraseToAnyPublisher()
}
// MARK: - Prepared Requests
public static func preparedSubscribe(
publicKey: String,
subkey: String?,
ed25519KeyPair: KeyPair,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<SubscribeResponse> {
guard
UserDefaults.standard[.isUsingFullAPNs],
let token: String = dependencies.standardUserDefaults[.deviceToken]
else { throw HTTPError.invalidRequest }
guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies) else {
SNLog("Unable to retrieve PN encryption key.")
throw StorageError.invalidKeySpec
}
return try PushNotificationAPI
.prepareRequest(
request: Request(
method: .post,
server: PushNotificationAPI.server,
endpoint: .subscribe,
body: SubscribeRequest(
pubkey: publicKey,
namespaces: [.default],
// Note: Unfortunately we always need the message content because without the content
// control messages can't be distinguished from visible messages which results in the
// 'generic' notification being shown when receiving things like typing indicator updates
includeMessageData: true,
serviceInfo: SubscribeRequest.ServiceInfo(
token: token
),
notificationsEncryptionKey: notificationsEncryptionKey,
subkey: subkey,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: ed25519KeyPair.publicKey,
ed25519SecretKey: ed25519KeyPair.secretKey
)
),
responseType: SubscribeResponse.self,
retryCount: PushNotificationAPI.maxRetryCount
)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't subscribe for push notifications for: \(publicKey) due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
},
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Couldn't subscribe for push notifications for: \(publicKey).")
}
}
)
}
public static func preparedUnsubscribe(
token: Data,
publicKey: String,
subkey: String?,
ed25519KeyPair: KeyPair,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<UnsubscribeResponse> {
return try PushNotificationAPI
.prepareRequest(
request: Request(
method: .post,
server: PushNotificationAPI.server,
endpoint: .unsubscribe,
body: UnsubscribeRequest(
pubkey: publicKey,
serviceInfo: UnsubscribeRequest.ServiceInfo(
token: token.toHexString()
),
subkey: nil,
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
ed25519PublicKey: ed25519KeyPair.publicKey,
ed25519SecretKey: ed25519KeyPair.secretKey
)
),
responseType: UnsubscribeResponse.self,
retryCount: PushNotificationAPI.maxRetryCount
)
.handleEvents(
receiveOutput: { _, response in
guard response.success == true else {
return SNLog("Couldn't unsubscribe for push notifications for: \(publicKey) due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
}
},
receiveCompletion: { result in
switch result {
case .finished: break
case .failure: SNLog("Couldn't unsubscribe for push notifications for: \(publicKey).")
}
}
)
}
// MARK: - Legacy Notifications
// FIXME: Remove this once legacy notifications and legacy groups are deprecated
public static func legacyNotify(
public static func preparedLegacyNotify(
recipient: String,
with message: String,
maxRetryCount: Int? = nil,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
) throws -> HTTP.PreparedRequest<LegacyPushServerResponse> {
return try PushNotificationAPI
.prepareRequest(
request: Request(
method: .post,
server: PushNotificationAPI.legacyServer,
endpoint: .legacyNotify,
body: LegacyNotifyRequest(
data: message,
sendTo: recipient
)
),
using: dependencies
responseType: LegacyPushServerResponse.self,
retryCount: (maxRetryCount ?? PushNotificationAPI.maxRetryCount)
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -258,35 +311,32 @@ public enum PushNotificationAPI {
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
}
// MARK: - Legacy Groups
// FIXME: Remove this once legacy groups are deprecated
public static func subscribeToLegacyGroups(
public static func preparedSubscribeToLegacyGroups(
forced: Bool = false,
token: String? = nil,
currentUserPublicKey: String,
legacyGroupIds: Set<String>,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
) throws -> HTTP.PreparedRequest<LegacyPushServerResponse>? {
let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs]
// Only continue if PNs are enabled and we have a device token
guard
!legacyGroupIds.isEmpty,
(forced || isUsingFullAPNs),
let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken])
else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
else { return nil }
return PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
return try PushNotificationAPI
.prepareRequest(
request: Request(
method: .post,
server: PushNotificationAPI.legacyServer,
endpoint: .legacyGroupsOnlySubscribe,
body: LegacyGroupOnlyRequest(
token: deviceToken,
@ -295,10 +345,9 @@ public enum PushNotificationAPI {
legacyGroupPublicKeys: legacyGroupIds
)
),
using: dependencies
responseType: LegacyPushServerResponse.self,
retryCount: PushNotificationAPI.maxRetryCount
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -312,29 +361,28 @@ public enum PushNotificationAPI {
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
}
// FIXME: Remove this once legacy groups are deprecated
public static func unsubscribeFromLegacyGroup(
public static func preparedUnsubscribeFromLegacyGroup(
legacyGroupId: String,
currentUserPublicKey: String,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<Void, Error> {
return PushNotificationAPI
.send(
request: PushNotificationAPIRequest(
) throws -> HTTP.PreparedRequest<LegacyPushServerResponse> {
return try PushNotificationAPI
.prepareRequest(
request: Request(
method: .post,
server: PushNotificationAPI.legacyServer,
endpoint: .legacyGroupUnsubscribe,
body: LegacyGroupRequest(
pubKey: currentUserPublicKey,
closedGroupPublicKey: legacyGroupId
)
),
using: dependencies
responseType: LegacyPushServerResponse.self,
retryCount: PushNotificationAPI.maxRetryCount
)
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
.retry(maxRetryCount, using: dependencies)
.handleEvents(
receiveOutput: { _, response in
guard response.code != 0 else {
@ -348,8 +396,6 @@ public enum PushNotificationAPI {
}
}
)
.map { _ in () }
.eraseToAnyPublisher()
}
// MARK: - Notification Handling
@ -472,7 +518,7 @@ public enum PushNotificationAPI {
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
guard
let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.rawValue)"),
let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.path)"),
let payload: Data = try? JSONEncoder().encode(request.body)
else {
return Fail(error: HTTPError.invalidJSON)
@ -483,7 +529,7 @@ public enum PushNotificationAPI {
return HTTP
.execute(
.post,
"\(request.endpoint.server)/\(request.endpoint.rawValue)",
"\(request.endpoint.server)/\(request.endpoint.path)",
body: payload
)
.map { response in (HTTP.ResponseInfo(code: -1, headers: [:]), response) }
@ -505,4 +551,23 @@ public enum PushNotificationAPI {
)
.eraseToAnyPublisher()
}
// MARK: - Convenience
private static func prepareRequest<T: Encodable, R: Decodable>(
request: Request<T, Endpoint>,
responseType: R.Type,
retryCount: Int = 0,
timeout: TimeInterval = HTTP.defaultTimeout,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.PreparedRequest<R> {
return HTTP.PreparedRequest<R>(
request: request,
urlRequest: try request.generateUrlRequest(),
publicKey: request.endpoint.serverPublicKey,
responseType: responseType,
retryCount: retryCount,
timeout: timeout
)
}
}

View File

@ -1,20 +1,36 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
public extension PushNotificationAPI {
enum Endpoint: String {
case subscribe = "subscribe"
case unsubscribe = "unsubscribe"
enum Endpoint: EndpointType {
case subscribe
case unsubscribe
// MARK: - Legacy Endpoints
case legacyNotify = "notify"
case legacyRegister = "register"
case legacyUnregister = "unregister"
case legacyGroupsOnlySubscribe = "register_legacy_groups_only"
case legacyGroupSubscribe = "subscribe_closed_group"
case legacyGroupUnsubscribe = "unsubscribe_closed_group"
case legacyNotify
case legacyRegister
case legacyUnregister
case legacyGroupsOnlySubscribe
case legacyGroupSubscribe
case legacyGroupUnsubscribe
public var path: String {
switch self {
case .subscribe: return "subscribe"
case .unsubscribe: return "unsubscribe"
// Legacy Endpoints
case .legacyNotify: return "notify"
case .legacyRegister: return "register"
case .legacyUnregister: return "unregister"
case .legacyGroupsOnlySubscribe: return "register_legacy_groups_only"
case .legacyGroupSubscribe: return "subscribe_closed_group"
case .legacyGroupUnsubscribe: return "unsubscribe_closed_group"
}
}
// MARK: - Convenience

View File

@ -7,13 +7,19 @@ import SessionSnodeKit
import SessionUtilitiesKit
public final class ClosedGroupPoller: Poller {
public static var legacyNamespaces: [SnodeAPI.Namespace] = [.legacyClosedGroup ]
public static var namespaces: [SnodeAPI.Namespace] = [
.legacyClosedGroup, .default, .configGroupInfo, .configGroupMembers, .configGroupKeys
.groupMessages, .configGroupInfo, .configGroupMembers, .configGroupKeys
]
// MARK: - Settings
override var namespaces: [SnodeAPI.Namespace] { ClosedGroupPoller.namespaces }
override func namespaces(for publicKey: String) -> [SnodeAPI.Namespace] {
guard SessionId.Prefix(from: publicKey) == .group else { return ClosedGroupPoller.legacyNamespaces }
return ClosedGroupPoller.namespaces
}
override var maxNodePollCount: UInt { 0 }
private static let minPollInterval: Double = 3

View File

@ -14,7 +14,7 @@ public final class CurrentUserPoller: Poller {
// MARK: - Settings
override var namespaces: [SnodeAPI.Namespace] { CurrentUserPoller.namespaces }
override func namespaces(for publicKey: String) -> [SnodeAPI.Namespace] { CurrentUserPoller.namespaces }
/// After polling a given snode this many times we always switch to a new one.
///

View File

@ -126,7 +126,7 @@ extension OpenGroupAPI {
)
return dependencies.storage
.readPublisher { db -> (Int64, PreparedSendData<BatchResponse>) in
.readPublisher { db -> (Int64, HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>) in
let failureCount: Int64 = (try? OpenGroup
.filter(OpenGroup.Columns.server == server)
.select(max(OpenGroup.Columns.pollFailureCount))
@ -146,8 +146,8 @@ extension OpenGroupAPI {
)
)
}
.flatMap { failureCount, sendData in
OpenGroupAPI.send(data: sendData, using: dependencies)
.flatMap { failureCount, preparedRequest in
preparedRequest.send(using: dependencies)
.map { info, response in (failureCount, info, response) }
}
.handleEvents(
@ -330,7 +330,7 @@ extension OpenGroupAPI {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.flatMap { [weak self] _, responseBody -> AnyPublisher<Void, Error> in
guard let strongSelf = self, isBackgroundPollerValid() else {
return Just(())
@ -372,7 +372,7 @@ extension OpenGroupAPI {
private func handlePollResponse(
info: ResponseInfoType,
response: BatchResponse,
response: HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>,
failureCount: Int64,
using dependencies: Dependencies
) {

View File

@ -19,7 +19,7 @@ public class Poller {
// MARK: - Settings
/// The namespaces which this poller queries
internal var namespaces: [SnodeAPI.Namespace] {
internal func namespaces(for publicKey: String) -> [SnodeAPI.Namespace] {
preconditionFailure("abstract class - override in subclass")
}
@ -139,7 +139,7 @@ public class Poller {
) {
guard isPolling.wrappedValue[publicKey] == true else { return }
let namespaces: [SnodeAPI.Namespace] = self.namespaces
let namespaces: [SnodeAPI.Namespace] = self.namespaces(for: publicKey)
let lastPollStart: TimeInterval = dependencies.dateNow.timeIntervalSince1970
let lastPollInterval: TimeInterval = nextPollDelay(for: publicKey, using: dependencies)
let getSnodePublisher: AnyPublisher<Snode, Error> = getSnodeForPolling(for: publicKey, using: dependencies)

View File

@ -487,12 +487,18 @@ internal extension SessionUtil {
return updated
}
static func updatingDisappearingConfigs<T>(_ db: Database, _ updated: [T]) throws -> [T] {
static func updatingDisappearingConfigsOneToOne<T>(_ db: Database, _ updated: [T]) throws -> [T] {
guard let updatedDisappearingConfigs: [DisappearingMessagesConfiguration] = updated as? [DisappearingMessagesConfiguration] else { throw StorageError.generic }
// Filter out any disappearing config changes related to groups
let targetUpdatedConfigs: [DisappearingMessagesConfiguration] = updatedDisappearingConfigs
.filter { SessionId.Prefix(from: $0.id) != .group }
guard !targetUpdatedConfigs.isEmpty else { return updated }
// We should only sync disappearing messages configs which are associated to existing contacts
let existingContactIds: [String] = (try? Contact
.filter(ids: updatedDisappearingConfigs.map { $0.id })
.filter(ids: targetUpdatedConfigs.map { $0.id })
.select(.id)
.asRequest(of: String.self)
.fetchAll(db))
@ -504,7 +510,7 @@ internal extension SessionUtil {
// Get the user public key (updating note to self is handled separately)
let userPublicKey: String = getUserHexEncodedPublicKey(db)
let targetDisappearingConfigs: [DisappearingMessagesConfiguration] = updatedDisappearingConfigs
let targetDisappearingConfigs: [DisappearingMessagesConfiguration] = targetUpdatedConfigs
.filter {
$0.id != userPublicKey &&
SessionId(from: $0.id)?.prefix == .standard &&
@ -512,7 +518,7 @@ internal extension SessionUtil {
}
// Update the note to self disappearing messages config first (if needed)
if let updatedUserDisappearingConfig: DisappearingMessagesConfiguration = updatedDisappearingConfigs.first(where: { $0.id == userPublicKey }) {
if let updatedUserDisappearingConfig: DisappearingMessagesConfiguration = targetUpdatedConfigs.first(where: { $0.id == userPublicKey }) {
try SessionUtil.performAndPushChange(
db,
for: .userProfile,

View File

@ -0,0 +1,131 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtil
import SessionUtilitiesKit
// MARK: - Group Info Handling
internal extension SessionUtil {
static let columnsRelatedToGroupInfo: [ColumnExpression] = [
ClosedGroup.Columns.name,
ClosedGroup.Columns.displayPictureUrl,
ClosedGroup.Columns.displayPictureEncryptionKey,
DisappearingMessagesConfiguration.Columns.isEnabled,
DisappearingMessagesConfiguration.Columns.type,
DisappearingMessagesConfiguration.Columns.durationSeconds
]
// MARK: - Incoming Changes
static func handleGroupInfoUpdate(
_ db: Database,
in conf: UnsafeMutablePointer<config_object>?,
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
) throws {
typealias GroupData = (profileName: String, profilePictureUrl: String?, profilePictureKey: Data?)
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
}
}
// MARK: - Outgoing Changes
internal extension SessionUtil {
static func updatingGroupInfo<T>(_ db: Database, _ updated: [T]) throws -> [T] {
guard let updatedGroups: [ClosedGroup] = updated as? [ClosedGroup] else { throw StorageError.generic }
// Exclude legacy groups as they aren't managed via SessionUtil
let targetGroups: [ClosedGroup] = updatedGroups
.filter { SessionId(from: $0.id)?.prefix == .group }
// If we only updated the current user contact then no need to continue
guard !targetGroups.isEmpty else { return updated }
// Loop through each of the groups and update their settings
try targetGroups.forEach { group in
try SessionUtil.performAndPushChange(
db,
for: .groupInfo,
publicKey: group.threadId
) { conf in
// Update the name
var updatedName: [CChar] = group.name.cArray.nullTerminated()
groups_info_set_name(conf, &updatedName)
// Either assign the updated display pic, or sent a blank pic (to remove the current one)
var displayPic: user_profile_pic = user_profile_pic()
displayPic.url = group.displayPictureUrl.toLibSession()
displayPic.key = group.displayPictureEncryptionKey.toLibSession()
groups_info_set_pic(conf, displayPic)
}
}
return updated
}
static func updatingDisappearingConfigsGroups<T>(_ db: Database, _ updated: [T]) throws -> [T] {
guard let updatedDisappearingConfigs: [DisappearingMessagesConfiguration] = updated as? [DisappearingMessagesConfiguration] else { throw StorageError.generic }
// Filter out any disappearing config changes not related to updated groups
let targetUpdatedConfigs: [DisappearingMessagesConfiguration] = updatedDisappearingConfigs
.filter { SessionId.Prefix(from: $0.id) == .group }
guard !targetUpdatedConfigs.isEmpty else { return updated }
// We should only sync disappearing messages configs which are associated to existing groups
let existingGroupIds: [String] = (try? ClosedGroup
.filter(ids: targetUpdatedConfigs.map { $0.id })
.select(.threadId)
.asRequest(of: String.self)
.fetchAll(db))
.defaulting(to: [])
// If none of the disappearing messages configs are associated with existing groups then ignore
// the changes (no need to do a config sync)
guard !existingGroupIds.isEmpty else { return updated }
// Loop through each of the groups and update their settings
try existingGroupIds
.compactMap { groupId in targetUpdatedConfigs.first(where: { $0.id == groupId }).map { (groupId, $0) } }
.forEach { groupIdentityPublicKey, updatedConfig in
try SessionUtil.performAndPushChange(
db,
for: .groupInfo,
publicKey: groupIdentityPublicKey
) { conf in
groups_info_set_expiry_timer(conf, Int32(updatedConfig.durationSeconds))
}
}
return updated
}
}
// MARK: - External Outgoing Changes
public extension SessionUtil {
static func update(
_ db: Database,
groupIdentityPublicKey: String,
name: String? = nil,
disappearingConfig: DisappearingMessagesConfiguration? = nil
) throws {
try SessionUtil.performAndPushChange(
db,
for: .groupInfo,
publicKey: groupIdentityPublicKey
) { conf in
if let name: String = name {
groups_info_set_name(conf, name.toLibSession())
}
if let config: DisappearingMessagesConfiguration = disappearingConfig {
groups_info_set_expiry_timer(conf, Int32(config.durationSeconds))
}
}
}
}

View File

@ -0,0 +1,29 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtil
import SessionUtilitiesKit
// MARK: - Group Info Handling
internal extension SessionUtil {
static let columnsRelatedToGroupKeys: [ColumnExpression] = []
// MARK: - Incoming Changes
static func handleGroupKeysUpdate(
_ db: Database,
in conf: UnsafeMutablePointer<config_object>?,
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
) throws {
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
}
}
// MARK: - Outgoing Changes
internal extension SessionUtil {
}

View File

@ -0,0 +1,29 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtil
import SessionUtilitiesKit
// MARK: - Group Info Handling
internal extension SessionUtil {
static let columnsRelatedToGroupMembers: [ColumnExpression] = []
// MARK: - Incoming Changes
static func handleGroupMembersUpdate(
_ db: Database,
in conf: UnsafeMutablePointer<config_object>?,
mergeNeedsDump: Bool,
latestConfigSentTimestampMs: Int64
) throws {
guard mergeNeedsDump else { return }
guard conf != nil else { throw SessionUtilError.nilConfigObject }
}
}
// MARK: - Outgoing Changes
internal extension SessionUtil {
}

View File

@ -28,6 +28,9 @@ internal extension SessionUtil {
.appending(contentsOf: columnsRelatedToConvoInfoVolatile)
.appending(contentsOf: columnsRelatedToUserGroups)
.appending(contentsOf: columnsRelatedToThreads)
.appending(contentsOf: columnsRelatedToGroupInfo)
.appending(contentsOf: columnsRelatedToGroupMembers)
.appending(contentsOf: columnsRelatedToGroupKeys)
.map { ColumnKey($0) }
.asSet()
@ -203,7 +206,24 @@ internal extension SessionUtil {
}
case .group:
break
try SessionUtil.performAndPushChange(
db,
for: .userGroups,
publicKey: userPublicKey
) { conf in
try SessionUtil.upsert(
groups: threads
.map { thread in
GroupInfo(
groupIdentityPublicKey: thread.id,
priority: thread.pinnedPriority
.map { Int32($0 == 0 ? SessionUtil.visiblePriority : max($0, 1)) }
.defaulting(to: SessionUtil.visiblePriority)
)
},
in: conf
)
}
}
}
@ -346,7 +366,8 @@ internal extension SessionUtil {
) -> Bool {
let targetPublicKey: String = {
switch targetConfig {
default: return getUserHexEncodedPublicKey(db)
case .userProfile, .contacts, .convoInfoVolatile, .userGroups: return getUserHexEncodedPublicKey(db)
case .groupInfo, .groupMembers, .groupKeys: return threadId
}
}()

View File

@ -0,0 +1,143 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import GRDB
import SessionUIKit
import SessionSnodeKit
import SessionUtil
import SessionUtilitiesKit
// MARK: - Convenience
internal extension SessionUtil {
static func createGroup(
_ db: Database,
name: String,
displayPictureUrl: String?,
displayPictureFilename: String?,
displayPictureEncryptionKey: Data?,
members: Set<String>,
admins: Set<String>,
using dependencies: Dependencies
) throws -> (identityKeyPair: KeyPair, group: ClosedGroup, members: [GroupMember]) {
guard
let groupIdentityKeyPair: KeyPair = dependencies.crypto.generate(.ed25519KeyPair()),
let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db)
else { throw MessageSenderError.noKeyPair }
// There will probably be custom init functions, will need a way to save the conf into
// the in-memory state after init though
var secretKey: [UInt8] = userED25519KeyPair.secretKey
var groupIdentityPublicKey: [UInt8] = groupIdentityKeyPair.publicKey
var groupIdentityPrivateKey: [UInt8] = groupIdentityKeyPair.secretKey
let groupIdentityPublicKeyString: String = groupIdentityKeyPair.publicKey.toHexString()
let creationTimestamp: TimeInterval = TimeInterval(SnodeAPI.currentOffsetTimestampMs() * 1000)
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
// Create the new config objects
var groupKeysConf: UnsafeMutablePointer<config_group_keys>? = nil
var groupInfoConf: UnsafeMutablePointer<config_object>? = nil
var groupMembersConf: UnsafeMutablePointer<config_object>? = nil
var error: [CChar] = [CChar](repeating: 0, count: 256)
try groups_info_init(
&groupInfoConf,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
nil,
0,
&error
).orThrow(error: error)
try groups_members_init(
&groupMembersConf,
&groupIdentityPublicKey,
&groupIdentityPrivateKey,
nil,
0,
&error
).orThrow(error: error)
// Set the initial values in the confs
var groupName: [CChar] = name.cArray.nullTerminated()
groups_info_set_name(groupInfoConf, &groupName)
groups_info_set_created(groupInfoConf, Int64(floor(creationTimestamp)))
if var displayPictureUrl: String = displayPictureUrl, var displayPictureEncryptionKey: Data = displayPictureEncryptionKey {
var displayPic: user_profile_pic = user_profile_pic()
displayPic.url = displayPictureUrl.toLibSession()
displayPic.key = displayPictureEncryptionKey.toLibSession()
groups_info_set_pic(groupInfoConf, displayPic)
}
// Create dumps for the configs
try [.groupKeys, .groupInfo, .groupMembers].forEach { variant in
try SessionUtil.pushChangesIfNeeded(db, for: variant, publicKey: groupIdentityPublicKeyString)
}
// Add the new group to the USER_GROUPS config message
try SessionUtil.add(
db,
groupIdentityPublicKey: groupIdentityPublicKeyString,
groupIdentityPrivateKey: Data(groupIdentityPrivateKey),
name: name,
tag: nil,
subkey: nil
)
return (
groupIdentityKeyPair,
ClosedGroup(
threadId: groupIdentityPublicKeyString,
name: name,
formationTimestamp: creationTimestamp,
displayPictureUrl: displayPictureUrl,
displayPictureFilename: displayPictureFilename,
displayPictureEncryptionKey: displayPictureEncryptionKey,
lastDisplayPictureUpdate: creationTimestamp,
groupIdentityPrivateKey: Data(groupIdentityPrivateKey),
approved: true
),
members
.map { memberId -> GroupMember in
GroupMember(
groupId: groupIdentityPublicKeyString,
profileId: memberId,
role: .standard,
isHidden: false
)
}
.appending(
contentsOf: admins.map { memberId -> GroupMember in
GroupMember(
groupId: groupIdentityPublicKeyString,
profileId: memberId,
role: .admin,
isHidden: false
)
}
)
)
}
@discardableResult static func addGroup(
_ db: Database,
groupIdentityPublicKey: [UInt8],
groupIdentityPrivateKey: Data?,
name: String,
tag: Data?,
subkey: Data?,
joinedAt: Int64,
approved: Bool,
using dependencies: Dependencies
) throws -> (group: ClosedGroup, members: [GroupMember]) {
// TODO: This!!!
preconditionFailure()
}
private extension Int32 {
func orThrow(error: [CChar]) throws {
guard self != 0 else { return }
SNLog("[SessionUtil Error] Unable to create group config objects: \(String(cString: error))")
throw SessionUtilError.unableToCreateConfigObject
}
}

View File

@ -605,7 +605,7 @@ public extension SessionUtil {
static func add(
_ db: Database,
groupPublicKey: String,
legacyGroupPublicKey: String,
name: String,
latestKeyPairPublicKey: Data,
latestKeyPairSecretKey: Data,
@ -621,7 +621,7 @@ public extension SessionUtil {
) { conf in
guard conf != nil else { throw SessionUtilError.nilConfigObject }
var cGroupId: [CChar] = groupPublicKey.cArray.nullTerminated()
var cGroupId: [CChar] = legacyGroupPublicKey.cArray.nullTerminated()
let userGroup: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cGroupId)
// Need to make sure the group doesn't already exist (otherwise we will end up overriding the
@ -635,10 +635,10 @@ public extension SessionUtil {
try SessionUtil.upsert(
legacyGroups: [
LegacyGroupInfo(
id: groupPublicKey,
id: legacyGroupPublicKey,
name: name,
lastKeyPair: ClosedGroupKeyPair(
threadId: groupPublicKey,
threadId: legacyGroupPublicKey,
publicKey: latestKeyPairPublicKey,
secretKey: latestKeyPairSecretKey,
receivedTimestamp: latestKeyPairReceivedTimestamp
@ -647,7 +647,7 @@ public extension SessionUtil {
groupMembers: members
.map { memberId in
GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
@ -656,7 +656,7 @@ public extension SessionUtil {
groupAdmins: admins
.map { memberId in
GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: memberId,
role: .admin,
isHidden: false
@ -671,7 +671,7 @@ public extension SessionUtil {
static func update(
_ db: Database,
groupPublicKey: String,
legacyGroupPublicKey: String,
name: String? = nil,
latestKeyPair: ClosedGroupKeyPair? = nil,
disappearingConfig: DisappearingMessagesConfiguration? = nil,
@ -686,14 +686,14 @@ public extension SessionUtil {
try SessionUtil.upsert(
legacyGroups: [
LegacyGroupInfo(
id: groupPublicKey,
id: legacyGroupPublicKey,
name: name,
lastKeyPair: latestKeyPair,
disappearingConfig: disappearingConfig,
groupMembers: members?
.map { memberId in
GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: memberId,
role: .standard,
isHidden: false
@ -702,7 +702,7 @@ public extension SessionUtil {
groupAdmins: admins?
.map { memberId in
GroupMember(
groupId: groupPublicKey,
groupId: legacyGroupPublicKey,
profileId: memberId,
role: .admin,
isHidden: false
@ -758,6 +758,52 @@ public extension SessionUtil {
// MARK: -- Group Changes
static func add(
_ db: Database,
groupIdentityPublicKey: String,
groupIdentityPrivateKey: Data?,
name: String,
tag: Data?,
subkey: Data?
) throws {
try SessionUtil.performAndPushChange(
db,
for: .userGroups,
publicKey: getUserHexEncodedPublicKey(db)
) { conf in
guard conf != nil else { throw SessionUtilError.nilConfigObject }
var cGroupId: [CChar] = groupIdentityPublicKey.cArray.nullTerminated()
}
}
static func update(
_ db: Database,
groupIdentityPublicKey: String,
groupIdentityPrivateKey: Data? = nil,
name: String? = nil,
tag: Data? = nil,
subkey: Data? = nil
) throws {
try SessionUtil.performAndPushChange(
db,
for: .userGroups,
publicKey: getUserHexEncodedPublicKey(db)
) { conf in
try SessionUtil.upsert(
groups: [
GroupInfo(
groupIdentityPublicKey: groupIdentityPublicKey,
groupIdentityPrivateKey: groupIdentityPrivateKey,
name: name,
tag: tag,
subkey: subkey
)
],
in: conf
)
}
}
static func remove(_ db: Database, groupIds: [String]) throws {
guard !groupIds.isEmpty else { return }
@ -768,13 +814,6 @@ public extension SessionUtil {
extension SessionUtil {
struct LegacyGroupInfo: Decodable, FetchableRecord, ColumnExpressible {
private static let threadIdKey: SQL = SQL(stringLiteral: CodingKeys.threadId.stringValue)
private static let nameKey: SQL = SQL(stringLiteral: CodingKeys.name.stringValue)
private static let lastKeyPairKey: SQL = SQL(stringLiteral: CodingKeys.lastKeyPair.stringValue)
private static let disappearingConfigKey: SQL = SQL(stringLiteral: CodingKeys.disappearingConfig.stringValue)
private static let priorityKey: SQL = SQL(stringLiteral: CodingKeys.priority.stringValue)
private static let joinedAtKey: SQL = SQL(stringLiteral: CodingKeys.joinedAt.stringValue)
typealias Columns = CodingKeys
enum CodingKeys: String, CodingKey, ColumnExpression, CaseIterable {
case threadId
@ -821,15 +860,11 @@ extension SessionUtil {
static func fetchAll(_ db: Database) throws -> [LegacyGroupInfo] {
let closedGroup: TypedTableAlias<ClosedGroup> = TypedTableAlias()
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
let keyPair: TypedTableAlias<ClosedGroupKeyPair> = TypedTableAlias()
let prefixLiteral: SQL = SQL(stringLiteral: "\(SessionId.Prefix.standard.rawValue)%")
let keyPairThreadIdColumnLiteral: SQL = SQL(stringLiteral: ClosedGroupKeyPair.Columns.threadId.name)
let receivedTimestampColumnLiteral: SQL = SQL(stringLiteral: ClosedGroupKeyPair.Columns.receivedTimestamp.name)
let threadIdColumnLiteral: SQL = SQL(stringLiteral: DisappearingMessagesConfiguration.Columns.threadId.name)
let lastKeyPair: TypedTableAlias<ClosedGroupKeyPair> = TypedTableAlias(LegacyGroupInfo.self, column: .lastKeyPair)
let disappearingConfig: TypedTableAlias<DisappearingMessagesConfiguration> = TypedTableAlias(LegacyGroupInfo.self, column: .disappearingConfig)
/// **Note:** The `numColumnsBeforeTypes` value **MUST** match the number of fields before
/// the `LegacyGroupInfo.lastKeyPairKey` entry below otherwise the query will fail to
/// the `lastKeyPair` entry below otherwise the query will fail to
/// parse and might throw
///
/// Explicitly set default values for the fields ignored for search results
@ -837,28 +872,28 @@ extension SessionUtil {
let request: SQLRequest<LegacyGroupInfo> = """
SELECT
\(closedGroup[.threadId]) AS \(LegacyGroupInfo.threadIdKey),
\(closedGroup[.name]) AS \(LegacyGroupInfo.nameKey),
\(closedGroup[.formationTimestamp]) AS \(LegacyGroupInfo.joinedAtKey),
\(thread[.pinnedPriority]) AS \(LegacyGroupInfo.priorityKey),
\(LegacyGroupInfo.lastKeyPairKey).*,
\(LegacyGroupInfo.disappearingConfigKey).*
\(closedGroup[.threadId]) AS \(LegacyGroupInfo.Columns.threadId),
\(closedGroup[.name]) AS \(LegacyGroupInfo.Columns.name),
\(closedGroup[.formationTimestamp]) AS \(LegacyGroupInfo.Columns.joinedAt),
\(thread[.pinnedPriority]) AS \(LegacyGroupInfo.Columns.priority),
\(lastKeyPair.allColumns),
\(disappearingConfig.allColumns)
FROM \(ClosedGroup.self)
JOIN \(SessionThread.self) ON \(thread[.id]) = \(closedGroup[.threadId])
LEFT JOIN (
SELECT
\(keyPair[.threadId]),
\(keyPair[.publicKey]),
\(keyPair[.secretKey]),
MAX(\(keyPair[.receivedTimestamp])) AS \(receivedTimestampColumnLiteral),
\(keyPair[.threadKeyPairHash])
\(lastKeyPair[.threadId]),
\(lastKeyPair[.publicKey]),
\(lastKeyPair[.secretKey]),
MAX(\(lastKeyPair[.receivedTimestamp])) AS \(ClosedGroupKeyPair.Columns.receivedTimestamp),
\(lastKeyPair[.threadKeyPairHash])
FROM \(ClosedGroupKeyPair.self)
GROUP BY \(keyPair[.threadId])
) AS \(LegacyGroupInfo.lastKeyPairKey) ON \(LegacyGroupInfo.lastKeyPairKey).\(keyPairThreadIdColumnLiteral) = \(closedGroup[.threadId])
LEFT JOIN \(DisappearingMessagesConfiguration.self) AS \(LegacyGroupInfo.disappearingConfigKey) ON \(LegacyGroupInfo.disappearingConfigKey).\(threadIdColumnLiteral) = \(closedGroup[.threadId])
GROUP BY \(lastKeyPair[.threadId])
) AS \(lastKeyPair) ON \(lastKeyPair[.threadId]) = \(closedGroup[.threadId])
LEFT JOIN \(disappearingConfig) ON \(disappearingConfig[.threadId]) = \(closedGroup[.threadId])
WHERE \(SQL("\(closedGroup[.threadId]) LIKE '\(prefixLiteral)'"))
WHERE \(SQL("\(closedGroup[.threadId]) LIKE '\(SessionId.Prefix.standard)%'"))
"""
let legacyGroupInfoNoMembers: [LegacyGroupInfo] = try request
@ -869,9 +904,9 @@ extension SessionUtil {
DisappearingMessagesConfiguration.numberOfSelectedColumns(db)
])
return ScopeAdapter([
CodingKeys.lastKeyPair.stringValue: adapters[1],
CodingKeys.disappearingConfig.stringValue: adapters[2]
return ScopeAdapter.with(LegacyGroupInfo.self, [
.lastKeyPair: adapters[1],
.disappearingConfig: adapters[2]
])
}
.fetchAll(db)
@ -898,7 +933,43 @@ extension SessionUtil {
}
}
}
}
// MARK: - GroupInfo
extension SessionUtil {
struct GroupInfo {
let groupIdentityPublicKey: String
let groupIdentityPrivateKey: Data?
let name: String?
let tag: Data?
let subkey: Data?
let priority: Int32?
let joinedAt: Int64?
init(
groupIdentityPublicKey: String,
groupIdentityPrivateKey: Data? = nil,
name: String? = nil,
tag: Data? = nil,
subkey: Data? = nil,
priority: Int32? = nil,
joinedAt: Int64? = nil
) {
self.groupIdentityPublicKey = groupIdentityPublicKey
self.groupIdentityPrivateKey = groupIdentityPrivateKey
self.name = name
self.tag = tag
self.subkey = subkey
self.priority = priority
self.joinedAt = joinedAt
}
}
}
// MARK: - CommunityInfo
extension SessionUtil {
struct CommunityInfo {
let urlInfo: OpenGroupUrlInfo
let priority: Int32?
@ -911,13 +982,21 @@ extension SessionUtil {
self.priority = priority
}
}
}
// MARK: - GroupThreadData
extension SessionUtil {
fileprivate struct GroupThreadData {
let communities: [PrioritisedData<SessionUtil.OpenGroupUrlInfo>]
let legacyGroups: [PrioritisedData<LegacyGroupInfo>]
let groups: [PrioritisedData<String>]
let groups: [PrioritisedData<GroupInfo>]
}
}
// MARK: - PrioritisedData
extension SessionUtil {
fileprivate struct PrioritisedData<T> {
let data: T
let priority: Int32

View File

@ -114,6 +114,15 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
case is QueryInterfaceRequest<SessionThread>:
return try SessionUtil.updatingThreads(db, updatedData)
case is QueryInterfaceRequest<ClosedGroup>:
return try SessionUtil.updatingGroupInfo(db, updatedData)
case is QueryInterfaceRequest<DisappearingMessagesConfiguration>:
let oneToOneUpdates: [RowDecoder] = try SessionUtil.updatingDisappearingConfigsOneToOne(db, updatedData)
let groupUpdates: [RowDecoder] = try SessionUtil.updatingDisappearingConfigsGroups(db, updatedData)
return (oneToOneUpdates + groupUpdates)
default: return updatedData
}

View File

@ -74,27 +74,16 @@ public enum SessionUtil {
}
}
public static func loadState(
_ db: Database? = nil,
userPublicKey: String,
ed25519SecretKey: [UInt8]?
) {
public static func loadState(_ db: Database, using dependencies: Dependencies = Dependencies()) {
// Ensure we have the ed25519 key and that we haven't already loaded the state before
// we continue
guard
let secretKey: [UInt8] = ed25519SecretKey,
let ed25519SecretKey: [UInt8] = Identity.fetchUserEd25519KeyPair(db)?.secretKey,
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 currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let existingDumps: Set<ConfigDump> = ((try? ConfigDump.fetchSet(db)) ?? [])
let existingDumpVariants: Set<ConfigDump.Variant> = existingDumps
.map { $0.variant }
@ -102,6 +91,11 @@ public enum SessionUtil {
let missingRequiredVariants: Set<ConfigDump.Variant> = ConfigDump.Variant.userVariants
.asSet()
.subtracting(existingDumpVariants)
let groupsByKey: [String: Data] = (try? ClosedGroup
.filter(ids: existingDumps.map { $0.publicKey })
.fetchAll(db)
.reduce(into: [:]) { result, next in result[next.threadId] = next.groupIdentityPrivateKey })
.defaulting(to: [:])
// Create the 'config_object' records for each dump
SessionUtil.configStore.mutate { confStore in
@ -109,17 +103,21 @@ public enum SessionUtil {
confStore[ConfigKey(variant: dump.variant, publicKey: dump.publicKey)] = Atomic(
try? SessionUtil.loadState(
for: dump.variant,
secretKey: secretKey,
publicKey: dump.publicKey,
userEd25519SecretKey: ed25519SecretKey,
groupEd25519SecretKey: groupsByKey[dump.publicKey].map { Array($0) },
cachedData: dump.data
)
)
}
missingRequiredVariants.forEach { variant in
confStore[ConfigKey(variant: variant, publicKey: userPublicKey)] = Atomic(
confStore[ConfigKey(variant: variant, publicKey: currentUserPublicKey)] = Atomic(
try? SessionUtil.loadState(
for: variant,
secretKey: secretKey,
publicKey: currentUserPublicKey,
userEd25519SecretKey: ed25519SecretKey,
groupEd25519SecretKey: nil,
cachedData: nil
)
)
@ -127,14 +125,27 @@ public enum SessionUtil {
}
}
public static func storeState(
_ configs: [ConfigDump.Variant: UnsafeMutablePointer<config_object>?],
publicKey: String
) {
SessionUtil.configStore.mutate { confStore in
configs.forEach { variant, conf in
confStore[ConfigKey(variant: variant, publicKey: publicKey)] = Atomic(conf)
}
}
}
private static func loadState(
for variant: ConfigDump.Variant,
secretKey ed25519SecretKey: [UInt8],
publicKey: String,
userEd25519SecretKey: [UInt8],
groupEd25519SecretKey: [UInt8]?,
cachedData: Data?
) throws -> UnsafeMutablePointer<config_object>? {
// Setup initial variables (including getting the memory address for any cached data)
var conf: UnsafeMutablePointer<config_object>? = nil
let error: UnsafeMutablePointer<CChar>? = nil
var error: [CChar] = [CChar](repeating: 0, count: 256)
let cachedDump: (data: UnsafePointer<UInt8>, length: Int)? = cachedData?.withUnsafeBytes { unsafeBytes in
return unsafeBytes.baseAddress.map {
(
@ -144,33 +155,35 @@ public enum SessionUtil {
}
}
// No need to deallocate the `cachedDump.data` as it'll automatically be cleaned up by
// the `cachedDump` lifecycle, but need to deallocate the `error` if it gets set
defer {
error?.deallocate()
}
// Try to create the object
var secretKey: [UInt8] = ed25519SecretKey
var secretKey: [UInt8]? = userEd25519SecretKey
let result: Int32 = {
switch variant {
case .userProfile:
return user_profile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
return user_profile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .contacts:
return contacts_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
return contacts_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .convoInfoVolatile:
return convo_info_volatile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
return convo_info_volatile_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .userGroups:
return user_groups_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), error)
return user_groups_init(&conf, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .groupInfo:
return groups_info_init(&conf, &secretKey, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .groupMembers:
return groups_members_init(&conf, &secretKey, &secretKey, cachedDump?.data, (cachedDump?.length ?? 0), &error)
case .groupKeys:
preconditionFailure()
}
}()
guard result == 0 else {
let errorString: String = (error.map { String(cString: $0) } ?? "unknown error")
SNLog("[SessionUtil Error] Unable to create \(variant.rawValue) config object: \(errorString)")
SNLog("[SessionUtil Error] Unable to create \(variant.rawValue) config object: \(String(cString: error))")
throw SessionUtilError.unableToCreateConfigObject
}
@ -414,6 +427,30 @@ public enum SessionUtil {
mergeNeedsDump: config_needs_dump(conf),
latestConfigSentTimestampMs: latestConfigSentTimestampMs
)
case .groupInfo:
try SessionUtil.handleGroupInfoUpdate(
db,
in: conf,
mergeNeedsDump: config_needs_dump(conf),
latestConfigSentTimestampMs: latestConfigSentTimestampMs
)
case .groupMembers:
try SessionUtil.handleGroupMembersUpdate(
db,
in: conf,
mergeNeedsDump: config_needs_dump(conf),
latestConfigSentTimestampMs: latestConfigSentTimestampMs
)
case .groupKeys:
try SessionUtil.handleGroupKeysUpdate(
db,
in: conf,
mergeNeedsDump: config_needs_dump(conf),
latestConfigSentTimestampMs: latestConfigSentTimestampMs
)
}
}
catch {

View File

@ -87,6 +87,7 @@ public extension Crypto.Verification {
public extension Crypto.Size {
static let signature: Crypto.Size = Crypto.Size(id: "signature") { Sodium().sign.Bytes }
static let publicKey: Crypto.Size = Crypto.Size(id: "publicKey") { Sodium().sign.PublicKeyBytes }
static let secretKey: Crypto.Size = Crypto.Size(id: "secretKey") { Sodium().sign.SecretKeyBytes }
}
public extension Crypto.Action {
@ -149,4 +150,26 @@ public extension Crypto.KeyPairType {
return KeyPair(publicKey: Array(keyPair.publicKey), secretKey: Array(keyPair.privateKey))
}
}
static func ed25519KeyPair(
seed: Data? = nil,
using dependencies: Dependencies = Dependencies()
) -> Crypto.KeyPairType {
return Crypto.KeyPairType(id: "ed25519KeyPair") {
let pkSize: Int = dependencies.crypto.size(.publicKey)
let skSize: Int = dependencies.crypto.size(.secretKey)
var edPK: [UInt8] = [UInt8](repeating: 0, count: pkSize)
var edSK: [UInt8] = [UInt8](repeating: 0, count: skSize)
var targetSeed: [UInt8] = ((seed ?? (try? Randomness.generateRandomBytes(numberBytes: skSize)))
.map { Array($0) })
.defaulting(to: [])
// Generate the key
guard Sodium.lib_crypto_sign_ed25519_seed_keypair(&edPK, &edSK, &targetSeed) == 0 else {
return nil
}
return KeyPair(publicKey: edPK, secretKey: edSK)
}
}
}

View File

@ -0,0 +1,553 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
extension LibSessionSpec {
class ConfigContacts {
enum ContactProperty: CaseIterable {
case name
case nickname
case approved
case approved_me
case blocked
case profile_pic
case created
case notifications
case mute_until
}
static func tests() {
context("CONTACTS") {
// MARK: - when checking error catching
context("when checking error catching") {
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: [CChar]!
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = [CChar](repeating: 0, count: 256)
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, &error)
}
afterEach {
error = nil
conf = nil
}
// MARK: -- it can catch size limit errors thrown when pushing
it("can catch size limit errors thrown when pushing") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
try (0..<10000).forEach { index in
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: .allProperties
)
contacts_set(conf, &contact)
}
expect(contacts_size(conf)).to(equal(10000))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
expect {
try CExceptionHelper.performSafely { config_push(conf).deallocate() }
}
.to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"])))
}
}
// MARK: - when checking size limits
context("when checking size limits") {
var numRecords: Int!
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: [CChar]!
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
numRecords = 0
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = [CChar](repeating: 0, count: 256)
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, &error)
}
afterEach {
error = nil
conf = nil
}
// MARK: -- has not changed the max empty records
it("has not changed the max empty records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(2370))
}
// MARK: -- has not changed the max name only records
it("has not changed the max name only records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: [.name]
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(796))
}
// MARK: -- has not changed the max name and profile pic only records
it("has not changed the max name and profile pic only records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: [.name, .profile_pic]
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(290))
}
// MARK: -- has not changed the max filled records
it("has not changed the max filled records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: .allProperties
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(236))
}
}
// MARK: - generates config correctly
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf, &edSK, nil, 0, &error)).to(equal(0))
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
expect(contacts_size(conf)).to(equal(0))
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact2.name)).to(beEmpty())
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
expect(contact2.created).to(equal(0))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(contact2.mute_until).to(equal(0))
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
pushData1.deallocate()
// Update the contact data
contact2.name = "Joe".toLibSession()
contact2.nickname = "Joey".toLibSession()
contact2.approved = true
contact2.approved_me = true
contact2.created = createdTs
contact2.notifications = CONVO_NOTIFY_ALL
contact2.mute_until = nowTs + 1800
// Update the contact
contacts_set(conf, &contact2)
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
expect(contact3.blocked).to(beFalse())
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
expect(contact3.created).to(equal(createdTs))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(contact2.mute_until).to(equal(nowTs + 1800))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
expect(pushData2.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
pushData2.deallocate()
// NB: Not going to check encrypted data and decryption here because that's general (not
// specific to contacts) and is covered already in the user profile tests.
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData3.pointee.seqno).to(equal(1))
pushData3.deallocate()
// Because we just called dump() above, to load up contacts2
expect(config_needs_dump(conf)).to(beFalse())
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
expect(contact4.blocked).to(beFalse())
expect(contact4.created).to(equal(createdTs))
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
expect(String(libSessionVal: contact5.name)).to(beEmpty())
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
expect(contact5.blocked).to(beFalse())
// We're not setting any fields, but we should still keep a record of the session id
contacts_set(conf2, &contact5)
expect(config_needs_push(conf2)).to(beTrue())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2)
mergeHashes.forEach { $0?.deallocate() }
pushData4.deallocate()
expect(config_needs_push(conf)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData5.pointee.seqno).to(equal(2))
pushData5.deallocate()
// Iterate through and make sure we got everything we expected
var sessionIds: [String] = []
var nicknames: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact6: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(String(libSessionVal: contact6.session_id))
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.count).to(equal(contacts_size(conf)))
expect(sessionIds.first).to(equal(definitelyRealId))
expect(sessionIds.last).to(equal(anotherId))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
// Conflict! Oh no!
// On client 1 delete a contact:
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
contact7.nickname = "Nickname 3".toLibSession()
contact7.approved = true
contact7.approved_me = true
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData6.pointee.seqno).to(equal(3))
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(3))
let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)!
let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)!
expect(pushData6Str).toNot(equal(pushData7Str))
expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len))
.to(equal([fakeHash2]))
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash2]))
let fakeHash3a: String = "fakehash3a"
var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated()
let fakeHash3b: String = "fakehash3b"
var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated()
config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a)
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b)
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3b].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData7.pointee.config)]
var mergeSize2: [Int] = [pushData7.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(config_needs_push(conf)).to(beTrue())
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3a].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData6.pointee.config)]
var mergeSize3: [Int] = [pushData6.pointee.config_len]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beTrue())
mergeHashes2.forEach { $0?.deallocate() }
mergeHashes3.forEach { $0?.deallocate() }
pushData6.deallocate()
pushData7.deallocate()
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData8.pointee.seqno).to(equal(4))
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno))
let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)!
let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)!
expect(pushData8Str).to(equal(pushData9Str))
expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len))
.to(equal([fakeHash3b, fakeHash3a]))
expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len))
.to(equal([fakeHash3a, fakeHash3b]))
let fakeHash4: String = "fakeHash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4)
pushData8.deallocate()
pushData9.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Validate the changes
var sessionIds2: [String] = []
var nicknames2: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(String(libSessionVal: contact8.session_id))
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator2)
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(anotherId))
expect(sessionIds2.last).to(equal(thirdId))
expect(nicknames2.first).to(equal("(N/A)"))
expect(nicknames2.last).to(equal("Nickname 3"))
}
}
}
// MARK: - Convenience
private static func createContact(
for index: Int,
in conf: UnsafeMutablePointer<config_object>?,
rand: inout ARC4RandomNumberGenerator,
maxing properties: [ContactProperty] = []
) throws -> contacts_contact {
let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())"
let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count))
var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
var contact: contacts_contact = contacts_contact()
guard contacts_get_or_construct(conf, &contact, &cSessionId) else {
throw SessionUtilError.getOrConstructFailedUnexpectedly
}
// Set the values to the maximum data that can fit
properties.forEach { property in
switch property {
case .approved: contact.approved = true
case .approved_me: contact.approved_me = true
case .blocked: contact.blocked = true
case .created: contact.created = Int64.max
case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY
case .mute_until: contact.mute_until = Int64.max
case .name:
contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
.toHexString()
.toLibSession()
case .nickname:
contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
.toHexString()
.toLibSession()
case .profile_pic:
contact.profile_pic = user_profile_pic(
url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength)
.toHexString()
.toLibSession(),
key: Data(rand.nextBytes(count: 32))
.toLibSession()
)
}
}
return contact
}
}
}
fileprivate extension Array where Element == LibSessionSpec.ConfigContacts.ContactProperty {
static var allProperties: [LibSessionSpec.ConfigContacts.ContactProperty] = LibSessionSpec.ConfigContacts.ContactProperty.allCases
}

View File

@ -1,547 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigContactsSpec {
enum ContactProperty: CaseIterable {
case name
case nickname
case approved
case approved_me
case blocked
case profile_pic
case created
case notifications
case mute_until
}
// MARK: - Spec
static func spec() {
context("CONTACTS") {
// MARK: - when checking error catching
context("when checking error catching") {
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: UnsafeMutablePointer<CChar>?
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = nil
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, error)
error?.deallocate()
}
// MARK: -- it can catch size limit errors thrown when pushing
it("can catch size limit errors thrown when pushing") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
try (0..<10000).forEach { index in
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: .allProperties
)
contacts_set(conf, &contact)
}
expect(contacts_size(conf)).to(equal(10000))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
expect {
try CExceptionHelper.performSafely { config_push(conf).deallocate() }
}
.to(throwError(NSError(domain: "cpp_exception", code: -2, userInfo: ["NSLocalizedDescription": "Config data is too large"])))
}
}
// MARK: - when checking size limits
context("when checking size limits") {
var numRecords: Int!
var seed: Data!
var identity: (ed25519KeyPair: KeyPair, x25519KeyPair: KeyPair)!
var edSK: [UInt8]!
var error: UnsafeMutablePointer<CChar>?
var conf: UnsafeMutablePointer<config_object>?
beforeEach {
numRecords = 0
seed = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
identity = try! Identity.generate(from: seed)
edSK = identity.ed25519KeyPair.secretKey
// Initialize a brand new, empty config because we have no dump data to deal with.
error = nil
conf = nil
_ = contacts_init(&conf, &edSK, nil, 0, error)
error?.deallocate()
}
// MARK: -- has not changed the max empty records
it("has not changed the max empty records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(2370))
}
// MARK: -- has not changed the max name only records
it("has not changed the max name only records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: [.name]
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(796))
}
// MARK: -- has not changed the max name and profile pic only records
it("has not changed the max name and profile pic only records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: [.name, .profile_pic]
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(290))
}
// MARK: -- has not changed the max filled records
it("has not changed the max filled records") {
var randomGenerator: ARC4RandomNumberGenerator = ARC4RandomNumberGenerator(seed: 1000)
for index in (0..<100000) {
var contact: contacts_contact = try createContact(
for: index,
in: conf,
rand: &randomGenerator,
maxing: .allProperties
)
contacts_set(conf, &contact)
do { try CExceptionHelper.performSafely { config_push(conf).deallocate() } }
catch { break }
// We successfully inserted a contact and didn't hit the limit so increment the counter
numRecords += 1
}
// Check that the record count matches the maximum when we last checked
expect(numRecords).to(equal(236))
}
}
// MARK: - generates config correctly
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "050000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let contactPtr: UnsafeMutablePointer<contacts_contact>? = nil
expect(contacts_get(conf, contactPtr, &cDefinitelyRealId)).to(beFalse())
expect(contacts_size(conf)).to(equal(0))
var contact2: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact2.name)).to(beEmpty())
expect(String(libSessionVal: contact2.nickname)).to(beEmpty())
expect(contact2.approved).to(beFalse())
expect(contact2.approved_me).to(beFalse())
expect(contact2.blocked).to(beFalse())
expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty())
expect(contact2.created).to(equal(0))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(contact2.mute_until).to(equal(0))
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
pushData1.deallocate()
// Update the contact data
contact2.name = "Joe".toLibSession()
contact2.nickname = "Joey".toLibSession()
contact2.approved = true
contact2.approved_me = true
contact2.created = createdTs
contact2.notifications = CONVO_NOTIFY_ALL
contact2.mute_until = nowTs + 1800
// Update the contact
contacts_set(conf, &contact2)
// Ensure the contact details were updated
var contact3: contacts_contact = contacts_contact()
expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact3.name)).to(equal("Joe"))
expect(String(libSessionVal: contact3.nickname)).to(equal("Joey"))
expect(contact3.approved).to(beTrue())
expect(contact3.approved_me).to(beTrue())
expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty())
expect(contact3.blocked).to(beFalse())
expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId))
expect(contact3.created).to(equal(createdTs))
expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(contact2.mute_until).to(equal(nowTs + 1800))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
// incremented since we made changes (this only increments once between
// dumps; even though we changed multiple fields here).
expect(pushData2.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
pushData2.deallocate()
// NB: Not going to check encrypted data and decryption here because that's general (not
// specific to contacts) and is covered already in the user profile tests.
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(contacts_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData3.pointee.seqno).to(equal(1))
pushData3.deallocate()
// Because we just called dump() above, to load up contacts2
expect(config_needs_dump(conf)).to(beFalse())
// Ensure the contact details were updated
var contact4: contacts_contact = contacts_contact()
expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue())
expect(String(libSessionVal: contact4.name)).to(equal("Joe"))
expect(String(libSessionVal: contact4.nickname)).to(equal("Joey"))
expect(contact4.approved).to(beTrue())
expect(contact4.approved_me).to(beTrue())
expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty())
expect(contact4.blocked).to(beFalse())
expect(contact4.created).to(equal(createdTs))
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var contact5: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue())
expect(String(libSessionVal: contact5.name)).to(beEmpty())
expect(String(libSessionVal: contact5.nickname)).to(beEmpty())
expect(contact5.approved).to(beFalse())
expect(contact5.approved_me).to(beFalse())
expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently
expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty())
expect(contact5.blocked).to(beFalse())
// We're not setting any fields, but we should still keep a record of the session id
contacts_set(conf2, &contact5)
expect(config_needs_push(conf2)).to(beTrue())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf2, pushData4.pointee.seqno, &cFakeHash2)
mergeHashes.forEach { $0?.deallocate() }
pushData4.deallocate()
expect(config_needs_push(conf)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData5.pointee.seqno).to(equal(2))
pushData5.deallocate()
// Iterate through and make sure we got everything we expected
var sessionIds: [String] = []
var nicknames: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact6: contacts_contact = contacts_contact()
let contactIterator: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator, &contact6) {
sessionIds.append(String(libSessionVal: contact6.session_id))
nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator)
}
contacts_iterator_free(contactIterator) // Need to free the iterator
expect(sessionIds.count).to(equal(2))
expect(sessionIds.count).to(equal(contacts_size(conf)))
expect(sessionIds.first).to(equal(definitelyRealId))
expect(sessionIds.last).to(equal(anotherId))
expect(nicknames.first).to(equal("Joey"))
expect(nicknames.last).to(equal("(N/A)"))
// Conflict! Oh no!
// On client 1 delete a contact:
contacts_erase(conf, definitelyRealId)
// Client 2 adds a new friend:
let thirdId: String = "052222222222222222222222222222222222222222222222222222222222222222"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var contact7: contacts_contact = contacts_contact()
expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue())
contact7.nickname = "Nickname 3".toLibSession()
contact7.approved = true
contact7.approved_me = true
contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession()
contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession()
contacts_set(conf2, &contact7)
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData6.pointee.seqno).to(equal(3))
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(3))
let pushData6Str: String = String(pointer: pushData6.pointee.config, length: pushData6.pointee.config_len, encoding: .ascii)!
let pushData7Str: String = String(pointer: pushData7.pointee.config, length: pushData7.pointee.config_len, encoding: .ascii)!
expect(pushData6Str).toNot(equal(pushData7Str))
expect([String](pointer: pushData6.pointee.obsolete, count: pushData6.pointee.obsolete_len))
.to(equal([fakeHash2]))
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash2]))
let fakeHash3a: String = "fakehash3a"
var cFakeHash3a: [CChar] = fakeHash3a.cArray.nullTerminated()
let fakeHash3b: String = "fakehash3b"
var cFakeHash3b: [CChar] = fakeHash3b.cArray.nullTerminated()
config_confirm_pushed(conf, pushData6.pointee.seqno, &cFakeHash3a)
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash3b)
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3b].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData7.pointee.config)]
var mergeSize2: [Int] = [pushData7.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(config_needs_push(conf)).to(beTrue())
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3a].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData6.pointee.config)]
var mergeSize3: [Int] = [pushData6.pointee.config_len]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beTrue())
mergeHashes2.forEach { $0?.deallocate() }
mergeHashes3.forEach { $0?.deallocate() }
pushData6.deallocate()
pushData7.deallocate()
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData8.pointee.seqno).to(equal(4))
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(pushData8.pointee.seqno))
let pushData8Str: String = String(pointer: pushData8.pointee.config, length: pushData8.pointee.config_len, encoding: .ascii)!
let pushData9Str: String = String(pointer: pushData9.pointee.config, length: pushData9.pointee.config_len, encoding: .ascii)!
expect(pushData8Str).to(equal(pushData9Str))
expect([String](pointer: pushData8.pointee.obsolete, count: pushData8.pointee.obsolete_len))
.to(equal([fakeHash3b, fakeHash3a]))
expect([String](pointer: pushData9.pointee.obsolete, count: pushData9.pointee.obsolete_len))
.to(equal([fakeHash3a, fakeHash3b]))
let fakeHash4: String = "fakeHash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
config_confirm_pushed(conf, pushData8.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData9.pointee.seqno, &cFakeHash4)
pushData8.deallocate()
pushData9.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Validate the changes
var sessionIds2: [String] = []
var nicknames2: [String] = []
expect(contacts_size(conf)).to(equal(2))
var contact8: contacts_contact = contacts_contact()
let contactIterator2: UnsafeMutablePointer<contacts_iterator> = contacts_iterator_new(conf)
while !contacts_iterator_done(contactIterator2, &contact8) {
sessionIds2.append(String(libSessionVal: contact8.session_id))
nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)")
contacts_iterator_advance(contactIterator2)
}
contacts_iterator_free(contactIterator2) // Need to free the iterator
expect(sessionIds2.count).to(equal(2))
expect(sessionIds2.first).to(equal(anotherId))
expect(sessionIds2.last).to(equal(thirdId))
expect(nicknames2.first).to(equal("(N/A)"))
expect(nicknames2.last).to(equal("Nickname 3"))
}
}
}
// MARK: - Convenience
private static func createContact(
for index: Int,
in conf: UnsafeMutablePointer<config_object>?,
rand: inout ARC4RandomNumberGenerator,
maxing properties: [ContactProperty] = []
) throws -> contacts_contact {
let postPrefixId: String = "05\(rand.nextBytes(count: 32).toHexString())"
let sessionId: String = ("05\(index)a" + postPrefixId.suffix(postPrefixId.count - "05\(index)a".count))
var cSessionId: [CChar] = sessionId.cArray.nullTerminated()
var contact: contacts_contact = contacts_contact()
guard contacts_get_or_construct(conf, &contact, &cSessionId) else {
throw SessionUtilError.getOrConstructFailedUnexpectedly
}
// Set the values to the maximum data that can fit
properties.forEach { property in
switch property {
case .approved: contact.approved = true
case .approved_me: contact.approved_me = true
case .blocked: contact.blocked = true
case .created: contact.created = Int64.max
case .notifications: contact.notifications = CONVO_NOTIFY_MENTIONS_ONLY
case .mute_until: contact.mute_until = Int64.max
case .name:
contact.name = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
.toHexString()
.toLibSession()
case .nickname:
contact.nickname = rand.nextBytes(count: SessionUtil.libSessionMaxNameByteLength)
.toHexString()
.toLibSession()
case .profile_pic:
contact.profile_pic = user_profile_pic(
url: rand.nextBytes(count: SessionUtil.libSessionMaxProfileUrlByteLength)
.toHexString()
.toLibSession(),
key: Data(rand.nextBytes(count: 32))
.toLibSession()
)
}
}
return contact
}
}
fileprivate extension Array where Element == ConfigContactsSpec.ContactProperty {
static var allProperties: [ConfigContactsSpec.ContactProperty] = ConfigContactsSpec.ContactProperty.allCases
}

View File

@ -0,0 +1,265 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
extension LibSessionSpec {
class ConfigConvoInfoVolatile {
static func tests() {
context("CONVO_INFO_VOLATILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf, &edSK, nil, 0, &error)).to(equal(0))
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse())
expect(convo_info_volatile_size(conf)).to(equal(0))
var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId))
.to(beTrue())
expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId))
expect(oneToOne2.last_read).to(equal(0))
expect(oneToOne2.unread).to(beFalse())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// Update the last read
let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
oneToOne2.last_read = nowTimestampMs
// The new data doesn't get stored until we call this:
convo_info_volatile_set_1to1(conf, &oneToOne2)
var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId))
.to(beFalse())
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue())
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
let openGroupBaseUrl: String = "http://Example.ORG:5678"
var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated()
let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased()
// ("http://Example.ORG:5678"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
// )
let openGroupRoom: String = "SudokuRoom"
var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated()
let openGroupRoomResult: String = openGroupRoom.lowercased()
// ("SudokuRoom"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (65 - openGroupRoom.count))
// )
var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.bytes
var community1: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.unread = true
// The new data doesn't get stored until we call this:
convo_info_volatile_set_community(conf, &community1);
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
pushData1.deallocate()
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true))
expect(oneToOne4.last_read).to(equal(nowTimestampMs))
expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId))
expect(oneToOne4.unread).to(beFalse())
var community2: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community2.unread = true
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue())
oneToOne5.unread = true
convo_info_volatile_set_1to1(conf2, &oneToOne5)
let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue())
legacyGroup2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
expect(config_needs_push(conf2)).to(beTrue())
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData2.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
var mergeSize: [Int] = [pushData2.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
expect(convo_info_volatile_size(conf)).to(equal(4))
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
expect(convo_info_volatile_size_communities(conf)).to(equal(1))
expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1))
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var c2: convo_info_volatile_community = convo_info_volatile_community()
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
while !convo_info_volatile_iterator_done(it) {
if convo_info_volatile_it_is_1to1(it, &c1) {
seen.append("1-to-1: \(String(libSessionVal: c1.session_id))")
}
else if convo_info_volatile_it_is_community(it, &c2) {
seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else if convo_info_volatile_it_is_legacy_group(it, &c3) {
seen.append("cl: \(String(libSessionVal: c3.group_id))")
}
convo_info_volatile_iterator_advance(it)
}
convo_info_volatile_iterator_free(it)
expect(seen).to(equal([
"1-to-1: 051111111111111111111111111111111111111111111111111111111111111111",
"1-to-1: 055000000000000000000000000000000000000000000000000000000000000000",
"og: http://example.org:5678/r/sudokuroom",
"cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000"
var cFourthId: [CChar] = fourthId.cArray.nullTerminated()
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cFourthId)
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId)
expect(config_needs_push(conf)).to(beTrue())
expect(convo_info_volatile_size(conf)).to(equal(3))
expect(convo_info_volatile_size_1to1(conf)).to(equal(1))
// Check the single-type iterators:
var seen1: [String?] = []
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf)
while !convo_info_volatile_iterator_done(it1) {
expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue())
seen1.append(String(libSessionVal: c1.session_id))
convo_info_volatile_iterator_advance(it1)
}
convo_info_volatile_iterator_free(it1)
expect(seen1).to(equal([
"051111111111111111111111111111111111111111111111111111111111111111"
]))
var seen2: [String?] = []
var c2: convo_info_volatile_community = convo_info_volatile_community()
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
while !convo_info_volatile_iterator_done(it2) {
expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue())
seen2.append(String(libSessionVal: c2.base_url))
convo_info_volatile_iterator_advance(it2)
}
convo_info_volatile_iterator_free(it2)
expect(seen2).to(equal([
"http://example.org:5678"
]))
var seen3: [String?] = []
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
while !convo_info_volatile_iterator_done(it3) {
expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue())
seen3.append(String(libSessionVal: c3.group_id))
convo_info_volatile_iterator_advance(it3)
}
convo_info_volatile_iterator_free(it3)
expect(seen3).to(equal([
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
}
}
}
}

View File

@ -1,267 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigConvoInfoVolatileSpec {
// MARK: - Spec
static func spec() {
context("CONVO_INFO_VOLATILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
var oneToOne1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf, &oneToOne1, &cDefinitelyRealId)).to(beFalse())
expect(convo_info_volatile_size(conf)).to(equal(0))
var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId))
.to(beTrue())
expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId))
expect(oneToOne2.last_read).to(equal(0))
expect(oneToOne2.unread).to(beFalse())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// Update the last read
let nowTimestampMs: Int64 = Int64(floor(Date().timeIntervalSince1970 * 1000))
oneToOne2.last_read = nowTimestampMs
// The new data doesn't get stored until we call this:
convo_info_volatile_set_1to1(conf, &oneToOne2)
var legacyGroup1: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
var oneToOne3: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_legacy_group(conf, &legacyGroup1, &cDefinitelyRealId))
.to(beFalse())
expect(convo_info_volatile_get_1to1(conf, &oneToOne3, &cDefinitelyRealId)).to(beTrue())
expect(oneToOne3.last_read).to(equal(nowTimestampMs))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
let openGroupBaseUrl: String = "http://Example.ORG:5678"
var cOpenGroupBaseUrl: [CChar] = openGroupBaseUrl.cArray.nullTerminated()
let openGroupBaseUrlResult: String = openGroupBaseUrl.lowercased()
// ("http://Example.ORG:5678"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (268 - openGroupBaseUrl.count))
// )
let openGroupRoom: String = "SudokuRoom"
var cOpenGroupRoom: [CChar] = openGroupRoom.cArray.nullTerminated()
let openGroupRoomResult: String = openGroupRoom.lowercased()
// ("SudokuRoom"
// .lowercased()
// .cArray +
// [CChar](repeating: 0, count: (65 - openGroupRoom.count))
// )
var cOpenGroupPubkey: [UInt8] = Data(hex: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")
.bytes
var community1: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.unread = true
// The new data doesn't get stored until we call this:
convo_info_volatile_set_community(conf, &community1);
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(1))
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
pushData1.deallocate()
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(convo_info_volatile_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true))
expect(oneToOne4.last_read).to(equal(nowTimestampMs))
expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId))
expect(oneToOne4.unread).to(beFalse())
var community2: convo_info_volatile_community = convo_info_volatile_community()
expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult))
expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult))
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community2.unread = true
let anotherId: String = "051111111111111111111111111111111111111111111111111111111111111111"
var cAnotherId: [CChar] = anotherId.cArray.nullTerminated()
var oneToOne5: convo_info_volatile_1to1 = convo_info_volatile_1to1()
expect(convo_info_volatile_get_or_construct_1to1(conf2, &oneToOne5, &cAnotherId)).to(beTrue())
oneToOne5.unread = true
convo_info_volatile_set_1to1(conf2, &oneToOne5)
let thirdId: String = "05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
var cThirdId: [CChar] = thirdId.cArray.nullTerminated()
var legacyGroup2: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
expect(convo_info_volatile_get_or_construct_legacy_group(conf2, &legacyGroup2, &cThirdId)).to(beTrue())
legacyGroup2.last_read = (nowTimestampMs - 50)
convo_info_volatile_set_legacy_group(conf2, &legacyGroup2)
expect(config_needs_push(conf2)).to(beTrue())
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData2.pointee.seqno).to(equal(2))
// Check the merging
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
var mergeSize: [Int] = [pushData2.pointee.config_len]
expect(config_merge(conf, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash2)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
expect(convo_info_volatile_size(conf)).to(equal(4))
expect(convo_info_volatile_size_1to1(conf)).to(equal(2))
expect(convo_info_volatile_size_communities(conf)).to(equal(1))
expect(convo_info_volatile_size_legacy_groups(conf)).to(equal(1))
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
var c2: convo_info_volatile_community = convo_info_volatile_community()
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it: OpaquePointer = convo_info_volatile_iterator_new(targetConf)
while !convo_info_volatile_iterator_done(it) {
if convo_info_volatile_it_is_1to1(it, &c1) {
seen.append("1-to-1: \(String(libSessionVal: c1.session_id))")
}
else if convo_info_volatile_it_is_community(it, &c2) {
seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else if convo_info_volatile_it_is_legacy_group(it, &c3) {
seen.append("cl: \(String(libSessionVal: c3.group_id))")
}
convo_info_volatile_iterator_advance(it)
}
convo_info_volatile_iterator_free(it)
expect(seen).to(equal([
"1-to-1: 051111111111111111111111111111111111111111111111111111111111111111",
"1-to-1: 055000000000000000000000000000000000000000000000000000000000000000",
"og: http://example.org:5678/r/sudokuroom",
"cl: 05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
let fourthId: String = "052000000000000000000000000000000000000000000000000000000000000000"
var cFourthId: [CChar] = fourthId.cArray.nullTerminated()
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cFourthId)
expect(config_needs_push(conf)).to(beFalse())
convo_info_volatile_erase_1to1(conf, &cDefinitelyRealId)
expect(config_needs_push(conf)).to(beTrue())
expect(convo_info_volatile_size(conf)).to(equal(3))
expect(convo_info_volatile_size_1to1(conf)).to(equal(1))
// Check the single-type iterators:
var seen1: [String?] = []
var c1: convo_info_volatile_1to1 = convo_info_volatile_1to1()
let it1: OpaquePointer = convo_info_volatile_iterator_new_1to1(conf)
while !convo_info_volatile_iterator_done(it1) {
expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue())
seen1.append(String(libSessionVal: c1.session_id))
convo_info_volatile_iterator_advance(it1)
}
convo_info_volatile_iterator_free(it1)
expect(seen1).to(equal([
"051111111111111111111111111111111111111111111111111111111111111111"
]))
var seen2: [String?] = []
var c2: convo_info_volatile_community = convo_info_volatile_community()
let it2: OpaquePointer = convo_info_volatile_iterator_new_communities(conf)
while !convo_info_volatile_iterator_done(it2) {
expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue())
seen2.append(String(libSessionVal: c2.base_url))
convo_info_volatile_iterator_advance(it2)
}
convo_info_volatile_iterator_free(it2)
expect(seen2).to(equal([
"http://example.org:5678"
]))
var seen3: [String?] = []
var c3: convo_info_volatile_legacy_group = convo_info_volatile_legacy_group()
let it3: OpaquePointer = convo_info_volatile_iterator_new_legacy_groups(conf)
while !convo_info_volatile_iterator_done(it3) {
expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue())
seen3.append(String(libSessionVal: c3.group_id))
convo_info_volatile_iterator_advance(it3)
}
convo_info_volatile_iterator_free(it3)
expect(seen3).to(equal([
"05cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
]))
}
}
}
}

View File

@ -0,0 +1,108 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
extension LibSessionSpec {
class ConfigGroupInfo {
static func tests() {
context("GROUP_INFO") {
// MARK: - generates config correctly
it("generates config correctly") {
let seed: Data = Data(
hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"
)
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let keyPair: KeyPair = Crypto().generate(.ed25519KeyPair(seed: seed))!
var edPK: [UInt8] = keyPair.publicKey
var edSK: [UInt8] = keyPair.secretKey
expect(edPK.toHexString())
.to(equal("cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"))
expect(String(Data(edSK.prefix(32)).toHexString())).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(groups_info_init(&conf, &edPK, &edSK, nil, 0, &error)).to(equal(0))
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(groups_info_init(&conf2, &edPK, &edSK, nil, 0, &error)).to(equal(0))
expect(groups_info_set_name(conf, "GROUP Name")).to(equal(0))
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(1))
expect(pushData1.pointee.config_len).to(equal(256))
expect(pushData1.pointee.obsolete_len).to(equal(0))
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData1.pointee.seqno, &cFakeHash1)
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue())
var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData1.pointee.config)]
var mergeSize1: [Int] = [pushData1.pointee.config_len]
expect(config_merge(conf2, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
expect(config_needs_push(conf2)).to(beFalse())
mergeHashes1.forEach { $0?.deallocate() }
mergeData1.forEach { $0?.deallocate() }
let namePtr: UnsafePointer<CChar>? = groups_info_get_name(conf)
expect(namePtr).toNot(beNil())
expect(String(cString: namePtr!)).to(equal("GROUP Name"))
let createTime: Int64 = 1682529839
let pic: user_profile_pic = user_profile_pic(
url: "http://example.com/12345".toLibSession(),
key: Data(hex: "abcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcdabcd")
.toLibSession()
)
expect(groups_info_set_pic(conf, pic)).to(equal(0))
groups_info_set_expiry_timer(conf, 60 * 60)
groups_info_set_created(conf, createTime)
groups_info_set_delete_before(conf, createTime + (50 * 86400))
groups_info_set_attach_delete_before(conf, createTime + (70 * 86400))
groups_info_destroy_group(conf)
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf2)
let obsoleteHashes: [String] = [String](
pointer: pushData2.pointee.obsolete,
count: pushData2.pointee.obsolete_len,
defaultValue: []
)
expect(pushData2.pointee.seqno).to(equal(2))
expect(pushData2.pointee.config_len).to(equal(512))
expect(obsoleteHashes).to(equal(["fakehash2"]))
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
config_confirm_pushed(conf2, pushData2.pointee.seqno, &cFakeHash2)
expect(groups_info_set_name(conf, "Better name!")).to(equal(0))
// This fails because ginfo1 doesn't yet have the new key that ginfo2 used (bbb...)
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData2.pointee.config)]
var mergeSize2: [Int] = [pushData2.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(0))
mergeHashes2.forEach { $0?.deallocate() }
mergeData2.forEach { $0?.deallocate() }
}
}
}
}
}

View File

@ -0,0 +1,50 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
extension LibSessionSpec {
class ConfigGroupKeys {
static func tests() {
context("GROUP_KEYS") {
// MARK: - generates config correctly
it("generates config correctly") {
let userSeed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
let seed: Data = Data(
hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"
)
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: userSeed)
let keyPair: KeyPair = Crypto().generate(.ed25519KeyPair(seed: seed))!
var userEdSK: [UInt8] = identity.ed25519KeyPair.secretKey
var edPK: [UInt8] = keyPair.publicKey
var edSK: [UInt8] = keyPair.secretKey
expect(userEdSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(edPK.toHexString())
.to(equal("cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"))
expect(String(Data(edSK.prefix(32)).toHexString())).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var infoConf: UnsafeMutablePointer<config_object>? = nil
expect(groups_info_init(&infoConf, &edPK, &edSK, nil, 0, &error)).to(equal(0))
var membersConf: UnsafeMutablePointer<config_object>? = nil
expect(groups_members_init(&membersConf, &edPK, &edSK, nil, 0, &error)).to(equal(0))
}
}
}
}
}

View File

@ -0,0 +1,43 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import Sodium
import SessionUtil
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
extension LibSessionSpec {
class ConfigGroupMembers {
static func tests() {
context("GROUP_MEMBERS") {
// MARK: - generates config correctly
it("generates config correctly") {
let seed: Data = Data(
hex: "0123456789abcdef0123456789abcdeffedcba9876543210fedcba9876543210"
)
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let keyPair: KeyPair = Crypto().generate(.ed25519KeyPair(seed: seed))!
var edPK: [UInt8] = keyPair.publicKey
var edSK: [UInt8] = keyPair.secretKey
expect(edPK.toHexString())
.to(equal("cbd569f56fb13ea95a3f0c05c331cc24139c0090feb412069dc49fab34406ece"))
expect(String(Data(edSK.prefix(32)).toHexString())).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(groups_members_init(&conf, &edPK, &edSK, nil, 0, &error)).to(equal(0))
}
}
}
}
}

View File

@ -0,0 +1,587 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import SessionMessagingKit
import Quick
import Nimble
extension LibSessionSpec {
class ConfigUserGroups {
static func tests() {
it("parses community URLs correctly") {
let result1 = SessionUtil.parseCommunity(url: [
"https://example.com/",
"SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
].joined())
let result2 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.COM/",
"sOMErOOM?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result3 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.COM/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result4 = SessionUtil.parseCommunity(url: [
"http://example.com/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result5 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.com:443/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result6 = SessionUtil.parseCommunity(url: [
"HTTP://EXAMPLE.com:80/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result7 = SessionUtil.parseCommunity(url: [
"http://example.com:80/r/",
"someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8"
].joined())
let result8 = SessionUtil.parseCommunity(url: [
"http://example.com:80/r/",
"someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo"
].joined())
expect(result1?.server).to(equal("https://example.com"))
expect(result1?.server).to(equal(result2?.server))
expect(result1?.server).to(equal(result3?.server))
expect(result1?.server).toNot(equal(result4?.server))
expect(result4?.server).to(equal("http://example.com"))
expect(result1?.server).to(equal(result5?.server))
expect(result4?.server).to(equal(result6?.server))
expect(result4?.server).to(equal(result7?.server))
expect(result4?.server).to(equal(result8?.server))
expect(result1?.room).to(equal("SomeRoom"))
expect(result2?.room).to(equal("sOMErOOM"))
expect(result3?.room).to(equal("someroom"))
expect(result4?.room).to(equal("someroom"))
expect(result5?.room).to(equal("someroom"))
expect(result6?.room).to(equal("someroom"))
expect(result7?.room).to(equal("someroom"))
expect(result8?.room).to(equal("someroom"))
expect(result1?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result2?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result3?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result4?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result5?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result6?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result7?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result8?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
}
context("USER_GROUPS") {
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf, &edSK, nil, 0, &error)).to(equal(0))
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let legacyGroup1: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup1?.pointee).to(beNil())
expect(user_groups_size(conf)).to(equal(0))
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup2.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup2.pointee.session_id))
.to(equal(definitelyRealId))
expect(legacyGroup2.pointee.disappearing_timer).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup2.pointee.priority).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal(""))
expect(legacyGroup2.pointee.joined_at).to(equal(0))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(legacyGroup2.pointee.mute_until).to(equal(0))
// Iterate through and make sure we got everything we expected
var membersSeen1: [String: Bool] = [:]
var memberSessionId1: UnsafePointer<CChar>? = nil
var memberAdmin1: Bool = false
let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) {
membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1
}
ugroups_legacy_members_free(membersIt1)
expect(membersSeen1).to(beEmpty())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len))
.to(beEmpty())
expect(pushData1.pointee.config_len).to(equal(256))
pushData1.deallocate()
let users: [String] = [
"050000000000000000000000000000000000000000000000000000000000000000",
"051111111111111111111111111111111111111111111111111111111111111111",
"052222222222222222222222222222222222222222222222222222222222222222",
"053333333333333333333333333333333333333333333333333333333333333333",
"054444444444444444444444444444444444444444444444444444444444444444",
"055555555555555555555555555555555555555555555555555555555555555555",
"056666666666666666666666666666666666666666666666666666666666666666"
]
var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() }
legacyGroup2.pointee.name = "Englishmen".toLibSession()
legacyGroup2.pointee.disappearing_timer = 60
legacyGroup2.pointee.joined_at = createdTs
legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL
legacyGroup2.pointee.mute_until = (nowTs + 3600)
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse())
// Flip to and from admin
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue())
var membersSeen2: [String: Bool] = [:]
var memberSessionId2: UnsafePointer<CChar>? = nil
var memberAdmin2: Bool = false
let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) {
membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2
}
ugroups_legacy_members_free(membersIt2)
expect(membersSeen2).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)!
let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)!
// Note: this isn't exactly what Session actually does here for legacy closed
// groups (rather it uses X25519 keys) but for this test the distinction doesn't matter.
legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession()
legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession()
legacyGroup2.pointee.priority = 3
expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString())
.to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"))
expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString())
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
// The new data doesn't get stored until we call this:
user_groups_set_free_legacy_group(conf, legacyGroup2)
let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup3?.pointee).toNot(beNil())
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
ugroups_legacy_group_free(legacyGroup3)
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated()
var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community1: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey))
.to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case
expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.priority = 14
// The new data doesn't get stored until we call this:
user_groups_set_community(conf, &community1)
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len))
.to(beEmpty())
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2
expect(config_needs_push(conf)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(1))
expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len))
.to(beEmpty())
pushData3.deallocate()
let currentHashes1: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf)
expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len))
.to(beEmpty())
pushData4.deallocate()
let currentHashes2: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes2?.deallocate()
expect(user_groups_size(conf2)).to(equal(2))
expect(user_groups_size_communities(conf2)).to(equal(1))
expect(user_groups_size_legacy_groups(conf2)).to(equal(1))
let legacyGroup4: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(legacyGroup4?.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60))
expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId))
expect(legacyGroup4?.pointee.priority).to(equal(3))
expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen"))
expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600))
var membersSeen3: [String: Bool] = [:]
var memberSessionId3: UnsafePointer<CChar>? = nil
var memberAdmin3: Bool = false
let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4)
while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) {
membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3
}
ugroups_legacy_members_free(membersIt3)
ugroups_legacy_group_free(legacyGroup4)
expect(membersSeen3).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData5.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://example.org:5678/r/SudokuRoom",
"legacy: Englishmen, 1 admins, 2 members"
]))
}
var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated()
var community2: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room))
.to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678"))
expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(community2.priority).to(equal(14))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData6.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData6.deallocate()
community2.room = "sudokuRoom".toLibSession() // Change capitalization
user_groups_set_community(conf2, &community2)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2)
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash1]))
let currentHashes3: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
.to(equal([fakeHash2]))
currentHashes3?.deallocate()
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf2, &dump2, &dump2Len)
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData8.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2)
expect(config_needs_dump(conf2)).to(beFalse())
var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData8.pointee.config)]
var mergeSize1: [Int] = [pushData8.pointee.config_len]
expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
pushData8.deallocate()
var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community3: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room))
.to(beTrue())
expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change
expect(user_groups_size(conf)).to(equal(2))
expect(user_groups_size_communities(conf)).to(equal(1))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let legacyGroup5: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(2))
expect(config_needs_dump(conf2)).to(beFalse())
pushData9.deallocate()
user_groups_set_free_legacy_group(conf2, legacyGroup5)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated()
var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated()
user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData10: UnsafeMutablePointer<config_push_data> = config_push(conf2)
config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3)
expect(pushData10.pointee.seqno).to(equal(3))
expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len))
.to(equal([fakeHash2]))
let currentHashes4: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
.to(equal([fakeHash3]))
currentHashes4?.deallocate()
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData10.pointee.config)]
var mergeSize2: [Int] = [pushData10.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(user_groups_size(conf)).to(equal(1))
expect(user_groups_size_communities(conf)).to(equal(0))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
var prio: Int32 = 0
var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated()
var cBeanstalkPubkey: [UInt8] = Data(
hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
).cArray
["fee", "fi", "fo", "fum"].forEach { room in
var cRoom: [CChar] = room.cArray.nullTerminated()
prio += 1
var community4: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey))
.to(beTrue())
community4.priority = prio
user_groups_set_community(conf, &community4)
}
expect(user_groups_size(conf)).to(equal(5))
expect(user_groups_size_communities(conf)).to(equal(4))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let pushData11: UnsafeMutablePointer<config_push_data> = config_push(conf)
config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4)
expect(pushData11.pointee.seqno).to(equal(4))
expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len))
.to(equal([fakeHash3, fakeHash2, fakeHash1]))
// Load some obsolete ones in just to check that they get immediately obsoleted
let fakeHash10: String = "fakehash10"
let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated()
let fakeHash11: String = "fakehash11"
let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated()
let fakeHash12: String = "fakehash12"
let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [
UnsafePointer(pushData10.pointee.config),
UnsafePointer(pushData2.pointee.config),
UnsafePointer(pushData7.pointee.config),
UnsafePointer(pushData11.pointee.config)
]
var mergeSize3: [Int] = [
pushData10.pointee.config_len,
pushData2.pointee.config_len,
pushData7.pointee.config_len,
pushData11.pointee.config_len
]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4))
expect(config_needs_dump(conf2)).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
pushData2.deallocate()
pushData7.deallocate()
pushData10.deallocate()
pushData11.deallocate()
let currentHashes5: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
.to(equal([fakeHash4]))
currentHashes5?.deallocate()
let pushData12: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData12.pointee.seqno).to(equal(4))
expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len))
.to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3]))
pushData12.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://jacksbeanstalk.org/r/fee",
"community: http://jacksbeanstalk.org/r/fi",
"community: http://jacksbeanstalk.org/r/fo",
"community: http://jacksbeanstalk.org/r/fum",
"legacy: Englishmen, 3 admins, 2 members"
]))
}
}
}
}
}
}

View File

@ -1,589 +0,0 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import SessionMessagingKit
import Quick
import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigUserGroupsSpec {
// MARK: - Spec
static func spec() {
it("parses community URLs correctly") {
let result1 = SessionUtil.parseCommunity(url: [
"https://example.com/",
"SomeRoom?public_key=0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
].joined())
let result2 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.COM/",
"sOMErOOM?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result3 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.COM/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result4 = SessionUtil.parseCommunity(url: [
"http://example.com/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result5 = SessionUtil.parseCommunity(url: [
"HTTPS://EXAMPLE.com:443/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result6 = SessionUtil.parseCommunity(url: [
"HTTP://EXAMPLE.com:80/r/",
"someroom?public_key=0123456789aBcdEF0123456789abCDEF0123456789ABCdef0123456789ABCDEF"
].joined())
let result7 = SessionUtil.parseCommunity(url: [
"http://example.com:80/r/",
"someroom?public_key=ASNFZ4mrze8BI0VniavN7wEjRWeJq83vASNFZ4mrze8"
].joined())
let result8 = SessionUtil.parseCommunity(url: [
"http://example.com:80/r/",
"someroom?public_key=yrtwk3hjixg66yjdeiuauk6p7hy1gtm8tgih55abrpnsxnpm3zzo"
].joined())
expect(result1?.server).to(equal("https://example.com"))
expect(result1?.server).to(equal(result2?.server))
expect(result1?.server).to(equal(result3?.server))
expect(result1?.server).toNot(equal(result4?.server))
expect(result4?.server).to(equal("http://example.com"))
expect(result1?.server).to(equal(result5?.server))
expect(result4?.server).to(equal(result6?.server))
expect(result4?.server).to(equal(result7?.server))
expect(result4?.server).to(equal(result8?.server))
expect(result1?.room).to(equal("SomeRoom"))
expect(result2?.room).to(equal("sOMErOOM"))
expect(result3?.room).to(equal("someroom"))
expect(result4?.room).to(equal("someroom"))
expect(result5?.room).to(equal("someroom"))
expect(result6?.room).to(equal("someroom"))
expect(result7?.room).to(equal("someroom"))
expect(result8?.room).to(equal("someroom"))
expect(result1?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result2?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result3?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result4?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result5?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result6?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result7?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(result8?.publicKey)
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
}
context("USER_GROUPS") {
it("generates config correctly") {
let createdTs: Int64 = 1680064059
let nowTs: Int64 = Int64(Date().timeIntervalSince1970)
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// Empty contacts shouldn't have an existing contact
let definitelyRealId: String = "055000000000000000000000000000000000000000000000000000000000000000"
var cDefinitelyRealId: [CChar] = definitelyRealId.cArray.nullTerminated()
let legacyGroup1: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup1?.pointee).to(beNil())
expect(user_groups_size(conf)).to(equal(0))
let legacyGroup2: UnsafeMutablePointer<ugroups_legacy_group_info> = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup2.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup2.pointee.session_id))
.to(equal(definitelyRealId))
expect(legacyGroup2.pointee.disappearing_timer).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup2.pointee.priority).to(equal(0))
expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal(""))
expect(legacyGroup2.pointee.joined_at).to(equal(0))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT))
expect(legacyGroup2.pointee.mute_until).to(equal(0))
// Iterate through and make sure we got everything we expected
var membersSeen1: [String: Bool] = [:]
var memberSessionId1: UnsafePointer<CChar>? = nil
var memberAdmin1: Bool = false
let membersIt1: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt1, &memberSessionId1, &memberAdmin1) {
membersSeen1[String(cString: memberSessionId1!)] = memberAdmin1
}
ugroups_legacy_members_free(membersIt1)
expect(membersSeen1).to(beEmpty())
// No need to sync a conversation with a default state
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beFalse())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee.seqno).to(equal(0))
expect([String](pointer: pushData1.pointee.obsolete, count: pushData1.pointee.obsolete_len))
.to(beEmpty())
expect(pushData1.pointee.config_len).to(equal(256))
pushData1.deallocate()
let users: [String] = [
"050000000000000000000000000000000000000000000000000000000000000000",
"051111111111111111111111111111111111111111111111111111111111111111",
"052222222222222222222222222222222222222222222222222222222222222222",
"053333333333333333333333333333333333333333333333333333333333333333",
"054444444444444444444444444444444444444444444444444444444444444444",
"055555555555555555555555555555555555555555555555555555555555555555",
"056666666666666666666666666666666666666666666666666666666666666666"
]
var cUsers: [[CChar]] = users.map { $0.cArray.nullTerminated() }
legacyGroup2.pointee.name = "Englishmen".toLibSession()
legacyGroup2.pointee.disappearing_timer = 60
legacyGroup2.pointee.joined_at = createdTs
legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL
legacyGroup2.pointee.mute_until = (nowTs + 3600)
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[0], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[4], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[5], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], false)).to(beFalse())
// Flip to and from admin
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[2], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup2, &cUsers[1], false)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[5])).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup2, &cUsers[4])).to(beTrue())
var membersSeen2: [String: Bool] = [:]
var memberSessionId2: UnsafePointer<CChar>? = nil
var memberAdmin2: Bool = false
let membersIt2: OpaquePointer = ugroups_legacy_members_begin(legacyGroup2)
while ugroups_legacy_members_next(membersIt2, &memberSessionId2, &memberAdmin2) {
membersSeen2[String(cString: memberSessionId2!)] = memberAdmin2
}
ugroups_legacy_members_free(membersIt2)
expect(membersSeen2).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let groupSeed: Data = Data(hex: "00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")
let groupEd25519KeyPair = Sodium().sign.keyPair(seed: groupSeed.bytes)!
let groupX25519PublicKey = Sodium().sign.toX25519(ed25519PublicKey: groupEd25519KeyPair.publicKey)!
// Note: this isn't exactly what Session actually does here for legacy closed
// groups (rather it uses X25519 keys) but for this test the distinction doesn't matter.
legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession()
legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession()
legacyGroup2.pointee.priority = 3
expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString())
.to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e"))
expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString())
.to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"))
// The new data doesn't get stored until we call this:
user_groups_set_free_legacy_group(conf, legacyGroup2)
let legacyGroup3: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf, &cDefinitelyRealId)
expect(legacyGroup3?.pointee).toNot(beNil())
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
ugroups_legacy_group_free(legacyGroup3)
let communityPubkey: String = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
var cCommunityPubkey: [UInt8] = Data(hex: communityPubkey).cArray
var cCommunityBaseUrl: [CChar] = "http://Example.ORG:5678".cArray.nullTerminated()
var cCommunityRoom: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community1: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey))
.to(beTrue())
expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case
expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving
expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
community1.priority = 14
// The new data doesn't get stored until we call this:
user_groups_set_community(conf, &community1)
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
expect([String](pointer: pushData2.pointee.obsolete, count: pushData2.pointee.obsolete_len))
.to(beEmpty())
// Pretend we uploaded it
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
expect(config_needs_dump(conf)).to(beTrue())
expect(config_needs_push(conf)).to(beFalse())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_groups_init(&conf2, &edSK, dump1, dump1Len, error2)).to(equal(0))
error2?.deallocate()
dump1?.deallocate()
expect(config_needs_dump(conf)).to(beFalse()) // Because we just called dump() above, to load up conf2
expect(config_needs_push(conf)).to(beFalse())
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(1))
expect([String](pointer: pushData3.pointee.obsolete, count: pushData3.pointee.obsolete_len))
.to(beEmpty())
pushData3.deallocate()
let currentHashes1: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf)
expect([String](pointer: currentHashes1?.pointee.value, count: currentHashes1?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes1?.deallocate()
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
expect([String](pointer: pushData4.pointee.obsolete, count: pushData4.pointee.obsolete_len))
.to(beEmpty())
pushData4.deallocate()
let currentHashes2: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes2?.pointee.value, count: currentHashes2?.pointee.len))
.to(equal(["fakehash1"]))
currentHashes2?.deallocate()
expect(user_groups_size(conf2)).to(equal(2))
expect(user_groups_size_communities(conf2)).to(equal(1))
expect(user_groups_size_legacy_groups(conf2)).to(equal(1))
let legacyGroup4: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(legacyGroup4?.pointee).toNot(beNil())
expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal(""))
expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal(""))
expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60))
expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId))
expect(legacyGroup4?.pointee.priority).to(equal(3))
expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen"))
expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs))
expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL))
expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600))
var membersSeen3: [String: Bool] = [:]
var memberSessionId3: UnsafePointer<CChar>? = nil
var memberAdmin3: Bool = false
let membersIt3: OpaquePointer = ugroups_legacy_members_begin(legacyGroup4)
while ugroups_legacy_members_next(membersIt3, &memberSessionId3, &memberAdmin3) {
membersSeen3[String(cString: memberSessionId3!)] = memberAdmin3
}
ugroups_legacy_members_free(membersIt3)
ugroups_legacy_group_free(legacyGroup4)
expect(membersSeen3).to(equal([
"050000000000000000000000000000000000000000000000000000000000000000": false,
"051111111111111111111111111111111111111111111111111111111111111111": false,
"052222222222222222222222222222222222222222222222222222222222222222": true
]))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData5.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://example.org:5678/r/SudokuRoom",
"legacy: Englishmen, 1 admins, 2 members"
]))
}
var cCommunity2BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity2Room: [CChar] = "sudokuRoom".cArray.nullTerminated()
var community2: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room))
.to(beTrue())
expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678"))
expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value
expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString())
.to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"))
expect(community2.priority).to(equal(14))
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData6.pointee.seqno).to(equal(1))
expect(config_needs_dump(conf2)).to(beFalse())
pushData6.deallocate()
community2.room = "sudokuRoom".toLibSession() // Change capitalization
user_groups_set_community(conf2, &community2)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData7: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData7.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData7.pointee.seqno, &cFakeHash2)
expect([String](pointer: pushData7.pointee.obsolete, count: pushData7.pointee.obsolete_len))
.to(equal([fakeHash1]))
let currentHashes3: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes3?.pointee.value, count: currentHashes3?.pointee.len))
.to(equal([fakeHash2]))
currentHashes3?.deallocate()
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf2, &dump2, &dump2Len)
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData8: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData8.pointee.seqno).to(equal(2))
config_confirm_pushed(conf2, pushData8.pointee.seqno, &cFakeHash2)
expect(config_needs_dump(conf2)).to(beFalse())
var mergeHashes1: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData1: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData8.pointee.config)]
var mergeSize1: [Int] = [pushData8.pointee.config_len]
expect(config_merge(conf, &mergeHashes1, &mergeData1, &mergeSize1, 1)).to(equal(1))
pushData8.deallocate()
var cCommunity3BaseUrl: [CChar] = "http://example.org:5678".cArray.nullTerminated()
var cCommunity3Room: [CChar] = "SudokuRoom".cArray.nullTerminated()
var community3: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room))
.to(beTrue())
expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change
expect(user_groups_size(conf)).to(equal(2))
expect(user_groups_size_communities(conf)).to(equal(1))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let legacyGroup5: UnsafeMutablePointer<ugroups_legacy_group_info>? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId)
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[4], false)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[5], true)).to(beTrue())
expect(ugroups_legacy_member_add(legacyGroup5, &cUsers[6], true)).to(beTrue())
expect(ugroups_legacy_member_remove(legacyGroup5, &cUsers[1])).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
let pushData9: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData9.pointee.seqno).to(equal(2))
expect(config_needs_dump(conf2)).to(beFalse())
pushData9.deallocate()
user_groups_set_free_legacy_group(conf2, legacyGroup5)
expect(config_needs_push(conf2)).to(beTrue())
expect(config_needs_dump(conf2)).to(beTrue())
var cCommunity4BaseUrl: [CChar] = "http://exAMple.ORG:5678".cArray.nullTerminated()
var cCommunity4Room: [CChar] = "sudokuROOM".cArray.nullTerminated()
user_groups_erase_community(conf2, &cCommunity4BaseUrl, &cCommunity4Room)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData10: UnsafeMutablePointer<config_push_data> = config_push(conf2)
config_confirm_pushed(conf2, pushData10.pointee.seqno, &cFakeHash3)
expect(pushData10.pointee.seqno).to(equal(3))
expect([String](pointer: pushData10.pointee.obsolete, count: pushData10.pointee.obsolete_len))
.to(equal([fakeHash2]))
let currentHashes4: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes4?.pointee.value, count: currentHashes4?.pointee.len))
.to(equal([fakeHash3]))
currentHashes4?.deallocate()
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData10.pointee.config)]
var mergeSize2: [Int] = [pushData10.pointee.config_len]
expect(config_merge(conf, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
expect(user_groups_size(conf)).to(equal(1))
expect(user_groups_size_communities(conf)).to(equal(0))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
var prio: Int32 = 0
var cBeanstalkBaseUrl: [CChar] = "http://jacksbeanstalk.org".cArray.nullTerminated()
var cBeanstalkPubkey: [UInt8] = Data(
hex: "0000111122223333444455556666777788889999aaaabbbbccccddddeeeeffff"
).cArray
["fee", "fi", "fo", "fum"].forEach { room in
var cRoom: [CChar] = room.cArray.nullTerminated()
prio += 1
var community4: ugroups_community_info = ugroups_community_info()
expect(user_groups_get_or_construct_community(conf, &community4, &cBeanstalkBaseUrl, &cRoom, &cBeanstalkPubkey))
.to(beTrue())
community4.priority = prio
user_groups_set_community(conf, &community4)
}
expect(user_groups_size(conf)).to(equal(5))
expect(user_groups_size_communities(conf)).to(equal(4))
expect(user_groups_size_legacy_groups(conf)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let pushData11: UnsafeMutablePointer<config_push_data> = config_push(conf)
config_confirm_pushed(conf, pushData11.pointee.seqno, &cFakeHash4)
expect(pushData11.pointee.seqno).to(equal(4))
expect([String](pointer: pushData11.pointee.obsolete, count: pushData11.pointee.obsolete_len))
.to(equal([fakeHash3, fakeHash2, fakeHash1]))
// Load some obsolete ones in just to check that they get immediately obsoleted
let fakeHash10: String = "fakehash10"
let cFakeHash10: [CChar] = fakeHash10.cArray.nullTerminated()
let fakeHash11: String = "fakehash11"
let cFakeHash11: [CChar] = fakeHash11.cArray.nullTerminated()
let fakeHash12: String = "fakehash12"
let cFakeHash12: [CChar] = fakeHash12.cArray.nullTerminated()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash10, cFakeHash11, cFakeHash12, cFakeHash4].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [
UnsafePointer(pushData10.pointee.config),
UnsafePointer(pushData2.pointee.config),
UnsafePointer(pushData7.pointee.config),
UnsafePointer(pushData11.pointee.config)
]
var mergeSize3: [Int] = [
pushData10.pointee.config_len,
pushData2.pointee.config_len,
pushData7.pointee.config_len,
pushData11.pointee.config_len
]
expect(config_merge(conf2, &mergeHashes3, &mergeData3, &mergeSize3, 4)).to(equal(4))
expect(config_needs_dump(conf2)).to(beTrue())
expect(config_needs_push(conf2)).to(beFalse())
pushData2.deallocate()
pushData7.deallocate()
pushData10.deallocate()
pushData11.deallocate()
let currentHashes5: UnsafeMutablePointer<config_string_list>? = config_current_hashes(conf2)
expect([String](pointer: currentHashes5?.pointee.value, count: currentHashes5?.pointee.len))
.to(equal([fakeHash4]))
currentHashes5?.deallocate()
let pushData12: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData12.pointee.seqno).to(equal(4))
expect([String](pointer: pushData12.pointee.obsolete, count: pushData12.pointee.obsolete_len))
.to(equal([fakeHash11, fakeHash12, fakeHash10, fakeHash3]))
pushData12.deallocate()
for targetConf in [conf, conf2] {
// Iterate through and make sure we got everything we expected
var seen: [String] = []
var c1: ugroups_legacy_group_info = ugroups_legacy_group_info()
var c2: ugroups_community_info = ugroups_community_info()
let it: OpaquePointer = user_groups_iterator_new(targetConf)
while !user_groups_iterator_done(it) {
if user_groups_it_is_legacy_group(it, &c1) {
var memberCount: Int = 0
var adminCount: Int = 0
ugroups_legacy_members_count(&c1, &memberCount, &adminCount)
seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members")
}
else if user_groups_it_is_community(it, &c2) {
seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))")
}
else {
seen.append("unknown")
}
user_groups_iterator_advance(it)
}
user_groups_iterator_free(it)
expect(seen).to(equal([
"community: http://jacksbeanstalk.org/r/fee",
"community: http://jacksbeanstalk.org/r/fi",
"community: http://jacksbeanstalk.org/r/fo",
"community: http://jacksbeanstalk.org/r/fum",
"legacy: Englishmen, 3 admins, 2 members"
]))
}
}
}
}
}

View File

@ -0,0 +1,412 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import SessionMessagingKit
import Quick
import Nimble
extension LibSessionSpec {
class ConfigUserProfile {
static func tests() {
context("USER_PROFILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
var error: [CChar] = [CChar](repeating: 0, count: 256)
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf, &edSK, nil, 0, &error)).to(equal(0))
// We don't need to push anything, since this is an empty config
expect(config_needs_push(conf)).to(beFalse())
// And we haven't changed anything so don't need to dump to db
expect(config_needs_dump(conf)).to(beFalse())
// Since it's empty there shouldn't be a name.
let namePtr: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr).to(beNil())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee).toNot(beNil())
expect(pushData1.pointee.seqno).to(equal(0))
expect(pushData1.pointee.config_len).to(equal(256))
let encDomain: [CChar] = "UserProfile"
.bytes
.map { CChar(bitPattern: $0) }
expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile"))
var toPushDecSize: Int = 0
let toPushDecrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize)
let prefixPadding: String = (0..<193)
.map { _ in "\0" }
.joined()
expect(toPushDecrypted).toNot(beNil())
expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPushDecrypted, length: toPushDecSize))
.to(equal("\(prefixPadding)d1:#i0e1:&de1:<le1:=dee"))
pushData1.deallocate()
toPushDecrypted?.deallocate()
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
expect(String(libSessionVal: pic.url)).to(beEmpty())
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let p: user_profile_pic = user_profile_pic(
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
expect(user_profile_set_pic(conf, p)).to(equal(0))
user_profile_set_nts_priority(conf, 9)
// Retrieve them just to make sure they set properly:
let namePtr2: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr2).toNot(beNil())
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2: user_profile_pic = user_profile_get_pic(conf);
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
// Note: This hex value differs from the value in the library tests because
// it looks like the library has an "end of cell mark" character added at the
// end (0x07 or '0007') so we need to manually add it to work
let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1Decrypted: [UInt8] = ["""
d
1:#i1e
1:& d
1:+ i9e
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 32:secret78901234567890123456789012
e
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes,
expHash0,
"""
de e
e
1:= d
1:+ 0:
1:n 0:
1:p 0:
1:q 0:
e
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
].flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f",
"1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84",
"aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868",
"2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d",
"e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656",
"056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea",
"49bf122762d7bc1d6d9c02f6d54f8384"
].joined()).bytes
let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)!
let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)!
expect(pushData2Str).to(equal(expPush1EncryptedStr))
// Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data)
var pushData2DecSize: Int = 0
let pushData2Decrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(
pushData2.pointee.config,
pushData2.pointee.config_len,
edSK,
encDomain,
&pushData2DecSize
)
let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count))
.map { _ in "\0" }
.joined()
expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead
let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)!
let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii)
.map { "\(prefixPadding2)\($0)" }!
expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr))
pushData2Decrypted?.deallocate()
// We haven't dumped, so still need to dump:
expect(config_needs_dump(conf)).to(beTrue())
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
expect(config_needs_push(conf)).to(beTrue())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
// (in a real client we'd now store this to disk)
expect(config_needs_dump(conf)).to(beFalse())
let expDump1: [CChar] = [
"""
d
1:! i2e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(0:
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump1, length: dump1Len, encoding: .ascii))
.to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii)))
dump1?.deallocate()
// So now imagine we got back confirmation from the swarm that the push has been stored:
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
let expDump2: [CChar] = [
"""
d
1:! i0e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(9:fakehash1
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump2, length: dump2Len, encoding: .ascii))
.to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii)))
dump2?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
// Now we're going to set up a second, competing config object (in the real world this would be
// another Session client somewhere).
// Start with an empty config, as above:
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
error2?.deallocate()
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
var mergeSize: [Int] = [expPush1Encrypted.count]
expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
mergeHashes.forEach { $0?.deallocate() }
mergeData.forEach { $0?.deallocate() }
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<UInt8>? = nil
var dump3Len: Int = 0
config_dump(conf2, &dump3, &dump3Len)
// (store in db)
dump3?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
// didn't have any sort of merge conflict needed):
expect(config_needs_push(conf2)).to(beFalse())
// Now let's create a conflicting update:
// Change the name on both clients:
user_profile_set_name(conf, "Nibbler")
user_profile_set_name(conf2, "Raz")
// And, on conf2, we're also going to change the profile pic:
let p2: user_profile_pic = user_profile_pic(
url: "http://new.example.com/pic".toLibSession(),
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
)
user_profile_set_pic(conf2, p2)
user_profile_set_nts_expiry(conf2, 86400)
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 0)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0))
user_profile_set_blinded_msgreqs(conf2, -1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3)
var dump4: UnsafeMutablePointer<UInt8>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<UInt8>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
dump4?.deallocate()
dump5?.deallocate()
// Since we set different things, we're going to get back different serialized data to be
// pushed:
let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii)
let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii)
expect(pushData3Str).toNot(equal(pushData4Str))
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
// also fetches new messages and pulls down the other client's `seqno=2` value.
// Feed the new config into each other. (This array could hold multiple configs if we pulled
// down more than one).
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData3.pointee.config)]
var mergeSize2: [Int] = [pushData3.pointee.config_len]
expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
pushData3.deallocate()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize3: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
pushData4.deallocate()
// Now after the merge we *will* want to push from both client, since both will have generated a
// merge conflict update (with seqno = 3).
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(3))
expect(pushData6.pointee.seqno).to(equal(3))
// They should have resolved the conflict to the same thing:
expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler"))
expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler"))
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
// message just happens to have a higher hash -- and thus gets priority -- for this particular
// test).
// Since only one of them set a profile pic there should be no conflict there:
let pic3: user_profile_pic = user_profile_get_pic(conf)
expect(pic3.url).toNot(beNil())
expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic"))
expect(pic3.key).toNot(beNil())
expect(Data(libSessionVal: pic3.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
let pic4: user_profile_pic = user_profile_get_pic(conf2)
expect(pic4.url).toNot(beNil())
expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic"))
expect(pic4.key).toNot(beNil())
expect(Data(libSessionVal: pic4.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
expect(user_profile_get_nts_priority(conf2)).to(equal(9))
expect(user_profile_get_nts_expiry(conf)).to(equal(86400))
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let fakeHash5: String = "fakehash5"
var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated()
config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5)
pushData5.deallocate()
pushData6.deallocate()
var dump6: UnsafeMutablePointer<UInt8>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<UInt8>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)
dump6?.deallocate()
dump7?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Wouldn't do this in a normal session but doing it here to properly clean up
// after the test
conf?.deallocate()
conf2?.deallocate()
}
}
}
}
}

View File

@ -1,414 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Sodium
import SessionUtil
import SessionUtilitiesKit
import SessionMessagingKit
import Quick
import Nimble
/// This spec is designed to replicate the initial test cases for the libSession-util to ensure the behaviour matches
class ConfigUserProfileSpec {
// MARK: - Spec
static func spec() {
context("USER_PROFILE") {
it("generates config correctly") {
let seed: Data = Data(hex: "0123456789abcdef0123456789abcdef")
// FIXME: Would be good to move these into the libSession-util instead of using Sodium separately
let identity = try! Identity.generate(from: seed)
var edSK: [UInt8] = identity.ed25519KeyPair.secretKey
expect(edSK.toHexString().suffix(64))
.to(equal("4cb76fdc6d32278e3f83dbf608360ecc6b65727934b85d2fb86862ff98c46ab7"))
expect(identity.x25519KeyPair.publicKey.toHexString())
.to(equal("d2ad010eeb72d72e561d9de7bd7b6989af77dcabffa03a5111a6c859ae5c3a72"))
expect(String(edSK.toHexString().prefix(32))).to(equal(seed.toHexString()))
// Initialize a brand new, empty config because we have no dump data to deal with.
let error: UnsafeMutablePointer<CChar>? = nil
var conf: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf, &edSK, nil, 0, error)).to(equal(0))
error?.deallocate()
// We don't need to push anything, since this is an empty config
expect(config_needs_push(conf)).to(beFalse())
// And we haven't changed anything so don't need to dump to db
expect(config_needs_dump(conf)).to(beFalse())
// Since it's empty there shouldn't be a name.
let namePtr: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr).to(beNil())
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let pushData1: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData1.pointee).toNot(beNil())
expect(pushData1.pointee.seqno).to(equal(0))
expect(pushData1.pointee.config_len).to(equal(256))
let encDomain: [CChar] = "UserProfile"
.bytes
.map { CChar(bitPattern: $0) }
expect(String(cString: config_encryption_domain(conf))).to(equal("UserProfile"))
var toPushDecSize: Int = 0
let toPushDecrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(pushData1.pointee.config, pushData1.pointee.config_len, edSK, encDomain, &toPushDecSize)
let prefixPadding: String = (0..<193)
.map { _ in "\0" }
.joined()
expect(toPushDecrypted).toNot(beNil())
expect(toPushDecSize).to(equal(216)) // 256 - 40 overhead
expect(String(pointer: toPushDecrypted, length: toPushDecSize))
.to(equal("\(prefixPadding)d1:#i0e1:&de1:<le1:=dee"))
pushData1.deallocate()
toPushDecrypted?.deallocate()
// This should also be unset:
let pic: user_profile_pic = user_profile_get_pic(conf)
expect(String(libSessionVal: pic.url)).to(beEmpty())
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let p: user_profile_pic = user_profile_pic(
url: "http://example.org/omg-pic-123.bmp".toLibSession(),
key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession()
)
expect(user_profile_set_pic(conf, p)).to(equal(0))
user_profile_set_nts_priority(conf, 9)
// Retrieve them just to make sure they set properly:
let namePtr2: UnsafePointer<CChar>? = user_profile_get_name(conf)
expect(namePtr2).toNot(beNil())
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2: user_profile_pic = user_profile_get_pic(conf);
expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength))
.to(equal("secret78901234567890123456789012".data(using: .utf8)))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
// Since we've made changes, we should need to push new config to the swarm, *and* should need
// to dump the updated state:
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_dump(conf)).to(beTrue())
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
let pushData2: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData2.pointee.seqno).to(equal(1))
// Note: This hex value differs from the value in the library tests because
// it looks like the library has an "end of cell mark" character added at the
// end (0x07 or '0007') so we need to manually add it to work
let expHash0: [UInt8] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1Decrypted: [UInt8] = ["""
d
1:#i1e
1:& d
1:+ i9e
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 32:secret78901234567890123456789012
e
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes,
expHash0,
"""
de e
e
1:= d
1:+ 0:
1:n 0:
1:p 0:
1:q 0:
e
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
].flatMap { $0 }
let expPush1Encrypted: [UInt8] = Data(hex: [
"9693a69686da3055f1ecdfb239c3bf8e746951a36d888c2fb7c02e856a5c2091b24e39a7e1af828f",
"1fa09fe8bf7d274afde0a0847ba143c43ffb8722301b5ae32e2f078b9a5e19097403336e50b18c84",
"aade446cd2823b011f97d6ad2116a53feb814efecc086bc172d31f4214b4d7c630b63bbe575b0868",
"2d146da44915063a07a78556ab5eff4f67f6aa26211e8d330b53d28567a931028c393709a325425d",
"e7486ccde24416a7fd4a8ba5fa73899c65f4276dfaddd5b2100adcf0f793104fb235b31ce32ec656",
"056009a9ebf58d45d7d696b74e0c7ff0499c4d23204976f19561dc0dba6dc53a2497d28ce03498ea",
"49bf122762d7bc1d6d9c02f6d54f8384"
].joined()).bytes
let pushData2Str: String = String(pointer: pushData2.pointee.config, length: pushData2.pointee.config_len, encoding: .ascii)!
let expPush1EncryptedStr: String = String(pointer: expPush1Encrypted, length: expPush1Encrypted.count, encoding: .ascii)!
expect(pushData2Str).to(equal(expPush1EncryptedStr))
// Raw decryption doesn't unpad (i.e. the padding is part of the encrypted data)
var pushData2DecSize: Int = 0
let pushData2Decrypted: UnsafeMutablePointer<UInt8>? = config_decrypt(
pushData2.pointee.config,
pushData2.pointee.config_len,
edSK,
encDomain,
&pushData2DecSize
)
let prefixPadding2: String = (0..<(256 - 40 - expPush1Decrypted.count))
.map { _ in "\0" }
.joined()
expect(pushData2DecSize).to(equal(216)) // 256 - 40 overhead
let pushData2DecryptedStr: String = String(pointer: pushData2Decrypted, length: pushData2DecSize, encoding: .ascii)!
let expPush1DecryptedStr: String = String(pointer: expPush1Decrypted, length: expPush1Decrypted.count, encoding: .ascii)
.map { "\(prefixPadding2)\($0)" }!
expect(pushData2DecryptedStr).to(equal(expPush1DecryptedStr))
pushData2Decrypted?.deallocate()
// We haven't dumped, so still need to dump:
expect(config_needs_dump(conf)).to(beTrue())
// We did call push, but we haven't confirmed it as stored yet, so this will still return true:
expect(config_needs_push(conf)).to(beTrue())
var dump1: UnsafeMutablePointer<UInt8>? = nil
var dump1Len: Int = 0
config_dump(conf, &dump1, &dump1Len)
// (in a real client we'd now store this to disk)
expect(config_needs_dump(conf)).to(beFalse())
let expDump1: [CChar] = [
"""
d
1:! i2e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(0:
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump1, length: dump1Len, encoding: .ascii))
.to(equal(String(pointer: expDump1, length: expDump1.count, encoding: .ascii)))
dump1?.deallocate()
// So now imagine we got back confirmation from the swarm that the push has been stored:
let fakeHash1: String = "fakehash1"
var cFakeHash1: [CChar] = fakeHash1.cArray.nullTerminated()
config_confirm_pushed(conf, pushData2.pointee.seqno, &cFakeHash1)
pushData2.deallocate()
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_dump(conf)).to(beTrue()) // The confirmation changes state, so this makes us need a dump
var dump2: UnsafeMutablePointer<UInt8>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
let expDump2: [CChar] = [
"""
d
1:! i0e
1:$ \(expPush1Decrypted.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1Decrypted
.map { CChar(bitPattern: $0) },
"""
1:(9:fakehash1
1:)le
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
].flatMap { $0 }
expect(String(pointer: dump2, length: dump2Len, encoding: .ascii))
.to(equal(String(pointer: expDump2, length: expDump2.count, encoding: .ascii)))
dump2?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
// Now we're going to set up a second, competing config object (in the real world this would be
// another Session client somewhere).
// Start with an empty config, as above:
let error2: UnsafeMutablePointer<CChar>? = nil
var conf2: UnsafeMutablePointer<config_object>? = nil
expect(user_profile_init(&conf2, &edSK, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
error2?.deallocate()
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
var mergeHashes: [UnsafePointer<CChar>?] = [cFakeHash1].unsafeCopy()
var mergeData: [UnsafePointer<UInt8>?] = [expPush1Encrypted].unsafeCopy()
var mergeSize: [Int] = [expPush1Encrypted.count]
expect(config_merge(conf2, &mergeHashes, &mergeData, &mergeSize, 1)).to(equal(1))
mergeHashes.forEach { $0?.deallocate() }
mergeData.forEach { $0?.deallocate() }
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<UInt8>? = nil
var dump3Len: Int = 0
config_dump(conf2, &dump3, &dump3Len)
// (store in db)
dump3?.deallocate()
expect(config_needs_dump(conf2)).to(beFalse())
// We *don't* need to push: even though we updated, all we did is update to the merged data (and
// didn't have any sort of merge conflict needed):
expect(config_needs_push(conf2)).to(beFalse())
// Now let's create a conflicting update:
// Change the name on both clients:
user_profile_set_name(conf, "Nibbler")
user_profile_set_name(conf2, "Raz")
// And, on conf2, we're also going to change the profile pic:
let p2: user_profile_pic = user_profile_pic(
url: "http://new.example.com/pic".toLibSession(),
key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession()
)
user_profile_set_pic(conf2, p2)
user_profile_set_nts_expiry(conf2, 86400)
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 0)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(0))
user_profile_set_blinded_msgreqs(conf2, -1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(-1))
user_profile_set_blinded_msgreqs(conf2, 1)
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let fakeHash2: String = "fakehash2"
var cFakeHash2: [CChar] = fakeHash2.cArray.nullTerminated()
let pushData3: UnsafeMutablePointer<config_push_data> = config_push(conf)
expect(pushData3.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData3.pointee.seqno, &cFakeHash2)
let fakeHash3: String = "fakehash3"
var cFakeHash3: [CChar] = fakeHash3.cArray.nullTerminated()
let pushData4: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData4.pointee.seqno).to(equal(2)) // incremented, since we made a field change
config_confirm_pushed(conf, pushData4.pointee.seqno, &cFakeHash3)
var dump4: UnsafeMutablePointer<UInt8>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<UInt8>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
dump4?.deallocate()
dump5?.deallocate()
// Since we set different things, we're going to get back different serialized data to be
// pushed:
let pushData3Str: String? = String(pointer: pushData3.pointee.config, length: pushData3.pointee.config_len, encoding: .ascii)
let pushData4Str: String? = String(pointer: pushData4.pointee.config, length: pushData4.pointee.config_len, encoding: .ascii)
expect(pushData3Str).toNot(equal(pushData4Str))
// Now imagine that each client pushed its `seqno=2` config to the swarm, but then each client
// also fetches new messages and pulls down the other client's `seqno=2` value.
// Feed the new config into each other. (This array could hold multiple configs if we pulled
// down more than one).
var mergeHashes2: [UnsafePointer<CChar>?] = [cFakeHash2].unsafeCopy()
var mergeData2: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData3.pointee.config)]
var mergeSize2: [Int] = [pushData3.pointee.config_len]
expect(config_merge(conf2, &mergeHashes2, &mergeData2, &mergeSize2, 1)).to(equal(1))
pushData3.deallocate()
var mergeHashes3: [UnsafePointer<CChar>?] = [cFakeHash3].unsafeCopy()
var mergeData3: [UnsafePointer<UInt8>?] = [UnsafePointer(pushData4.pointee.config)]
var mergeSize3: [Int] = [pushData4.pointee.config_len]
expect(config_merge(conf, &mergeHashes3, &mergeData3, &mergeSize3, 1)).to(equal(1))
pushData4.deallocate()
// Now after the merge we *will* want to push from both client, since both will have generated a
// merge conflict update (with seqno = 3).
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
let pushData5: UnsafeMutablePointer<config_push_data> = config_push(conf)
let pushData6: UnsafeMutablePointer<config_push_data> = config_push(conf2)
expect(pushData5.pointee.seqno).to(equal(3))
expect(pushData6.pointee.seqno).to(equal(3))
// They should have resolved the conflict to the same thing:
expect(String(cString: user_profile_get_name(conf)!)).to(equal("Nibbler"))
expect(String(cString: user_profile_get_name(conf2)!)).to(equal("Nibbler"))
// (Note that they could have also both resolved to "Raz" here, but the hash of the serialized
// message just happens to have a higher hash -- and thus gets priority -- for this particular
// test).
// Since only one of them set a profile pic there should be no conflict there:
let pic3: user_profile_pic = user_profile_get_pic(conf)
expect(pic3.url).toNot(beNil())
expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic"))
expect(pic3.key).toNot(beNil())
expect(Data(libSessionVal: pic3.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
let pic4: user_profile_pic = user_profile_get_pic(conf2)
expect(pic4.url).toNot(beNil())
expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic"))
expect(pic4.key).toNot(beNil())
expect(Data(libSessionVal: pic4.key, count: 32).toHexString())
.to(equal("7177657274007975696f31323334353637383930313233343536373839303132"))
expect(user_profile_get_nts_priority(conf)).to(equal(9))
expect(user_profile_get_nts_priority(conf2)).to(equal(9))
expect(user_profile_get_nts_expiry(conf)).to(equal(86400))
expect(user_profile_get_nts_expiry(conf2)).to(equal(86400))
expect(user_profile_get_blinded_msgreqs(conf)).to(equal(1))
expect(user_profile_get_blinded_msgreqs(conf2)).to(equal(1))
let fakeHash4: String = "fakehash4"
var cFakeHash4: [CChar] = fakeHash4.cArray.nullTerminated()
let fakeHash5: String = "fakehash5"
var cFakeHash5: [CChar] = fakeHash5.cArray.nullTerminated()
config_confirm_pushed(conf, pushData5.pointee.seqno, &cFakeHash4)
config_confirm_pushed(conf2, pushData6.pointee.seqno, &cFakeHash5)
pushData5.deallocate()
pushData6.deallocate()
var dump6: UnsafeMutablePointer<UInt8>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<UInt8>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)
dump6?.deallocate()
dump7?.deallocate()
expect(config_needs_dump(conf)).to(beFalse())
expect(config_needs_dump(conf2)).to(beFalse())
expect(config_needs_push(conf)).to(beFalse())
expect(config_needs_push(conf2)).to(beFalse())
// Wouldn't do this in a normal session but doing it here to properly clean up
// after the test
conf?.deallocate()
conf2?.deallocate()
}
}
}
}

View File

@ -13,10 +13,13 @@ class LibSessionSpec: QuickSpec {
override func spec() {
describe("libSession") {
ConfigContactsSpec.spec()
ConfigUserProfileSpec.spec()
ConfigConvoInfoVolatileSpec.spec()
ConfigUserGroupsSpec.spec()
ConfigContacts.tests()
ConfigUserProfile.tests()
ConfigConvoInfoVolatile.tests()
ConfigUserGroups.tests()
ConfigGroupInfo.tests()
ConfigGroupMembers.tests()
ConfigGroupKeys.tests()
}
}
}

View File

@ -1,187 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionSnodeKit
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionMessagingKit
import AVFoundation
class BatchRequestInfoSpec: QuickSpec {
struct TestType: Codable, Equatable {
let stringValue: String
}
// MARK: - Spec
override func spec() {
// MARK: - BatchRequest.Child
describe("a BatchRequest.Child") {
var request: OpenGroupAPI.BatchRequest!
context("when encoding") {
it("successfully encodes a string body") {
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.PreparedSendData<NoResponse>(
request: Request<String, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: "testBody"
),
urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!),
publicKey: "",
responseType: NoResponse.self,
timeout: 0
)
]
)
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["b64"] as? String).to(equal("testBody"))
}
it("successfully encodes a byte body") {
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.PreparedSendData<NoResponse>(
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: [1, 2, 3]
),
urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!),
publicKey: "",
responseType: NoResponse.self,
timeout: 0
)
]
)
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["bytes"] as? [Int]).to(equal([1, 2, 3]))
}
it("successfully encodes a JSON body") {
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.PreparedSendData<NoResponse>(
request: Request<TestType, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [:],
body: TestType(stringValue: "testValue")
),
urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!),
publicKey: "",
responseType: NoResponse.self,
timeout: 0
)
]
)
let requestData: Data? = try? JSONEncoder().encode(request)
let requestJson: [[String: Any]]? = requestData
.map { try? JSONSerialization.jsonObject(with: $0) as? [[String: Any]] }
expect(requestJson?.first?["path"] as? String).to(equal("/batch"))
expect(requestJson?.first?["method"] as? String).to(equal("GET"))
expect(requestJson?.first?["json"] as? [String: String]).to(equal(["stringValue": "testValue"]))
}
it("strips authentication headers") {
let httpRequest: Request<NoBody, OpenGroupAPI.Endpoint> = Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [
"TestHeader": "Test",
HTTPHeader.sogsPubKey: "A",
HTTPHeader.sogsTimestamp: "B",
HTTPHeader.sogsNonce: "C",
HTTPHeader.sogsSignature: "D"
],
body: nil
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.PreparedSendData<NoResponse>(
request: httpRequest,
urlRequest: try! httpRequest.generateUrlRequest(),
publicKey: "",
responseType: NoResponse.self,
timeout: 0
)
]
)
let requestData: Data = try! JSONEncoder().encode(request)
let requestString: String? = String(data: requestData, encoding: .utf8)
expect(requestString)
.toNot(contain([
HTTPHeader.sogsPubKey,
HTTPHeader.sogsTimestamp,
HTTPHeader.sogsNonce,
HTTPHeader.sogsSignature
]))
}
}
it("does not strip non authentication headers") {
let httpRequest: Request<NoBody, OpenGroupAPI.Endpoint> = Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
endpoint: .batch,
queryParameters: [:],
headers: [
"TestHeader": "Test",
HTTPHeader.sogsPubKey: "A",
HTTPHeader.sogsTimestamp: "B",
HTTPHeader.sogsNonce: "C",
HTTPHeader.sogsSignature: "D"
],
body: nil
)
request = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.PreparedSendData<NoResponse>(
request: httpRequest,
urlRequest: try! httpRequest.generateUrlRequest(),
publicKey: "",
responseType: NoResponse.self,
timeout: 0
)
]
)
let requestData: Data = try! JSONEncoder().encode(request)
let requestString: String? = String(data: requestData, encoding: .utf8)
expect(requestString)
.to(contain("\"TestHeader\":\"Test\""))
}
}
}
}

View File

@ -122,7 +122,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a poll request") {
// MARK: -- generates the correct request
it("generates the correct request") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -135,14 +135,17 @@ class OpenGroupAPISpec: QuickSpec {
expect(preparedRequest?.request.url?.absoluteString).to(equal("testserver/batch"))
expect(preparedRequest?.request.httpMethod).to(equal("POST"))
expect(preparedRequest?.batchEndpoints.count).to(equal(3))
expect(preparedRequest?.batchEndpoints[test: 0]).to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1]).to(equal(.roomPollInfo("testRoom", 0)))
expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom")))
expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomPollInfo("testRoom", 0)))
expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomMessagesRecent("testRoom")))
}
// MARK: -- retrieves recent messages if there was no last message
it("retrieves recent messages if there was no last message") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -152,7 +155,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom")))
expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomMessagesRecent("testRoom")))
}
// MARK: -- retrieves recent messages if there was a last message and it has not performed the initial poll and the last message was too long ago
@ -162,7 +166,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: 121))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -172,7 +176,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints[test: 2]).to(equal(.roomMessagesRecent("testRoom")))
expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomMessagesRecent("testRoom")))
}
// MARK: -- retrieves recent messages if there was a last message and it has performed an initial poll but it was not too long ago
@ -182,7 +187,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: 122))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -192,7 +197,7 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints[test: 2])
expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomMessagesSince("testRoom", seqNo: 122)))
}
@ -203,7 +208,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.sequenceNumber.set(to: 123))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -213,7 +218,7 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints[test: 2])
expect(preparedRequest?.batchEndpoints[test: 2].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomMessagesSince("testRoom", seqNo: 123)))
}
@ -228,7 +233,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- does not call the inbox and outbox endpoints
it("does not call the inbox and outbox endpoints") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -238,8 +243,10 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox))
expect(preparedRequest?.batchEndpoints).toNot(contain(.outbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.toNot(contain(.inbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.toNot(contain(.outbox))
}
}
@ -257,7 +264,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- includes the inbox and outbox endpoints
it("includes the inbox and outbox endpoints") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -267,13 +274,15 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).to(contain(.inbox))
expect(preparedRequest?.batchEndpoints).to(contain(.outbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.inbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.outbox))
}
// MARK: ---- retrieves recent inbox messages if there was no last message
it("retrieves recent inbox messages if there was no last message") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -283,7 +292,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).to(contain(.inbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.inbox))
}
// MARK: ---- retrieves inbox messages since the last message if there was one
@ -293,7 +303,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.inboxLatestMessageId.set(to: 124))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -303,12 +313,13 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).to(contain(.inboxSince(id: 124)))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.inboxSince(id: 124)))
}
// MARK: ---- retrieves recent outbox messages if there was no last message
it("retrieves recent outbox messages if there was no last message") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -318,7 +329,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).to(contain(.outbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.outbox))
}
// MARK: ---- retrieves outbox messages since the last message if there was one
@ -328,7 +340,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.outboxLatestMessageId.set(to: 125))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -338,7 +350,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).to(contain(.outboxSince(id: 125)))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.to(contain(.outboxSince(id: 125)))
}
}
@ -356,7 +369,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- includes the inbox and outbox endpoints
it("does not include the inbox endpoint") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -366,12 +379,13 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.toNot(contain(.inbox))
}
// MARK: ---- does not retrieve recent inbox messages if there was no last message
it("does not retrieve recent inbox messages if there was no last message") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -381,7 +395,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inbox))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.toNot(contain(.inbox))
}
// MARK: ---- does not retrieve inbox messages since the last message if there was one
@ -391,7 +406,7 @@ class OpenGroupAPISpec: QuickSpec {
.updateAll(db, OpenGroup.Columns.inboxLatestMessageId.set(to: 124))
}
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedPoll(
db,
server: "testserver",
@ -401,7 +416,8 @@ class OpenGroupAPISpec: QuickSpec {
)
}
expect(preparedRequest?.batchEndpoints).toNot(contain(.inboxSince(id: 124)))
expect(preparedRequest?.batchEndpoints as? [OpenGroupAPI.Endpoint])
.toNot(contain(.inboxSince(id: 124)))
}
}
}
@ -410,7 +426,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a capabilities request") {
// MARK: -- generates the request correctly
it("generates the request and handles the response correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Capabilities>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Capabilities>? = mockStorage.read { db in
try OpenGroupAPI.preparedCapabilities(
db,
server: "testserver",
@ -428,7 +444,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
try OpenGroupAPI.preparedRooms(
db,
server: "testserver",
@ -445,7 +461,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a capabilitiesAndRoom request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.CapabilitiesAndRoomResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedCapabilitiesAndRoom(
db,
for: "testRoom",
@ -455,8 +471,10 @@ class OpenGroupAPISpec: QuickSpec {
}
expect(preparedRequest?.batchEndpoints.count).to(equal(2))
expect(preparedRequest?.batchEndpoints[test: 0]).to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1]).to(equal(.room("testRoom")))
expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.room("testRoom")))
expect(preparedRequest?.request.url?.absoluteString).to(equal("testserver/sequence"))
expect(preparedRequest?.request.httpMethod).to(equal("POST"))
@ -466,7 +484,7 @@ class OpenGroupAPISpec: QuickSpec {
it("processes a valid response correctly") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndRoomResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)?
@ -479,7 +497,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -495,7 +513,7 @@ class OpenGroupAPISpec: QuickSpec {
it("errors when not given a room response") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndBanResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndBanResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)?
@ -508,7 +526,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -521,7 +539,7 @@ class OpenGroupAPISpec: QuickSpec {
it("errors when not given a capabilities response") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockBanAndRoomResponse)
.thenReturn(HTTP.BatchResponse.mockBanAndRoomResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomResponse)?
@ -534,7 +552,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -549,7 +567,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a capabilitiesAndRooms request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.CapabilitiesAndRoomsResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.CapabilitiesAndRoomsResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedCapabilitiesAndRooms(
db,
on: "testserver",
@ -558,8 +576,10 @@ class OpenGroupAPISpec: QuickSpec {
}
expect(preparedRequest?.batchEndpoints.count).to(equal(2))
expect(preparedRequest?.batchEndpoints[test: 0]).to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1]).to(equal(.rooms))
expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.capabilities))
expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.rooms))
expect(preparedRequest?.request.url?.absoluteString).to(equal("testserver/sequence"))
expect(preparedRequest?.request.httpMethod).to(equal("POST"))
@ -569,7 +589,7 @@ class OpenGroupAPISpec: QuickSpec {
it("processes a valid response correctly") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomsResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndRoomsResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)?
@ -581,7 +601,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -597,7 +617,7 @@ class OpenGroupAPISpec: QuickSpec {
it("errors when not given a room response") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndBanResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndBanResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)?
@ -609,7 +629,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -622,7 +642,7 @@ class OpenGroupAPISpec: QuickSpec {
it("errors when not given a capabilities response") {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockBanAndRoomsResponse)
.thenReturn(HTTP.BatchResponse.mockBanAndRoomsResponse)
var response: (info: ResponseInfoType, data: OpenGroupAPI.CapabilitiesAndRoomsResponse)?
@ -634,7 +654,7 @@ class OpenGroupAPISpec: QuickSpec {
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.flatMap { $0.send(using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
@ -649,7 +669,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a send message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
try OpenGroupAPI.preparedSend(
db,
plaintext: "test".data(using: .utf8)!,
@ -677,7 +697,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs the message correctly
it("signs the message correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
try OpenGroupAPI.preparedSend(
db,
plaintext: "test".data(using: .utf8)!,
@ -703,7 +723,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -734,7 +754,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -765,7 +785,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -801,7 +821,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs the message correctly
it("signs the message correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
try OpenGroupAPI.preparedSend(
db,
plaintext: "test".data(using: .utf8)!,
@ -827,7 +847,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -858,7 +878,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -897,7 +917,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedSend(
db,
@ -926,7 +946,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an individual message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.Message>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.Message>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessage(
db,
id: 123,
@ -956,7 +976,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessageUpdate(
db,
id: 123,
@ -983,7 +1003,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs the message correctly
it("signs the message correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessageUpdate(
db,
id: 123,
@ -1008,7 +1028,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1038,7 +1058,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1068,7 +1088,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1103,7 +1123,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs the message correctly
it("signs the message correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessageUpdate(
db,
id: 123,
@ -1128,7 +1148,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1158,7 +1178,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1196,7 +1216,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedMessageUpdate(
db,
@ -1224,7 +1244,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a delete message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessageDelete(
db,
id: 123,
@ -1243,7 +1263,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a delete all messages request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedMessagesDeleteAll(
db,
sessionId: "testUserId",
@ -1262,7 +1282,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a pin message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedPinMessage(
db,
id: 123,
@ -1281,7 +1301,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an unpin message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUnpinMessage(
db,
id: 123,
@ -1300,7 +1320,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an unpin all request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUnpinAll(
db,
in: "testRoom",
@ -1318,7 +1338,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an upload file request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<FileUploadResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUploadFile(
db,
bytes: [],
@ -1334,7 +1354,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- doesn't add a fileName to the content-disposition header when not provided
it("doesn't add a fileName to the content-disposition header when not provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<FileUploadResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUploadFile(
db,
bytes: [],
@ -1350,7 +1370,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- adds the fileName to the content-disposition header when provided
it("adds the fileName to the content-disposition header when provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<FileUploadResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<FileUploadResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUploadFile(
db,
bytes: [],
@ -1370,7 +1390,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a download file request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<Data>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<Data>? = mockStorage.read { db in
try OpenGroupAPI.preparedDownloadFile(
db,
fileId: "1",
@ -1389,7 +1409,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an inbox request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.DirectMessage]?>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? = mockStorage.read { db in
try OpenGroupAPI.preparedInbox(
db,
on: "testserver",
@ -1406,7 +1426,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an inbox since request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.DirectMessage]?>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.DirectMessage]?>? = mockStorage.read { db in
try OpenGroupAPI.preparedInboxSince(
db,
id: 1,
@ -1424,7 +1444,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an inbox since request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.DeleteInboxResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.DeleteInboxResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedClearInbox(
db,
on: "testserver",
@ -1441,7 +1461,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a send direct message request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.SendDirectMessageResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<OpenGroupAPI.SendDirectMessageResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedSend(
db,
ciphertext: "test".data(using: .utf8)!,
@ -1460,7 +1480,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a ban user request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserBan(
db,
sessionId: "testUserId",
@ -1477,7 +1497,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does a global ban if no room tokens are provided
it("does a global ban if no room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserBan(
db,
sessionId: "testUserId",
@ -1496,7 +1516,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does room specific bans if room tokens are provided
it("does room specific bans if room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserBan(
db,
sessionId: "testUserId",
@ -1518,7 +1538,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing an unban user request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserUnban(
db,
sessionId: "testUserId",
@ -1534,7 +1554,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does a global unban if no room tokens are provided
it("does a global unban if no room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserUnban(
db,
sessionId: "testUserId",
@ -1552,7 +1572,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does room specific unbans if room tokens are provided
it("does room specific unbans if room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserUnban(
db,
sessionId: "testUserId",
@ -1573,7 +1593,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a user permissions request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserModeratorUpdate(
db,
sessionId: "testUserId",
@ -1592,7 +1612,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does a global update if no room tokens are provided
it("does a global update if no room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserModeratorUpdate(
db,
sessionId: "testUserId",
@ -1613,7 +1633,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- does room specific updates if room tokens are provided
it("does room specific updates if room tokens are provided") {
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserModeratorUpdate(
db,
sessionId: "testUserId",
@ -1635,7 +1655,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: -- fails if neither moderator or admin are set
it("fails if neither moderator or admin are set") {
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<NoResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<NoResponse>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedUserModeratorUpdate(
db,
@ -1663,7 +1683,7 @@ class OpenGroupAPISpec: QuickSpec {
context("when preparing a ban and delete all request") {
// MARK: -- generates the request correctly
it("generates the request correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<HTTP.BatchResponseMap<OpenGroupAPI.Endpoint>>? = mockStorage.read { db in
try OpenGroupAPI.preparedUserBanAndDeleteAllMessages(
db,
sessionId: "testUserId",
@ -1676,28 +1696,11 @@ class OpenGroupAPISpec: QuickSpec {
expect(preparedRequest?.request.url?.absoluteString).to(equal("testserver/sequence"))
expect(preparedRequest?.request.httpMethod).to(equal("POST"))
expect(preparedRequest?.batchEndpoints.count).to(equal(2))
expect(preparedRequest?.batchEndpoints[test: 0]).to(equal(.userBan("testUserId")))
expect(preparedRequest?.batchEndpoints[test: 1])
expect(preparedRequest?.batchEndpoints[test: 0].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.userBan("testUserId")))
expect(preparedRequest?.batchEndpoints[test: 1].asType(OpenGroupAPI.Endpoint.self))
.to(equal(.roomDeleteMessages("testRoom", sessionId: "testUserId")))
}
// // MARK: -- bans the user from the specified room rather than globally
// it("bans the user from the specified room rather than globally") {
// let preparedRequest: OpenGroupAPI.PreparedSendData<OpenGroupAPI.BatchResponse>? = mockStorage.read { db in
// try OpenGroupAPI.preparedUserBanAndDeleteAllMessages(
// db,
// sessionId: "testUserId",
// in: "testRoom",
// on: "testserver",
// using: dependencies
// )
// }
//
// let requestBody: OpenGroupAPI.UserBanRequest? = preparedRequest?.batchRequestBodies[test: 0]?
// .decoded(as: OpenGroupAPI.UserBanRequest.self)
// expect(requestBody?.global).to(beNil())
// expect(requestBody?.rooms).to(equal(["testRoom"]))
// }
}
// MARK: - when signing
@ -1709,7 +1712,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1735,7 +1738,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1760,7 +1763,7 @@ class OpenGroupAPISpec: QuickSpec {
}
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1789,7 +1792,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs correctly
it("signs correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
try OpenGroupAPI.preparedRooms(
db,
server: "testserver",
@ -1818,7 +1821,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1849,7 +1852,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: ---- signs correctly
it("signs correctly") {
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
try OpenGroupAPI.preparedRooms(
db,
server: "testserver",
@ -1886,7 +1889,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1919,7 +1922,7 @@ class OpenGroupAPISpec: QuickSpec {
.thenReturn(nil)
var preparationError: Error?
let preparedRequest: OpenGroupAPI.PreparedSendData<[OpenGroupAPI.Room]>? = mockStorage.read { db in
let preparedRequest: HTTP.PreparedRequest<[OpenGroupAPI.Room]>? = mockStorage.read { db in
do {
return try OpenGroupAPI.preparedRooms(
db,
@ -1938,56 +1941,13 @@ class OpenGroupAPISpec: QuickSpec {
}
}
}
// MARK: -- when sending
context("when sending") {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.response(type: [OpenGroupAPI.Room].self))
}
// MARK: -- triggers sending correctly
it("triggers sending correctly") {
var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])?
mockStorage
.readPublisher { db in
try OpenGroupAPI.preparedRooms(
db,
server: "testserver",
using: dependencies
)
}
.flatMap { OpenGroupAPI.send(data: $0, using: dependencies) }
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
expect(response).toNot(beNil())
expect(error).to(beNil())
}
// MARK: -- fails when not given prepared data
it("fails when not given prepared data") {
var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])?
OpenGroupAPI.send(data: nil, using: dependencies)
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
expect(error).to(matchError(OpenGroupAPIError.invalidPreparedData))
expect(response).to(beNil())
}
}
}
}
}
// MARK: - Mock Batch Responses
extension OpenGroupAPI.BatchResponse {
extension HTTP.BatchResponse {
// MARK: - Valid Responses
static let mockCapabilitiesAndRoomResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(

View File

@ -778,7 +778,7 @@ class OpenGroupManagerSpec: QuickSpec {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndRoomResponse)
mockOGMCache.when { $0.pollers }.thenReturn([:])
mockUserDefaults
@ -3055,7 +3055,7 @@ class OpenGroupManagerSpec: QuickSpec {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(OpenGroupAPI.BatchResponse.mockCapabilitiesAndRoomsResponse)
.thenReturn(HTTP.BatchResponse.mockCapabilitiesAndRoomsResponse)
mockStorage.write { db in
try OpenGroup.deleteAll(db)
@ -3673,7 +3673,7 @@ extension OpenGroupAPI.DirectMessage: Mocked {
)
}
extension OpenGroupAPI.BatchResponse {
extension HTTP.BatchResponse {
static let mockUnblindedPollResponse: AnyPublisher<(ResponseInfoType, Data?), Error> = MockNetwork.batchResponseData(
with: [
(OpenGroupAPI.Endpoint.capabilities, OpenGroupAPI.Capabilities.mockBatchSubResponse()),

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtilitiesKit
import Quick
import Nimble
@ -12,6 +13,19 @@ class SOGSEndpointSpec: QuickSpec {
override func spec() {
describe("a SOGSEndpoint") {
it("provides the correct batch request variant") {
expect(OpenGroupAPI.Endpoint.batchRequestVariant).to(equal(.sogs))
}
it("excludes the correct headers from batch sub request") {
expect(OpenGroupAPI.Endpoint.excludedSubRequestHeaders).to(equal([
HTTPHeader.sogsPubKey,
HTTPHeader.sogsTimestamp,
HTTPHeader.sogsNonce,
HTTPHeader.sogsSignature
]))
}
it("generates the path value correctly") {
// Utility

View File

@ -134,7 +134,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
)
case let closedGroupControlMessage as ClosedGroupControlMessage:
try MessageReceiver.handleClosedGroupControlMessage(
try MessageReceiver.handleLegacyClosedGroupControlMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,

View File

@ -0,0 +1,41 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
public extension HTTP.PreparedRequest {
/// Send an onion request for the prepared data
func send(using dependencies: Dependencies) -> AnyPublisher<(ResponseInfoType, R), Error> {
return dependencies.network
.send(
.onionRequest(
request,
to: server,
with: publicKey,
timeout: timeout
)
)
.decoded(with: self, using: dependencies)
.retry(retryCount, using: dependencies)
.handleEvents(
receiveOutput: self.outputEventHandler,
receiveCompletion: self.completionEventHandler,
receiveCancel: self.cancelEventHandler
)
.eraseToAnyPublisher()
}
}
public extension Optional {
func send<R>(
using dependencies: Dependencies
) -> AnyPublisher<(ResponseInfoType, R), Error> where Wrapped == HTTP.PreparedRequest<R> {
guard let instance: Wrapped = self else {
return Fail(error: HTTPError.invalidPreparedRequest)
.eraseToAnyPublisher()
}
return instance.send(using: dependencies)
}
}

View File

@ -442,8 +442,8 @@ public final class SnodeAPI {
using: dependencies
)
.decoded(as: responseTypes, using: dependencies)
.map { (batchResponse: HTTP.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)] in
let messageResponses: [HTTP.BatchSubResponse<GetMessagesResponse>] = batchResponse.responses
.map { (_: ResponseInfoType, batchResponse: HTTP.BatchResponse) -> [SnodeAPI.Namespace: (info: ResponseInfoType, data: (messages: [SnodeReceivedMessage], lastHash: String?)?)] in
let messageResponses: [HTTP.BatchSubResponse<GetMessagesResponse>] = batchResponse
.compactMap { $0 as? HTTP.BatchSubResponse<GetMessagesResponse> }
/// Since we have extended the TTL for a number of messages we need to make sure we update the local
@ -452,7 +452,6 @@ public final class SnodeAPI {
if
!refreshingConfigHashes.isEmpty,
let refreshTTLSubReponse: HTTP.BatchSubResponse<UpdateExpiryResponse> = batchResponse
.responses
.first(where: { $0 is HTTP.BatchSubResponse<UpdateExpiryResponse> })
.asType(HTTP.BatchSubResponse<UpdateExpiryResponse>.self),
let refreshTTLResponse: UpdateExpiryResponse = refreshTTLSubReponse.body,
@ -487,7 +486,7 @@ public final class SnodeAPI {
let namespace: SnodeAPI.Namespace = next.0
result[namespace] = (
info: next.1.responseInfo,
info: next.1,
data: (
messages: messageResponse.messages
.compactMap { rawMessage -> SnodeReceivedMessage? in
@ -723,7 +722,7 @@ public final class SnodeAPI {
_ messages: [(message: SnodeMessage, namespace: Namespace)],
allObsoleteHashes: [String],
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<HTTP.BatchResponse, Error> {
) -> AnyPublisher<(ResponseInfoType, HTTP.BatchResponse), Error> {
guard
!messages.isEmpty,
let recipient: String = messages.first?.message.recipient
@ -793,7 +792,7 @@ public final class SnodeAPI {
let responseTypes = requests.map { $0.responseType }
return getSwarm(for: publicKey)
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<HTTP.BatchResponse, Error> in
.tryFlatMapWithRandomSnode(retry: maxRetryCount) { snode -> AnyPublisher<(ResponseInfoType, HTTP.BatchResponse), Error> in
SnodeAPI
.send(
request: SnodeRequest(

View File

@ -10,9 +10,14 @@ public extension SnodeAPI {
case configContacts = 3
case configConvoInfoVolatile = 4
case configUserGroups = 5
case configGroupInfo = 11
case configGroupMembers = 12
case configGroupKeys = 13
// Messages sent to a closed group:
case groupMessages = 11
// Groups config namespaces (i.e. for shared config of the group itself, not one user's group settings)
case configGroupInfo = 12
case configGroupMembers = 13
case configGroupKeys = 14
case legacyClosedGroup = -10
@ -50,7 +55,7 @@ public extension SnodeAPI {
/// we have seen)
public var shouldDedupeMessages: Bool {
switch self {
case .`default`, .legacyClosedGroup: return true
case .`default`, .legacyClosedGroup, .groupMessages: return true
case .configUserProfile, .configContacts,
.configConvoInfoVolatile, .configUserGroups,
@ -87,7 +92,7 @@ public extension SnodeAPI {
///
var batchRequestSizePriority: Int64 {
switch self {
case .`default`, .legacyClosedGroup: return 10
case .`default`, .legacyClosedGroup, .groupMessages: return 10
case .configUserProfile, .configContacts,
.configConvoInfoVolatile, .configUserGroups,

View File

@ -0,0 +1,115 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import SessionUtilitiesKit
import Quick
import Nimble
@testable import SessionSnodeKit
class PreparedRequestOnionRequestsSpec: QuickSpec {
enum TestEndpoint: EndpointType {
case endpoint1
case endpoint2
static var batchRequestVariant: HTTP.BatchRequest.Child.Variant { .storageServer }
static var excludedSubRequestHeaders: [HTTPHeader] { [] }
var path: String {
switch self {
case .endpoint1: return "endpoint1"
case .endpoint2: return "endpoint2"
}
}
}
// MARK: - Spec
override func spec() {
var mockNetwork: MockNetwork!
var dependencies: Dependencies!
var disposables: [AnyCancellable] = []
var error: Error?
var preparedRequest: HTTP.PreparedRequest<Int>?
describe("a PreparedRequest sending Onion Requests") {
// MARK: - Configuration
beforeEach {
mockNetwork = MockNetwork()
dependencies = Dependencies(
network: mockNetwork,
dateNow: Date(timeIntervalSince1970: 1234567890)
)
let request = Request<NoBody, TestEndpoint>(
method: .post,
server: "https://www.oxen.io",
endpoint: TestEndpoint.endpoint1
)
preparedRequest = HTTP.PreparedRequest(
request: request,
urlRequest: try! request.generateUrlRequest(),
publicKey: TestConstants.publicKey,
responseType: Int.self,
metadata: [:],
retryCount: 0,
timeout: 10
)
}
afterEach {
disposables.forEach { $0.cancel() }
mockNetwork = nil
dependencies = nil
disposables = []
error = nil
preparedRequest = nil
}
// MARK: -- when sending
context("when sending") {
beforeEach {
mockNetwork
.when { $0.send(.onionRequest(any(), to: any(), with: any())) }
.thenReturn(MockNetwork.response(with: 1))
}
// MARK: -- triggers sending correctly
it("triggers sending correctly") {
var response: (info: ResponseInfoType, data: Int)?
preparedRequest
.send(using: dependencies)
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
expect(response).toNot(beNil())
expect(response?.data).to(equal(1))
expect(error).to(beNil())
}
// MARK: -- returns an error when the prepared request is null
it("returns an error when the prepared request is null") {
var response: (info: ResponseInfoType, data: Int)?
preparedRequest = nil
preparedRequest
.send(using: dependencies)
.handleEvents(receiveOutput: { result in response = result })
.mapError { error.setting(to: $0) }
.sinkAndStore(in: &disposables)
expect(error).to(matchError(HTTPError.invalidPreparedRequest))
expect(response).to(beNil())
}
}
}
}
}

View File

@ -29,6 +29,7 @@ extension Publishers {
public extension Publisher {
func retry(_ retries: Int, using dependencies: Dependencies) -> AnyPublisher<Output, Failure> {
guard retries > 0 else { return self.eraseToAnyPublisher() }
guard !dependencies.forceSynchronous else {
return Publishers.RetryWithDependencies(upstream: self, retries: retries, dependencies: dependencies)
.eraseToAnyPublisher()

View File

@ -50,7 +50,7 @@ open class Storage {
private var unprocessedMigrationRequirements: Atomic<[MigrationRequirement]> = Atomic(MigrationRequirement.allCases)
private var migrator: DatabaseMigrator?
private var migrationProgressUpdater: Atomic<((String, CGFloat) -> ())>?
private var migrationRequirementProcesser: Atomic<(Database?, MigrationRequirement) -> ()>?
private var migrationRequirementProcesser: Atomic<(Database, MigrationRequirement) -> ()>?
// MARK: - Initialization
@ -151,7 +151,7 @@ open class Storage {
migrationTargets: [MigratableTarget.Type],
async: Bool = true,
onProgressUpdate: ((CGFloat, TimeInterval) -> ())?,
onMigrationRequirement: @escaping (Database?, MigrationRequirement) -> (),
onMigrationRequirement: @escaping (Database, MigrationRequirement) -> (),
onComplete: @escaping (Swift.Result<Void, Error>, Bool) -> ()
) {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
@ -240,15 +240,21 @@ open class Storage {
// Store the logic to run when the migration completes
let migrationCompleted: (Swift.Result<Void, Error>) -> () = { [weak self] result in
// Process any unprocessed requirements which need to be processed before completion
// then clear out the state
self?.unprocessedMigrationRequirements.wrappedValue
.filter { $0.shouldProcessAtCompletionIfNotRequired }
.forEach { self?.migrationRequirementProcesser?.wrappedValue(nil, $0) }
// Clear out the stored migration state
let remainingRequirements: [MigrationRequirement] = (self?.unprocessedMigrationRequirements.wrappedValue
.filter { $0.shouldProcessAtCompletionIfNotRequired })
.defaulting(to: [])
let requirementProcesser: ((Database, MigrationRequirement) -> ())? = self?.migrationRequirementProcesser?.wrappedValue
self?.migrationsCompleted.mutate { $0 = true }
self?.migrationProgressUpdater = nil
self?.migrationRequirementProcesser = nil
// Process any unprocessed requirements which need to be processed before completion
// then clear out the state
if !remainingRequirements.isEmpty && requirementProcesser != nil {
self?.write { db in remainingRequirements.forEach { requirementProcesser?(db, $0) } }
}
// Reset in case there is a requirement on a migration which runs when returning from
// the background
self?.unprocessedMigrationRequirements.mutate { $0 = MigrationRequirement.allCases }

View File

@ -66,4 +66,14 @@ public extension SQLInterpolation {
private func generateSelection<T: ColumnExpressible>(for type: T.Type) -> String {
return "SELECT 1"
}
/// Appends the table name of the record type.
///
/// // SELECT * FROM user WHERE user.id LIKE '05%'
/// let user: TypedTableAlias<User> = TypedTableAlias()
/// let request: SQLRequest<User> = "SELECT * FROM \(user) WHERE \(user[.id]) LIKE '\(SessionId.Prefix.standard)%'"
@_disfavoredOverload
mutating func appendInterpolation(_ idPrefix: SessionId.Prefix) {
appendLiteral("\(SQL(stringLiteral: "\(idPrefix.rawValue)"))")
}
}

View File

@ -0,0 +1,51 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension HTTP {
struct BatchRequest: Encodable {
let requests: [Child]
public init(requests: [any ErasedPreparedRequest]) {
self.requests = requests.map { Child(request: $0) }
}
// MARK: - Encodable
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(requests)
}
// MARK: - BatchRequest.Child
public struct Child: Encodable {
public enum Variant {
case unsupported
case sogs
case storageServer
}
enum CodingKeys: String, CodingKey {
case method
// SOGS keys
case path
case headers
case json
case b64
case bytes
// Storage Server keys
case params
}
let request: any ErasedPreparedRequest
public func encode(to encoder: Encoder) throws {
try request.encodeForBatchRequest(to: encoder)
}
}
}
}

View File

@ -4,57 +4,59 @@ import Foundation
import Combine
public extension HTTP {
// MARK: - BatchResponse
// MARK: - HTTP.BatchResponse
struct BatchResponse {
public let info: ResponseInfoType
public let responses: [Decodable]
typealias BatchResponse = [Decodable]
// MARK: - BatchResponseMap<E>
struct BatchResponseMap<E: EndpointType>: Decodable, ErasedBatchResponseMap {
public let data: [E: Decodable]
public static func decodingResponses(
from data: Data?,
as types: [Decodable.Type],
requireAllResults: Bool,
using dependencies: Dependencies = Dependencies()
) throws -> [Decodable] {
// Need to split the data into an array of data so each item can be Decoded correctly
guard let data: Data = data else { throw HTTPError.parsingFailed }
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
throw HTTPError.parsingFailed
}
public subscript(position: E) -> Decodable? {
get { return data[position] }
}
public var count: Int { data.count }
public var keys: Dictionary<E, Decodable>.Keys { data.keys }
public var values: Dictionary<E, Decodable>.Values { data.values }
// MARK: - Initialization
init(data: [E: Decodable]) {
self.data = data
}
public init(from decoder: Decoder) throws {
#if DEBUG
preconditionFailure("The `HTTP.BatchResponseMap` type cannot be decoded directly, this is simply here to allow for `PreparedSendData<HTTP.BatchResponseMap>` support")
#else
data = [:]
#endif
}
// MARK: - ErasedBatchResponseMap
public static func from(
batchEndpoints: [any EndpointType],
responses: [Decodable]
) throws -> Self {
let convertedEndpoints: [E] = batchEndpoints.compactMap { $0 as? E }
let dataArray: [Data]
guard convertedEndpoints.count == responses.count else { throw HTTPError.parsingFailed }
switch jsonObject {
case let anyArray as [Any]:
dataArray = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) }
guard !requireAllResults || dataArray.count == types.count else {
throw HTTPError.parsingFailed
return BatchResponseMap(
data: zip(convertedEndpoints, responses)
.reduce(into: [:]) { result, next in
result[next.0] = next.1
}
case let anyDict as [String: Any]:
guard
let resultsArray: [Data] = (anyDict["results"] as? [Any])?
.compactMap({ try? JSONSerialization.data(withJSONObject: $0) }),
(
!requireAllResults ||
resultsArray.count == types.count
)
else { throw HTTPError.parsingFailed }
dataArray = resultsArray
default: throw HTTPError.parsingFailed
}
return try zip(dataArray, types)
.map { data, type in try type.decoded(from: data, using: dependencies) }
)
}
}
// MARK: - BatchSubResponse<T>
struct BatchSubResponse<T: Decodable>: BatchSubResponseType {
struct BatchSubResponse<T>: ResponseInfoType {
public enum CodingKeys: String, CodingKey {
case code
case headers
@ -87,22 +89,22 @@ public extension HTTP {
}
}
public protocol BatchSubResponseType: Decodable {
var code: Int { get }
var headers: [String: String] { get }
var failedToParseBody: Bool { get }
// MARK: - ErasedBatchResponseMap
public protocol ErasedBatchResponseMap {
static func from(
batchEndpoints: [any EndpointType],
responses: [Decodable]
) throws -> Self
}
extension BatchSubResponseType {
public var responseInfo: ResponseInfoType { HTTP.ResponseInfo(code: code, headers: headers) }
}
// MARK: - BatchSubResponse<T> Coding
extension HTTP.BatchSubResponse: Encodable where T: Encodable {}
public extension HTTP.BatchSubResponse {
init(from decoder: Decoder) throws {
extension HTTP.BatchSubResponse: Decodable {
public init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
let body: T? = try? container.decode(T.self, forKey: .body)
let body: T? = ((try? (T.self as? Decodable.Type)?.decoded(with: container, forKey: .body)) as? T)
self = HTTP.BatchSubResponse(
code: try container.decode(Int.self, forKey: .code),
@ -119,9 +121,46 @@ public extension HTTP.BatchSubResponse {
// MARK: - Convenience
public extension Decodable {
static func decoded(from data: Data, using dependencies: Dependencies = Dependencies()) throws -> Self {
return try data.decoded(as: Self.self, using: dependencies)
internal extension HTTP.BatchResponse {
static func decodingResponses(
from data: Data?,
as types: [Decodable.Type],
requireAllResults: Bool,
using dependencies: Dependencies = Dependencies()
) throws -> HTTP.BatchResponse {
// Need to split the data into an array of data so each item can be Decoded correctly
guard let data: Data = data else { throw HTTPError.parsingFailed }
guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else {
throw HTTPError.parsingFailed
}
let dataArray: [Data]
switch jsonObject {
case let anyArray as [Any]:
dataArray = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) }
guard !requireAllResults || dataArray.count == types.count else {
throw HTTPError.parsingFailed
}
case let anyDict as [String: Any]:
guard
let resultsArray: [Data] = (anyDict["results"] as? [Any])?
.compactMap({ try? JSONSerialization.data(withJSONObject: $0) }),
(
!requireAllResults ||
resultsArray.count == types.count
)
else { throw HTTPError.parsingFailed }
dataArray = resultsArray
default: throw HTTPError.parsingFailed
}
return try zip(dataArray, types)
.map { data, type in try type.decoded(from: data, using: dependencies) }
}
}
@ -130,12 +169,12 @@ public extension Publisher where Output == (ResponseInfoType, Data?), Failure ==
as types: [Decodable.Type],
requireAllResults: Bool = true,
using dependencies: Dependencies = Dependencies()
) -> AnyPublisher<HTTP.BatchResponse, Error> {
) -> AnyPublisher<(ResponseInfoType, HTTP.BatchResponse), Error> {
self
.tryMap { responseInfo, maybeData -> HTTP.BatchResponse in
HTTP.BatchResponse(
info: responseInfo,
responses: try HTTP.BatchResponse.decodingResponses(
.tryMap { responseInfo, maybeData -> (ResponseInfoType, HTTP.BatchResponse) in
(
responseInfo,
try HTTP.BatchResponse.decodingResponses(
from: maybeData,
as: types,
requireAllResults: requireAllResults,

View File

@ -7,6 +7,8 @@ public enum HTTPError: LocalizedError, Equatable {
case invalidURL
case invalidJSON
case parsingFailed
case invalidPreparedRequest
case invalidRequest
case invalidResponse
case maxFileSizeExceeded
case httpRequestFailed(statusCode: UInt, data: Data?)
@ -17,6 +19,8 @@ public enum HTTPError: LocalizedError, Equatable {
case .generic: return "An error occurred."
case .invalidURL: return "Invalid URL."
case .invalidJSON: return "Invalid JSON."
case .invalidPreparedRequest: return "Invalid PreparedRequest provided."
case .invalidRequest: return "Invalid request."
case .parsingFailed, .invalidResponse: return "Invalid response."
case .maxFileSizeExceeded: return "Maximum file size exceeded."
case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)."

Some files were not shown because too many files have changed in this diff Show More