[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:
parent
812a951aba
commit
f44b545265
|
@ -1 +1 @@
|
|||
Subproject commit e3ccf29db08aaf0b9bb6bbe72ae5967cd183a78d
|
||||
Subproject commit 8d9ce6e30153a785b13354c99a9a210d5e8fc1a7
|
15
Podfile
15
Podfile
|
@ -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
|
||||
|
||||
|
|
|
@ -133,6 +133,6 @@ SPEC CHECKSUMS:
|
|||
xcbeautify: 6e2f57af5c3a86d490376d5758030a8dcc201c1b
|
||||
YYImage: f1ddd15ac032a58b78bbed1e012b50302d318331
|
||||
|
||||
PODFILE CHECKSUM: a5e8cbfd90e94ce2afb8c687b96bceb93f658e2e
|
||||
PODFILE CHECKSUM: 61875903156c6d0a9bdd56cb9af69f77648f2009
|
||||
|
||||
COCOAPODS: 1.12.1
|
||||
|
|
|
@ -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 */; };
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -537,7 +537,7 @@ class ThreadDisappearingMessagesSettingsViewModel: SessionTableViewModel<ThreadD
|
|||
try SessionUtil
|
||||
.update(
|
||||
db,
|
||||
groupPublicKey: threadId,
|
||||
legacyGroupPublicKey: threadId,
|
||||
disappearingConfig: updatedConfig
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)) },
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
@ -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(
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -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")
|
||||
)
|
||||
"""
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -0,0 +1,3 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public extension HTTPRequestMetadata {
|
||||
static let forceBlinded: HTTPRequestMetadata = "forceBlinded"
|
||||
}
|
|
@ -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 {}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
}
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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 }
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
public extension PushNotificationAPI {
|
||||
struct LegacyPushServerResponse: Codable {
|
||||
let code: Int
|
||||
let message: String?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
public extension PushNotificationAPI {
|
||||
struct SubscribeResponse: Codable {
|
||||
/// Flag indicating the success of the registration
|
||||
let success: Bool?
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
public extension PushNotificationAPI {
|
||||
struct UnsubscribeResponse: Codable {
|
||||
/// Flag indicating the success of the registration
|
||||
let success: Bool?
|
||||
|
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -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
|
||||
) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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 {
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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\""))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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()),
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -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)"))")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue