Started working on the integration

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	SessionMessagingKit/Configuration.swift
#	SessionMessagingKit/Database/Migrations/_012_AddClosedGroupInfo.swift
#	SessionMessagingKit/Database/Migrations/_013_AutoDownloadAttachments.swift
#	SessionMessagingKit/Open Groups/Models/SOGSBatchRequest.swift
#	SessionSnodeKit/OnionRequestAPI.swift
#	SessionSnodeKit/SSKDependencies.swift
#	SessionTests/Conversations/Settings/ThreadDisappearingMessagesViewModelSpec.swift
#	SessionTests/Conversations/Settings/ThreadSettingsViewModelSpec.swift
#	SessionTests/Settings/NotificationContentViewModelSpec.swift
#	SessionUtilitiesKit/Networking/BatchResponse.swift
This commit is contained in:
Morgan Pretty 2022-11-28 17:46:08 +11:00
parent ff65c84504
commit edf3bde573
38 changed files with 2623 additions and 450 deletions

View File

@ -642,8 +642,6 @@
FD3AABE928306BBD00E5099A /* ThreadPickerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */; };
FD3C905C27E3FBEF00CD579F /* BatchRequestInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905B27E3FBEF00CD579F /* BatchRequestInfoSpec.swift */; };
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; };
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; };
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; };
FD3C906727E416AF00CD579F /* BlindedIdLookupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906627E416AF00CD579F /* BlindedIdLookupSpec.swift */; };
FD3C906A27E417CE00CD579F /* SodiumUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906927E417CE00CD579F /* SodiumUtilitiesSpec.swift */; };
FD3C906D27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906C27E43C4B00CD579F /* MessageSenderEncryptionSpec.swift */; };
@ -753,6 +751,40 @@
FD87DCFE28B7582C00AF0F98 /* BlockedContactsViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */; };
FD87DD0028B820F200AF0F98 /* BlockedContactCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */; };
FD87DD0428B8727D00AF0F98 /* Configuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD87DD0328B8727D00AF0F98 /* Configuration.swift */; };
FD8ECF40292AF07900C0D1BB /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF3F292AF07900C0D1BB /* Poller.swift */; };
FD8ECF42292B340D00C0D1BB /* SOGSBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */; };
FD8ECF44292B397E00C0D1BB /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */; };
FD8ECF46292B4BD500C0D1BB /* SendMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */; };
FD8ECF48292C287500C0D1BB /* UpdateExpiryRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */; };
FD8ECF4A292C2A7300C0D1BB /* DeleteMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */; };
FD8ECF4C292C2AC200C0D1BB /* RevokeSubkeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */; };
FD8ECF4E292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */; };
FD8ECF50292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */; };
FD8ECF52292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */; };
FD8ECF54292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */; };
FD8ECF56292C327700C0D1BB /* LegacyGetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */; };
FD8ECF58292C350500C0D1BB /* LegacySendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */; };
FD8ECF5A292C431B00C0D1BB /* GetSwarmRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */; };
FD8ECF5C292C469100C0D1BB /* UpdateExpiryResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */; };
FD8ECF5E292C478900C0D1BB /* SnodeRecursiveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */; };
FD8ECF60292C4B2400C0D1BB /* SnodeSwarmItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */; };
FD8ECF62292C4C8200C0D1BB /* DeleteMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */; };
FD8ECF64292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */; };
FD8ECF66292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */; };
FD8ECF68292C72BA00C0D1BB /* UpdateExpiryAllResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */; };
FD8ECF6A292C74A000C0D1BB /* RevokeSubkeyResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */; };
FD8ECF6C292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */; };
FD8ECF6E292C9EA100C0D1BB /* GetServiceNodesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */; };
FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */; };
FD8ECF74292DDB4A00C0D1BB /* Format.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF73292DDB4A00C0D1BB /* Format.swift */; };
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */; };
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */; };
FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */; };
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */; };
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */; };
FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */; };
FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906127E411AF00CD579F /* HeaderSpec.swift */; };
FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C906327E4122F00CD579F /* RequestSpec.swift */; };
FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; };
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; };
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
@ -835,6 +867,8 @@
FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */; };
FDF0B75C2807F41D004C14C5 /* MessageSender+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */; };
FDF0B75E280AAF35004C14C5 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B75D280AAF35004C14C5 /* Preferences.swift */; };
FDF1AD5F28FF5F930080A701 /* EditGroupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */; };
FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */; };
FDF222072818CECF000A4995 /* ConversationViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222062818CECF000A4995 /* ConversationViewModel.swift */; };
FDF222092818D2B0000A4995 /* NSAttributedString+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */; };
FDF2220B2818F38D000A4995 /* SessionApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF2220A2818F38D000A4995 /* SessionApp.swift */; };
@ -1721,6 +1755,13 @@
FD37EA1628AC5605003AE748 /* NotificationContentViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationContentViewModel.swift; sourceTree = "<group>"; };
FD37EA1828AC5CCA003AE748 /* NotificationSoundViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSoundViewModel.swift; sourceTree = "<group>"; };
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _007_HomeQueryOptimisationIndexes.swift; sourceTree = "<group>"; };
FD37F5572908F5C3005A5E92 /* RemoveUsersModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoveUsersModal.swift; sourceTree = "<group>"; };
FD37F559290F9E9A005A5E92 /* GroupMembersViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMembersViewModel.swift; sourceTree = "<group>"; };
FD37F55F291867A8005A5E92 /* ImagePickerHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagePickerHandler.swift; sourceTree = "<group>"; };
FD37F5612918C471005A5E92 /* GroupMemberLeftMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupMemberLeftMessage.swift; sourceTree = "<group>"; };
FD37F5632918C58D005A5E92 /* GroupInviteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupInviteMessage.swift; sourceTree = "<group>"; };
FD37F5652918C697005A5E92 /* GroupPromoteMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GroupPromoteMessage.swift; sourceTree = "<group>"; };
FD37F5672918D458005A5E92 /* MessageReceiver+ClosedGroups.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MessageReceiver+ClosedGroups.swift"; sourceTree = "<group>"; };
FD39352B28F382920084DADA /* VersionFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionFooterView.swift; sourceTree = "<group>"; };
FD39353528F7C3390084DADA /* _004_FlagMessageHashAsDeletedOrInvalid.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_FlagMessageHashAsDeletedOrInvalid.swift; sourceTree = "<group>"; };
FD3AABE828306BBD00E5099A /* ThreadPickerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadPickerViewModel.swift; sourceTree = "<group>"; };
@ -1832,6 +1873,38 @@
FD87DCFD28B7582C00AF0F98 /* BlockedContactsViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactsViewModel.swift; sourceTree = "<group>"; };
FD87DCFF28B820F200AF0F98 /* BlockedContactCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlockedContactCell.swift; sourceTree = "<group>"; };
FD87DD0328B8727D00AF0F98 /* Configuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Configuration.swift; sourceTree = "<group>"; };
FD8ECF3F292AF07900C0D1BB /* Poller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Poller.swift; sourceTree = "<group>"; };
FD8ECF41292B340D00C0D1BB /* SOGSBatchRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSBatchRequest.swift; sourceTree = "<group>"; };
FD8ECF43292B397E00C0D1BB /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
FD8ECF45292B4BD500C0D1BB /* SendMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageResponse.swift; sourceTree = "<group>"; };
FD8ECF47292C287500C0D1BB /* UpdateExpiryRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryRequest.swift; sourceTree = "<group>"; };
FD8ECF49292C2A7300C0D1BB /* DeleteMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesRequest.swift; sourceTree = "<group>"; };
FD8ECF4B292C2AC200C0D1BB /* RevokeSubkeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyRequest.swift; sourceTree = "<group>"; };
FD8ECF4D292C2AF800C0D1BB /* DeleteAllMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesRequest.swift; sourceTree = "<group>"; };
FD8ECF4F292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeRequest.swift; sourceTree = "<group>"; };
FD8ECF51292C2CAE00C0D1BB /* UpdateExpiryAllRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllRequest.swift; sourceTree = "<group>"; };
FD8ECF53292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeAuthenticatedRequestBody.swift; sourceTree = "<group>"; };
FD8ECF55292C327700C0D1BB /* LegacyGetMessagesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGetMessagesRequest.swift; sourceTree = "<group>"; };
FD8ECF57292C350500C0D1BB /* LegacySendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacySendMessageRequest.swift; sourceTree = "<group>"; };
FD8ECF59292C431B00C0D1BB /* GetSwarmRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSwarmRequest.swift; sourceTree = "<group>"; };
FD8ECF5B292C469100C0D1BB /* UpdateExpiryResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryResponse.swift; sourceTree = "<group>"; };
FD8ECF5D292C478900C0D1BB /* SnodeRecursiveResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeRecursiveResponse.swift; sourceTree = "<group>"; };
FD8ECF5F292C4B2400C0D1BB /* SnodeSwarmItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeSwarmItem.swift; sourceTree = "<group>"; };
FD8ECF61292C4C8200C0D1BB /* DeleteMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteMessagesResponse.swift; sourceTree = "<group>"; };
FD8ECF63292C4D6600C0D1BB /* DeleteAllMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllMessagesResponse.swift; sourceTree = "<group>"; };
FD8ECF65292C6F8200C0D1BB /* DeleteAllBeforeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteAllBeforeResponse.swift; sourceTree = "<group>"; };
FD8ECF67292C72B900C0D1BB /* UpdateExpiryAllResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateExpiryAllResponse.swift; sourceTree = "<group>"; };
FD8ECF69292C74A000C0D1BB /* RevokeSubkeyResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RevokeSubkeyResponse.swift; sourceTree = "<group>"; };
FD8ECF6B292C9B6400C0D1BB /* GetNetworkTimestampResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetNetworkTimestampResponse.swift; sourceTree = "<group>"; };
FD8ECF6D292C9EA100C0D1BB /* GetServiceNodesRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetServiceNodesRequest.swift; sourceTree = "<group>"; };
FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _013_AutoDownloadAttachments.swift; sourceTree = "<group>"; };
FD8ECF73292DDB4A00C0D1BB /* Format.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Format.swift; sourceTree = "<group>"; };
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = "libsession-util.xcframework"; sourceTree = "<group>"; };
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionUtil.swift; sourceTree = "<group>"; };
FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _011_SharedUtilChanges.swift; sourceTree = "<group>"; };
FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharedConfigDump.swift; sourceTree = "<group>"; };
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigUserProfileSpec.swift; sourceTree = "<group>"; };
FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = "<group>"; };
@ -1915,6 +1988,8 @@
FDF0B7592807F3A3004C14C5 /* MessageSenderError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageSenderError.swift; sourceTree = "<group>"; };
FDF0B75B2807F41D004C14C5 /* MessageSender+Convenience.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "MessageSender+Convenience.swift"; sourceTree = "<group>"; };
FDF0B75D280AAF35004C14C5 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
FDF1AD5E28FF5F930080A701 /* EditGroupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditGroupViewModel.swift; sourceTree = "<group>"; };
FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _012_AddClosedGroupInfo.swift; sourceTree = "<group>"; };
FDF222062818CECF000A4995 /* ConversationViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationViewModel.swift; sourceTree = "<group>"; };
FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+Utilities.swift"; sourceTree = "<group>"; };
FDF2220A2818F38D000A4995 /* SessionApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionApp.swift; sourceTree = "<group>"; };
@ -1998,6 +2073,7 @@
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FD8ECF7929340F7200C0D1BB /* libsession-util.xcframework in Frameworks */,
FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */,
C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */,
CEE449BA3596483519120D91 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */,
@ -2241,14 +2317,6 @@
path = "Views & Modals";
sourceTree = "<group>";
};
7B81682428B30BEC0069F315 /* Recovered References */ = {
isa = PBXGroup;
children = (
FD37EA1A28ACB51F003AE748 /* _007_HomeQueryOptimisationIndexes.swift */,
);
name = "Recovered References";
sourceTree = "<group>";
};
7B8C44C328B49DA900FBE25F /* New Conversation */ = {
isa = PBXGroup;
children = (
@ -3232,6 +3300,8 @@
C3A721332558BDDF0043A11F /* Open Groups */,
FD3E0C82283B581F002A425C /* Shared Models */,
C3BBE0B32554F0D30050F1E3 /* Utilities */,
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */,
FDC438C027BB4E6800C60D73 /* SMKDependencies.swift */,
FD245C612850664300B966DD /* Configuration.swift */,
);
path = SessionMessagingKit;
@ -3391,7 +3461,6 @@
D221A08C169C9E5E00537ABF /* Frameworks */,
D221A08A169C9E5E00537ABF /* Products */,
2BADBA206E0B8D297E313FBA /* Pods */,
7B81682428B30BEC0069F315 /* Recovered References */,
);
sourceTree = "<group>";
};
@ -3524,6 +3593,7 @@
FDE77F6A280FEB28002CFC5D /* ControlMessageProcessRecord.swift */,
FD5C7308285007920029977D /* BlindedIdLookup.swift */,
FD09B7E6288670FD00ED0B66 /* Reaction.swift */,
FD8ECF7E2934298100C0D1BB /* SharedConfigDump.swift */,
);
path = Models;
sourceTree = "<group>";
@ -3541,6 +3611,9 @@
FD09B7E4288670BB00ED0B66 /* _008_EmojiReacts.swift */,
7BAA7B6528D2DE4700AE1489 /* _009_OpenGroupPermission.swift */,
FD7115F128C6CB3900B47552 /* _010_AddThreadIdToFTS.swift */,
FD8ECF7C2934293A00C0D1BB /* _011_SharedUtilChanges.swift */,
FDF1AD6028FF61110080A701 /* _012_AddClosedGroupInfo.swift */,
FD8ECF71292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift */,
);
path = Migrations;
sourceTree = "<group>";
@ -3728,8 +3801,6 @@
isa = PBXGroup;
children = (
FD3C905E27E410EE00CD579F /* Models */,
FD3C906127E411AF00CD579F /* HeaderSpec.swift */,
FD3C906327E4122F00CD579F /* RequestSpec.swift */,
);
path = "Common Networking";
sourceTree = "<group>";
@ -3899,6 +3970,7 @@
children = (
FD37EA1228AB3F60003AE748 /* Database */,
FD83B9B927CF20A5005E1583 /* General */,
FD8ECF832934507500C0D1BB /* Networking */,
);
path = SessionUtilitiesKitTests;
sourceTree = "<group>";
@ -3949,6 +4021,33 @@
path = Types;
sourceTree = "<group>";
};
FD8ECF7529340F4800C0D1BB /* LibSessionUtil */ = {
isa = PBXGroup;
children = (
FD8ECF7829340F7100C0D1BB /* libsession-util.xcframework */,
FD8ECF7A29340FFD00C0D1BB /* SessionUtil.swift */,
);
path = LibSessionUtil;
sourceTree = "<group>";
};
FD8ECF802934385900C0D1BB /* LibSessionUtil */ = {
isa = PBXGroup;
children = (
FD8ECF812934387A00C0D1BB /* ConfigUserProfileSpec.swift */,
);
path = LibSessionUtil;
sourceTree = "<group>";
};
FD8ECF832934507500C0D1BB /* Networking */ = {
isa = PBXGroup;
children = (
FD3C906127E411AF00CD579F /* HeaderSpec.swift */,
FD3C906327E4122F00CD579F /* RequestSpec.swift */,
FD8ECF842934508B00C0D1BB /* BatchResponseSpec.swift */,
);
path = Networking;
sourceTree = "<group>";
};
FD9004102818ABB000ABAAF6 /* JobRunner */ = {
isa = PBXGroup;
children = (
@ -4050,6 +4149,7 @@
FDC4389827BA001800C60D73 /* Open Groups */,
FD3C906B27E43C2400CD579F /* Sending & Receiving */,
FD3C906827E417B100CD579F /* Utilities */,
FD8ECF802934385900C0D1BB /* LibSessionUtil */,
);
path = SessionMessagingKitTests;
sourceTree = "<group>";
@ -5390,6 +5490,7 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FD8ECF7B29340FFD00C0D1BB /* SessionUtil.swift in Sources */,
7B81682828B310D50069F315 /* _007_HomeQueryOptimisationIndexes.swift in Sources */,
FD245C52285065D500B966DD /* SignalAttachment.swift in Sources */,
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
@ -5474,6 +5575,7 @@
FDF0B74F28079E5E004C14C5 /* SendReadReceiptsJob.swift in Sources */,
FDF0B7422804EA4F004C14C5 /* _002_SetupStandardJobs.swift in Sources */,
B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */,
FD8ECF7D2934293A00C0D1BB /* _011_SharedUtilChanges.swift in Sources */,
FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */,
FD245C5D2850660F00B966DD /* OWSAudioPlayer.m in Sources */,
FDF0B7582807F368004C14C5 /* MessageReceiverError.swift in Sources */,
@ -5520,15 +5622,18 @@
FD83B9CE27D17A04005E1583 /* Request.swift in Sources */,
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
FD8ECF7F2934298100C0D1BB /* SharedConfigDump.swift in Sources */,
C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */,
FDC438C327BB512200C60D73 /* SodiumProtocols.swift in Sources */,
B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */,
FD8ECF72292DCD1A00C0D1BB /* _013_AutoDownloadAttachments.swift in Sources */,
C3DB66C3260ACCE6001EFC55 /* OpenGroupPoller.swift in Sources */,
FD716E722850647600C96BF4 /* Data+Utilities.swift in Sources */,
FD245C5C2850660A00B966DD /* ConfigurationMessage.swift in Sources */,
FD5C7301284F0F7A0029977D /* MessageReceiver+UnsendRequests.swift in Sources */,
C3C2A75F2553A3C500C340D1 /* VisibleMessage+LinkPreview.swift in Sources */,
FD245C642850664F00B966DD /* Threading.swift in Sources */,
FDF1AD6128FF61110080A701 /* _012_AddClosedGroupInfo.swift in Sources */,
FD848B8D283E0B26000E298B /* MessageInputTypes.swift in Sources */,
C32C5C3D256DCBAF003C73A2 /* AppReadiness.m in Sources */,
FD09799B27FFC82D00936362 /* Quote.swift in Sources */,
@ -5783,12 +5888,15 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FD8ECF8729346DB500C0D1BB /* RequestSpec.swift in Sources */,
FD2AAAF228ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4927E02576000769AF /* CommonMockedExtensions.swift in Sources */,
FD83B9BF27CF2294005E1583 /* TestConstants.swift in Sources */,
FD8ECF8629346DA100C0D1BB /* HeaderSpec.swift in Sources */,
FD83B9BB27CF20AF005E1583 /* SessionIdSpec.swift in Sources */,
FDC290A927D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD23EA6328ED0B260058676E /* CombineExtensions.swift in Sources */,
FD8ECF852934508B00C0D1BB /* BatchResponseSpec.swift in Sources */,
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */,
FD37EA1528AB42CB003AE748 /* IdentitySpec.swift in Sources */,
FD1A94FE2900D2EA000D73D3 /* PersistableRecordUtilitiesSpec.swift in Sources */,
@ -5814,7 +5922,6 @@
FD3C906027E410F700CD579F /* FileUploadResponseSpec.swift in Sources */,
FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */,
FDC2909A27D71376005DAE71 /* NonceGeneratorSpec.swift in Sources */,
FD3C906427E4122F00CD579F /* RequestSpec.swift in Sources */,
FD2AAAF128ED57B500A49611 /* SynchronousStorage.swift in Sources */,
FD078E4827E02561000769AF /* CommonMockedExtensions.swift in Sources */,
FD859EF827C2F58900510D0C /* MockAeadXChaCha20Poly1305Ietf.swift in Sources */,
@ -5830,8 +5937,8 @@
FD23EA6228ED0B260058676E /* CombineExtensions.swift in Sources */,
FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */,
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */,
FD8ECF822934387A00C0D1BB /* ConfigUserProfileSpec.swift in Sources */,
FDC290A827D9B46D005DAE71 /* NimbleExtensions.swift in Sources */,
FD3C906227E411AF00CD579F /* HeaderSpec.swift in Sources */,
FDC2908F27D70938005DAE71 /* SendDirectMessageRequestSpec.swift in Sources */,
FDC438BD27BB2AB400C60D73 /* Mockable.swift in Sources */,
FD859EF627C2F52C00510D0C /* MockSign.swift in Sources */,
@ -7037,7 +7144,7 @@
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
FRAMEWORK_SEARCH_PATHS = "";
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
GCC_NO_COMMON_BLOCKS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@ -7078,6 +7185,7 @@
);
"OTHER_SWIFT_FLAGS[arch=*]" = "-D DEBUG";
SDKROOT = iphoneos;
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
SWIFT_VERSION = 4.0;
VALIDATE_PRODUCT = YES;
};
@ -7113,7 +7221,7 @@
CODE_SIGN_IDENTITY = "iPhone Distribution";
ENABLE_BITCODE = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
FRAMEWORK_SEARCH_PATHS = "";
FRAMEWORK_SEARCH_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
GCC_NO_COMMON_BLOCKS = YES;
GCC_TREAT_IMPLICIT_FUNCTION_DECLARATIONS_AS_ERRORS = YES;
GCC_TREAT_INCOMPATIBLE_POINTER_TYPE_WARNINGS_AS_ERRORS = YES;
@ -7151,6 +7259,7 @@
);
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_INCLUDE_PATHS = "\"$(SRCROOT)/SessionMessagingKit/LibSessionUtil\"";
SWIFT_OPTIMIZATION_LEVEL = "-O";
SWIFT_VERSION = 4.0;
VALIDATE_PRODUCT = YES;

View File

@ -27,6 +27,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// MARK: - Lifecycle
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
SessionUtil.loadState() // TODO: Remove this (move to 'Configuration'? Or call directly as part of 'AppSetup'?)
// These should be the first things we do (the startup process can fail without them)
SetCurrentAppContext(MainAppContext())
verifyDBKeysAvailableBeforeBackgroundLaunch()

View File

@ -24,7 +24,8 @@ public enum SNMessagingKit { // Just to make the external API nice
[
_008_EmojiReacts.self,
_009_OpenGroupPermission.self,
_010_AddThreadIdToFTS.self
_010_AddThreadIdToFTS.self,
_011_SharedUtilChanges.self
]
]
)

View File

@ -0,0 +1,28 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
/// This migration recreates the interaction FTS table and adds the threadId so we can do a performant in-conversation
/// searh (currently it's much slower than the global search)
enum _011_SharedUtilChanges: Migration {
static let target: TargetMigrations.Identifier = .messagingKit
static let identifier: String = "SharedUtilChanges"
static let needsConfigSync: Bool = false
static let minExpectedRunDuration: TimeInterval = 0.1
static func migrate(_ db: Database) throws {
try db.create(table: ConfigDump.self) { t in
t.column(.variant, .text)
.notNull()
.primaryKey()
t.column(.data, .blob)
.notNull()
}
// TODO: Create dumps for current data
Storage.update(progress: 1, for: self, in: target) // In case this is the last migration
}
}

View File

@ -0,0 +1,27 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
internal struct ConfigDump: Codable, Equatable, Hashable, Identifiable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible {
public static var databaseTableName: String { "configDump" }
public typealias Columns = CodingKeys
public enum CodingKeys: String, CodingKey, ColumnExpression {
case variant
case data
}
enum Variant: String, Codable, DatabaseValueConvertible {
case userProfile
}
var id: Variant { variant }
/// The type of config this dump is for
public let variant: Variant
/// The data for this dump
public let data: Data
}

View File

@ -0,0 +1,78 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtil
import SessionUtilitiesKit
/*internal*/public enum SessionUtil { // TODO: Rename this to be cleaner?
/*internal*/public static func loadState() {
let storedDump: Data? = Storage.shared
.read { db in try ConfigDump.fetchOne(db, id: .userProfile) }?
.data
var dump: UnsafePointer<CChar>? = nil // TODO: Load from DB/Disk
let dumpLen: size_t = 0
var conf: UnsafeMutablePointer<config_object>? = nil
// var confSetup: UnsafeMutablePointer<UnsafeMutablePointer<config_object>?>? = nil
var error: UnsafeMutablePointer<CChar>? = nil
// TODO: Will need to manually release any unsafe pointers
let result = user_profile_init(&conf, dump, dumpLen, error)
guard result == 0 else { return } // TODO: Throw error
// var conf: UnsafeMutablePointer<config_object>? = confSetup?.pointee
user_profile_set_name(conf, "TestName") // TODO: Confirm success
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp".bytes.map { CChar(bitPattern: $0) }
let profileKey: [CChar] = "secretNOTSECRET".bytes.map { CChar(bitPattern: $0) }
let profilePic: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
profileKey.withUnsafeBufferPointer { profileKeyPtr in
user_profile_pic(
url: profileUrlPtr.baseAddress,
key: profileKeyPtr.baseAddress,
keylen: profileKey.count
)
}
}
user_profile_set_pic(conf, profilePic) // TODO: Confirm success
if config_needs_push(conf) {
print("Needs Push!!!")
}
if config_needs_dump(conf) {
print("Needs Dump!!!")
}
var toPush: UnsafeMutablePointer<CChar>? = nil
var pushLen: Int = 0
let seqNo = config_push(conf, &toPush, &pushLen)
//var remoteAddr: [CChar] = remote.bytes.map { CChar(bitPattern: $0) }
//config_dump(conf, &dump1, &dump1len);
free(toPush) // TODO: Confirm
var dumpResult: UnsafeMutablePointer<CChar>? = nil
var dumpResultLen: Int = 0
config_dump(conf, &dumpResult, &dumpResultLen)
print("RAWR")
let str = String(cString: dumpResult!)
let stryBytes = str.bytes
let hexStr = stryBytes.toHexString()
let data = Data(bytes: dumpResult!, count: dumpResultLen)
// dumpResult.
// Storage.shared.write { db in
// try ConfigDump(variant: .userProfile, data: <#T##Data#>)
// .save(db)
// }
//
print("RAWR2")
//String(cString: dumpResult!)
}
}

View File

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AvailableLibraries</key>
<array>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64</string>
<key>LibraryPath</key>
<string>libsession-util.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
</dict>
<dict>
<key>LibraryIdentifier</key>
<string>ios-arm64_x86_64-simulator</string>
<key>LibraryPath</key>
<string>libsession-util.a</string>
<key>SupportedArchitectures</key>
<array>
<string>arm64</string>
<string>x86_64</string>
</array>
<key>SupportedPlatform</key>
<string>ios</string>
<key>SupportedPlatformVariant</key>
<string>simulator</string>
</dict>
</array>
<key>CFBundlePackageType</key>
<string>XFWK</string>
<key>XCFrameworkFormatVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -0,0 +1,9 @@
module SessionUtil {
module capi {
header "session/config/error.h"
header "session/config/user_profile.h"
header "session/config/base.h"
header "session/xed25519.h"
export *
}
}

View File

@ -0,0 +1,58 @@
#pragma once
#include <oxenc/bt_value.h>
#include <cassert>
#ifndef NDEBUG
#include <algorithm>
#endif
namespace session::bt {
using oxenc::bt_dict;
using oxenc::bt_list;
/// Merges two bt dicts together: the returned dict includes all keys in a or b. Keys in *both*
/// dicts get their value from `a`, otherwise the value is that of the dict that contains the key.
bt_dict merge(const bt_dict& a, const bt_dict& b);
/// Merges two ordered bt_lists together using a predicate to determine order. The input lists must
/// be sorted to begin with. `cmp` must be callable with a pair of `const bt_value&` arguments and
/// must return true if the first argument should be considered less than the second argument. By
/// default this skips elements from b that compare equal to a value of a, but you can include all
/// the duplicates by specifying the `duplicates` parameter as true.
template <typename Compare>
bt_list merge_sorted(const bt_list& a, const bt_list& b, Compare cmp, bool duplicates = false) {
bt_list result;
auto it_a = a.begin();
auto it_b = b.begin();
assert(std::is_sorted(it_a, a.end(), cmp));
assert(std::is_sorted(it_b, b.end(), cmp));
if (duplicates) {
while (it_a != a.end() && it_b != b.end()) {
if (!cmp(*it_a, *it_b)) // *b <= *a
result.push_back(*it_b++);
else // *a < *b
result.push_back(*it_a++);
}
} else {
while (it_a != a.end() && it_b != b.end()) {
if (cmp(*it_b, *it_a)) // *b < *a
result.push_back(*it_b++);
else if (cmp(*it_a, *it_b)) // *a < *b
result.push_back(*it_a++);
else // *a == *b
++it_b; // skip it
}
}
if (it_a != a.end())
result.insert(result.end(), it_a, a.end());
else if (it_b != b.end())
result.insert(result.end(), it_b, b.end());
return result;
}
} // namespace session::bt

View File

@ -0,0 +1,369 @@
#pragma once
#include <oxenc/bt_serialize.h>
#include <oxenc/bt_value.h>
#include <array>
#include <cassert>
#include <optional>
#include <set>
#include <stdexcept>
#include <variant>
#include <vector>
namespace session::config {
inline constexpr int MAX_MESSAGE_SIZE = 76800; // 76.8kB = Storage server's limit
// Application data data types:
using scalar = std::variant<int64_t, std::string>;
using set = std::set<scalar>;
struct dict_value;
using dict = std::map<std::string, dict_value>;
using dict_variant = std::variant<dict, set, scalar>;
struct dict_value : dict_variant {
using dict_variant::dict_variant;
using dict_variant::operator=;
};
// Helpers for gcc-10 and earlier which don't like visiting a std::variant subtype:
constexpr inline dict_variant& unwrap(dict_value& v) {
return static_cast<dict_variant&>(v);
}
constexpr inline const dict_variant& unwrap(const dict_value& v) {
return static_cast<const dict_variant&>(v);
}
using seqno_t = std::int64_t;
using hash_t = std::array<unsigned char, 32>;
using seqno_hash_t = std::pair<seqno_t, hash_t>;
using ustring = std::basic_string<unsigned char>;
using ustring_view = std::basic_string_view<unsigned char>;
class MutableConfigMessage;
/// Base type for all errors that can happen during config parsing
struct config_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Type thrown for bad signatures (bad or missing signature).
struct signature_error : config_error {
using config_error::config_error;
};
/// Type thrown for a missing signature when a signature is required.
struct missing_signature : signature_error {
using signature_error::signature_error;
};
/// Type thrown for an unparseable config (e.g. keys with invalid types, or keys before "#" or after
/// "~").
struct config_parse_error : config_error {
using config_error::config_error;
};
/// Class for a parsed, read-only config message; also serves as the base class of a
/// MutableConfigMessage which allows setting values.
class ConfigMessage {
public:
using lagged_diffs_t = std::map<seqno_hash_t, oxenc::bt_dict>;
#ifndef SESSION_TESTING_EXPOSE_INTERNALS
protected:
#endif
dict data_;
// diff data for *this* message, parsed during construction. Subclasses may use this for
// managing their own diff in the `diff()` method.
oxenc::bt_dict diff_;
// diffs of previous messages that are included in this message.
lagged_diffs_t lagged_diffs_;
// Unknown top-level config keys which we preserve even though we don't understand what they
// mean.
oxenc::bt_dict unknown_;
/// Seqno and hash of the message; we calculate this when loading. Subclasses put the hash here
/// (so that they can return a reference to it).
seqno_hash_t seqno_hash_{0, {0}};
bool verified_signature_ = false;
bool merged_ = false;
public:
constexpr static int DEFAULT_DIFF_LAGS = 5;
/// Verification function: this is passed the data that should have been signed and the 64-byte
/// signature. Should return true to accept the signature, false to reject it and skip the
/// message. It can also throw to abort message construction (that is: returning false skips
/// the message when loading multiple messages, but can still continue with other messages;
/// throwing aborts the entire construction).
using verify_callable = std::function<bool(ustring_view data, ustring_view signature)>;
/// Signing function: this is passed the data to be signed and returns the 64-byte signature.
using sign_callable = std::function<std::string(ustring_view data)>;
ConfigMessage();
ConfigMessage(const ConfigMessage&) = default;
ConfigMessage& operator=(const ConfigMessage&) = default;
ConfigMessage(ConfigMessage&&) = default;
ConfigMessage& operator=(ConfigMessage&&) = default;
virtual ~ConfigMessage() = default;
/// Initializes a config message by parsing a serialized message. Throws on any error. See the
/// vector version below for argument descriptions.
explicit ConfigMessage(
std::string_view serialized,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false);
/// Constructs a new ConfigMessage by loading and potentially merging multiple serialized
/// ConfigMessages together, according to the config conflict resolution rules. The result
/// of this call can either be one of the config messages directly (if one is found that
/// includes all the others), or can be a new config message that merges multiple configs
/// together. You can check `.merged()` to see which happened.
///
/// This constructor always requires at least one valid config from the given inputs; if all are
/// empty,
///
/// verifier - a signature verification function. If provided and not nullptr this will be
/// called to verify each signature in the provided messages: any that are missing a signature
/// or for which the verifier returns false will be dropped from consideration for merging. If
/// *all* messages fail verification an exception is raised.
///
/// signer - a signature generation function. This is not used directly by the ConfigMessage,
/// but providing it will allow it to be passed automatically to any MutableConfigMessage
/// derived from this ConfigMessage.
///
/// lag - the lag setting controlling the config merging rules. Any config message with lagged
/// diffs that exceeding this lag value will have those early lagged diffs dropping during
/// loading.
///
/// signature_optional - if true then accept a message with no signature even when a verifier is
/// set, thus allowing unsigned messages (though messages with an invalid signature are still
/// not allowed). This option is ignored when verifier is not set.
///
/// error_callback - if set then any config message parsing error will be passed to this
/// function for handling: the callback typically warns and, if the overall construction should
/// abort, rethrows the error. If this function is omitted then the default skips (without
/// failing) individual parse errors and only aborts construction if *all* messages fail to
/// parse. A simple handler such as `[](const auto& e) { throw e; }` can be used to make any
/// parse error of any message fatal.
explicit ConfigMessage(
const std::vector<std::string_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false,
std::function<void(const config_error&)> error_handler = nullptr);
/// Returns a read-only reference to the contained data. (To get a mutable config object use
/// MutableConfigMessage).
const dict& data() const { return data_; }
/// The verify function; if loading a message with a signature and this is set then it will
/// be called to verify the signature of the message. Takes a pointer to the signing data,
/// the data length, and a pointer to the 64-byte signature.
verify_callable verifier;
/// The signing function; this is not directly used by the non-mutable base class, but will be
/// propagated to mutable config messages that are derived e.g. by calling `.increment()`. This
/// is called when serializing a config message to add a signature. If it is nullptr then no
/// signature is added to the serialized data.
sign_callable signer;
/// How many lagged config diffs that should be carried forward to resolve conflicts,
/// including this message. If 0 then config messages won't have any diffs and will not be
/// mergeable.
int lag = DEFAULT_DIFF_LAGS;
/// The diff structure for changes in *this* config message. Subclasses that need to override
/// should populate into `diff_` and return a reference to it (internal code assumes `diff_` is
/// correct immediately after a call to this).
virtual const oxenc::bt_dict& diff();
/// Returns the seqno of this message
const seqno_t& seqno() const { return seqno_hash_.first; }
/// Calculates the hash of the current message. For a ConfigMessage this is calculated when the
/// message is first loaded; for a MutableConfigMessage this serializes the current value to
/// properly compute the current hash. Subclasses must ensure that seqno_hash_.second is set to
/// the correct value when this is called (and typically return a reference to it).
virtual const hash_t& hash() { return seqno_hash_.second; }
/// After loading multiple config files this flag indicates whether or not we had to produce a
/// new, merged configuration message (true) or did not need to merge (false). (For config
/// messages that were not loaded from serialized data this is always true).
bool merged() const { return merged_; }
/// Returns true if this message contained a valid, verified signature when it was parsed.
/// Returns false otherwise (e.g. not loaded from verification at all; loaded without a
/// verification function; or had no signature and a signature wasn't required).
bool verified_signature() const { return verified_signature_; }
/// Constructs a new MutableConfigMessage from this config message with an incremented seqno.
/// The new config message's diff will reflect changes made after this construction.
virtual MutableConfigMessage increment() const;
/// Serializes this config's data. Note that if the ConfigMessage was constructed from signed,
/// serialized input, this will only produce an exact copy of the original serialized input if
/// it uses the identical, deterministic signing function used to construct the original.
///
/// The optional `enable_signing` argument can be specified as false to disable signing (this is
/// typically for a local serialization value that isn't being pushed to the server). Note that
/// signing is always disabled if there is no signing callback set, regardless of the value of
/// this argument.
virtual std::string serialize(bool enable_signing = true);
protected:
std::string serialize_impl(const oxenc::bt_dict& diff, bool enable_signing = true);
};
// Constructor tag
struct increment_seqno_t {};
inline constexpr increment_seqno_t increment_seqno{};
class MutableConfigMessage : public ConfigMessage {
protected:
dict orig_data_{data_};
friend class ConfigMessage;
public:
MutableConfigMessage(const MutableConfigMessage&) = default;
MutableConfigMessage& operator=(const MutableConfigMessage&) = default;
MutableConfigMessage(MutableConfigMessage&&) = default;
MutableConfigMessage& operator=(MutableConfigMessage&&) = default;
/// Constructs a new, empty config message. Takes various fields to pre-fill the various
/// properties during construction (these are for convenience and equivalent to setting them via
/// properties/methods after construction).
///
/// seqno -- the message's seqno, default 0
/// lags -- number of lags to keep (when deriving messages, e.g. via increment())
/// signer -- if specified and not nullptr then this message will be signed when serialized
/// using the given signing function. If omitted no signing takes place.
explicit MutableConfigMessage(
seqno_t seqno = 0, int lag = DEFAULT_DIFF_LAGS, sign_callable signer = nullptr) {
this->lag = lag;
this->seqno(seqno);
this->signer = signer;
}
/// Wraps the ConfigMessage constructor with the same arguments but always produces a
/// MutableConfigMessage. In particular this means that if the base constructor performed a
/// merge (and thus incremented seqno) then the config stays as is, but contained in a Mutable
/// message that can be changed. If it did *not* merge (i.e. the highest seqno message it found
/// did not conflict with any other messages) then this construction is equivalent to doing a
/// base load followed by a .increment() call. In other words: this constructor *always* gives
/// you an incremented seqno value from the highest valid input config message.
///
/// This is almost equivalent to ConfigMessage{args...}.increment(), except that this
/// constructor only increments seqno once while the indirect version would increment twice in
/// the case of a required merge conflict resolution.
explicit MutableConfigMessage(
const std::vector<std::string_view>& configs,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false,
std::function<void(const config_error&)> error_handler = nullptr);
/// Wrapper around the above that takes a single string view to load a single message, doesn't
/// take an error handler and instead always throws on parse errors (the above also throws for
/// an erroneous single message, but with a less specific "no valid config messages" error).
explicit MutableConfigMessage(
std::string_view config,
verify_callable verifier = nullptr,
sign_callable signer = nullptr,
int lag = DEFAULT_DIFF_LAGS,
bool signature_optional = false);
/// Does the same as the base incrementing, but also records any diff info from the current
/// MutableConfigMessage. *this* object gets pruned and signed as part of this call. If the
/// sign argument is omitted/nullptr then the current object's `sign` callback gets copied into
/// the new object. After this call you typically do not want to further modify *this (because
/// any modifications will change the hash, making *this no longer a parent of the new object).
MutableConfigMessage increment() const override;
/// Constructor that does the same thing as the `m.increment()` factory method. The second
/// value should be the literal `increment_seqno` value (to select this constructor).
explicit MutableConfigMessage(const ConfigMessage& m, increment_seqno_t);
using ConfigMessage::data;
/// Returns a mutable reference to the underlying config data.
dict& data() { return data_; }
using ConfigMessage::seqno;
/// Sets the seqno of the message to a specific value. You usually want to use `.increment()`
/// from an existing config message rather than manually adjusting the seqno.
void seqno(seqno_t new_seqno) { seqno_hash_.first = new_seqno; }
/// Returns the current diff for this data relative to its original data. The data is pruned
/// implicitly by this call.
const oxenc::bt_dict& diff() override;
/// Prunes empty dicts/sets from data. This is called automatically when serializing or
/// calculating a diff. Returns true if the data was actually changed, false if nothing needed
/// pruning.
bool prune();
/// Calculates the hash of the current message. Can optionally be given the already-serialized
/// value, if available; if empty/omitted, `serialize()` will be called to compute it.
const hash_t& hash() override;
protected:
const hash_t& hash(std::string_view serialized);
void increment_impl();
};
/// Encrypts a config message using XChaCha20-Poly1305, using a blake2b keyed hash of the message
/// for the nonce (rather than pure random) so that different clients will encrypt the same data to
/// the same encrypted value (thus allowing for server-side deduplication of identical messages).
///
/// `key_base` must be 32 bytes. This value is a fixed key that all clients that might receive this
/// message can calculate independently (for instance a value derived from a secret key, or a shared
/// random key). This key will be hashed with the message size and domain suffix (see below) to
/// determine the actual encryption key.
///
/// `domain` is a short string (1-24 chars) used for the keyed hash. Typically this is the type of
/// config, e.g. "closed-group" or "contacts". The full key will be
/// "session-config-encrypted-message-[domain]". This value is also used for the encrypted key (see
/// above).
///
/// The returned result will consist of encrypted data with authentication tag and appended nonce,
/// suitable for being passed to decrypt() to authenticate and decrypt.
///
/// Throw std::invalid_argument on bad input (i.e. from invalid key_base or domain).
ustring encrypt(ustring_view message, ustring_view key_base, std::string_view domain);
/// Same as above but works with strings/string_views instead of ustring/ustring_view
std::string encrypt(std::string_view message, std::string_view key_base, std::string_view domain);
/// Thrown if decrypt() fails.
struct decrypt_error : std::runtime_error {
using std::runtime_error::runtime_error;
};
/// Takes a value produced by `encrypt()` and decrypts it. `key_base` and `domain` must be the same
/// given to encrypt or else decryption fails. Upon decryption failure a std::
ustring decrypt(ustring_view ciphertext, ustring_view key_base, std::string_view domain);
/// Same as above but using std::string/string_view
std::string decrypt(
std::string_view ciphertext, std::string_view key_base, std::string_view domain);
} // namespace session::config
namespace oxenc::detail {
template <>
struct bt_serialize<session::config::dict_value> : bt_serialize<session::config::dict_variant> {};
} // namespace oxenc::detail

View File

@ -0,0 +1,85 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#if defined(_WIN32) || defined(WIN32)
#define LIBSESSION_EXPORT __declspec(dllexport)
#else
#define LIBSESSION_EXPORT __attribute__((visibility("default")))
#endif
#define LIBSESSION_C_API extern "C" LIBSESSION_EXPORT
typedef int64_t seqno_t;
// Config object base type: this type holds the internal object and is initialized by the various
// config-dependent settings (e.g. config_user_profile_init) then passed to the various functions.
typedef struct config_object {
// Internal opaque object pointer; calling code should leave this alone.
void* internals;
// When an error occurs in the C API this string will be set to the specific error message. May
// be NULL.
const char* last_error;
} config_object;
// Common functions callable on any config instance:
/// Frees a config object created with one of the config-dependent ..._init functions (e.g.
/// user_profile_init).
void config_free(config_object* conf);
/// Returns the numeric namespace in which config messages of this type should be stored.
int16_t config_storage_namespace(const config_object* conf);
/// Merges the config object with one or more remotely obtained config strings. After this call the
/// config object may be unchanged, complete replaced, or updated and needing a push, depending on
/// the messages that are merged; the caller should check config_needs_push().
///
/// `configs` is an array of pointers to the start of the strings; `lengths` is an array of string
/// lengths; `count` is the length of those two arrays.
void config_merge(config_object* conf, const char** configs, const size_t* lengths, size_t count);
/// Returns true if this config object contains updated data that has not yet been confirmed stored
/// on the server.
bool config_needs_push(const config_object* conf);
/// Obtains the configuration data that needs to be pushed to the server. A new buffer of the
/// appropriate size is malloc'd and set to `out` The output is written to a new malloc'ed buffer of
/// the appropriate size; the buffer and the output length are set in the `out` and `outlen`
/// parameters. Note that this is binary data, *not* a null-terminated C string.
///
/// Generally this call should be guarded by a call to `config_needs_push`, however it can be used
/// to re-obtain the current serialized config even if no push is needed (for example, if the client
/// wants to re-submit it after a network error).
///
/// NB: The returned buffer belongs to the caller: that is, the caller *MUST* free() it when done
/// with it.
seqno_t config_push(config_object* conf, char** out, size_t* outlen);
/// Reports that data obtained from `config_push` has been successfully stored on the server. The
/// seqno value is the one returned by the config_push call that yielded the config data.
void config_confirm_pushed(config_object* conf, seqno_t seqno);
/// Returns a binary dump of the current state of the config object. This dump can be used to
/// resurrect the object at a later point (e.g. after a restart). Allocates a new buffer and sets
/// it in `out` and the length in `outlen`. Note that this is binary data, *not* a null-terminated
/// C string.
///
/// NB: It is the caller's responsibility to `free()` the buffer when done with it.
///
/// Immediately after this is called `config_needs_dump` will start returning true (until the
/// configuration is next modified).
void config_dump(config_object* conf, char** out, size_t* outlen);
/// Returns true if something has changed since the last call to `dump()` that requires calling
/// and saving the `config_dump()` data again.
bool config_needs_dump(const config_object* conf);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,503 @@
#pragma once
#include <memory>
#include <session/config.hpp>
#include <type_traits>
#include <variant>
#include "base.h"
#include "namespaces.hpp"
namespace session::config {
template <typename T, typename... U>
static constexpr bool is_one_of = (std::is_same_v<T, U> || ...);
/// True for a dict_value direct subtype, but not scalar sub-subtypes.
template <typename T>
static constexpr bool is_dict_subtype = is_one_of<T, config::scalar, config::set, config::dict>;
/// True for a dict_value or any of the types containable within a dict value
template <typename T>
static constexpr bool is_dict_value =
is_dict_subtype<T> || is_one_of<T, dict_value, int64_t, std::string>;
// Levels for the logging callback
enum class LogLevel { debug, info, warning, error };
/// Our current config state
enum class ConfigState : int {
/// Clean means the config is confirmed stored on the server and we haven't changed anything.
Clean = 0,
/// Dirty means we have local changes, and the changes haven't been serialized yet for sending
/// to the server.
Dirty = 1,
/// Waiting is halfway in-between clean and dirty: the caller has serialized the data, but
/// hasn't yet reported back that the data has been stored, *and* we haven't made any changes
/// since the data was serialize.
Waiting = 2,
};
/// Base config type for client-side configs containing common functionality needed by all config
/// sub-types.
class ConfigBase {
private:
// The object (either base config message or MutableConfigMessage) that stores the current
// config message. Subclasses do not directly access this: instead they call `dirty()` if they
// intend to make changes, or the `set_config_field` wrapper.
std::unique_ptr<ConfigMessage> _config;
// Tracks our current state
ConfigState _state = ConfigState::Clean;
protected:
// Constructs an empty base config with no config settings and seqno set to 0.
ConfigBase();
// Constructs a base config by loading the data from a dump as produced by `dump()`.
explicit ConfigBase(std::string_view dump);
// Tracks whether we need to dump again; most mutating methods should set this to true (unless
// calling set_state, which sets to to true implicitly).
bool _needs_dump = false;
// Sets the current state; this also sets _needs_dump to true.
void set_state(ConfigState s) {
_state = s;
_needs_dump = true;
}
// If set then we log things by calling this callback
std::function<void(LogLevel lvl, std::string msg)> logger;
// Invokes the above if set, does nothing if there is no logger.
void log(LogLevel lvl, std::string msg) {
if (logger)
logger(lvl, std::move(msg));
}
// Returns a reference to the current MutableConfigMessage. If the current message is not
// already dirty (i.e. Clean or Waiting) then calling this increments the seqno counter.
MutableConfigMessage& dirty();
// class for proxying subfield access; this class should never be stored but only used
// ephemerally (most of its methods are rvalue-qualified). This lets constructs such as
// foo["abc"]["def"]["ghi"] = 12;
// work, auto-vivifying (or trampling, if not a dict) subdicts to reach the target. It also
// allows non-vivifying value retrieval via .string(), .integer(), etc. methods.
class DictFieldProxy {
private:
ConfigBase& _conf;
std::vector<std::string> _inter_keys;
std::string _last_key;
// See if we can find the key without needing to create anything, so that we can attempt to
// access values without mutating anything (which allows, among other things, for assigning
// of the existing value to not dirty anything). Returns nullptr if the value or something
// along its path would need to be created, or has the wrong type; otherwise a const pointer
// to the value. The templated type, if provided, can be one of the types a dict_value can
// hold to also check that the returned value has a particular type; if omitted you get back
// the dict_value pointer itself.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
const T* get_clean() const {
const config::dict* data = &_conf._config->data();
// All but the last need to be dicts:
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return nullptr;
}
const dict_value* val;
// The last can be any value type:
if (auto it = data->find(_last_key); it != data->end())
val = &it->second;
else
return nullptr;
if constexpr (std::is_same_v<T, dict_value>)
return val;
else if constexpr (is_dict_subtype<T>) {
if (auto* v = std::get_if<T>(val))
return v;
} else { // int64 or std::string, i.e. the config::scalar sub-types.
if (auto* scalar = std::get_if<config::scalar>(val))
return std::get_if<T>(scalar);
}
return nullptr;
}
// Returns a lvalue reference to the value, stomping its way through the dict as it goes to
// create subdicts as needed to reach the target value. If given a template type then we
// also cast the final dict_value variant into the given type (and replace if with a
// default-constructed value if it has the wrong type) then return a reference to that.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
T& get_dirty() {
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto& val = (*data)[key];
data = std::get_if<config::dict>(&val);
if (!data)
data = &val.emplace<config::dict>();
}
auto& val = (*data)[_last_key];
if constexpr (std::is_same_v<T, dict_value>)
return val;
else if constexpr (is_dict_subtype<T>) {
if (auto* v = std::get_if<T>(&val))
return *v;
return val.emplace<T>();
} else { // int64 or std::string, i.e. the config::scalar sub-types.
if (auto* scalar = std::get_if<config::scalar>(&val)) {
if (auto* v = std::get_if<T>(scalar))
return *v;
return scalar->emplace<T>();
}
return val.emplace<scalar>().emplace<T>();
}
}
template <typename T>
void assign_if_changed(T value) {
// Try to avoiding dirtying the config if this assignment isn't changing anything
if (!_conf.is_dirty())
if (auto current = get_clean<T>(); current && *current == value)
return;
get_dirty<T>() = std::move(value);
}
void insert_if_missing(config::scalar&& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && current->count(value))
return;
get_dirty<config::set>().insert(std::move(value));
}
void set_erase_impl(const config::scalar& value) {
if (!_conf.is_dirty())
if (auto current = get_clean<config::set>(); current && !current->count(value))
return;
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return;
}
auto it = data->find(_last_key);
if (it == data->end())
return;
auto& val = it->second;
if (auto* current = std::get_if<config::set>(&val))
current->erase(value);
else
val.emplace<config::set>();
}
public:
DictFieldProxy(ConfigBase& b, std::string key) : _conf{b}, _last_key{std::move(key)} {}
/// Descends into a dict, returning a copied proxy object for the path to the requested
/// field. Nothing is created by doing this unless you actually assign to a value.
DictFieldProxy operator[](std::string subkey) const& {
DictFieldProxy subfield{_conf, std::move(subkey)};
subfield._inter_keys.reserve(_inter_keys.size() + 1);
subfield._inter_keys.insert(
subfield._inter_keys.end(), _inter_keys.begin(), _inter_keys.end());
subfield._inter_keys.push_back(_last_key);
return subfield;
}
// Same as above, but when called on an rvalue reference we just mutate the current proxy to
// the new dict path.
DictFieldProxy&& operator[](std::string subkey) && {
_inter_keys.push_back(std::move(_last_key));
_last_key = std::move(subkey);
return std::move(*this);
}
/// Returns a const pointer to the string if one exists at the given location, nullptr
/// otherwise.
const std::string* string() const { return get_clean<std::string>(); }
/// returns the value as a string_view or a fallback if the value doesn't exist (or isn't a
/// string). The returned view is directly into the value (or fallback) and so mustn't be
/// used beyond the validity of either.
std::string_view string_view_or(std::string_view fallback) const {
if (auto* s = string())
return {*s};
return fallback;
}
/// Returns a copy of the value as a string, if it exists and is a string; returns
/// `fallback` otherwise.
std::string string_or(std::string fallback) const {
if (auto* s = string())
return *s;
return std::move(fallback);
}
/// Returns a const pointer to the integer if one exists at the given location, nullptr
/// otherwise.
const int64_t* integer() const { return get_clean<int64_t>(); }
/// Returns the value as an integer or a fallback if the value doesn't exist (or isn't an
/// integer).
int64_t integer_or(int64_t fallback) const {
if (auto* i = integer())
return *i;
return fallback;
}
/// Returns a const pointer to the set if one exists at the given location, nullptr
/// otherwise.
const config::set* set() const { return get_clean<config::set>(); }
/// Returns a const pointer to the dict if one exists at the given location, nullptr
/// otherwise. (You typically don't need to use this but can rather just use [] to descend
/// into the dict).
const config::dict* dict() const { return get_clean<config::dict>(); }
/// Replaces the current value with the given string. This also auto-vivifies any
/// intermediate dicts needed to reach the given key, including replacing non-dict values if
/// they currently exist along the path.
void operator=(std::string value) { assign_if_changed(std::move(value)); }
/// Same as above, but takes a string_view for convenience.
void operator=(std::string_view value) { *this = std::string{value}; }
/// Replace the current value with the given integer. See above.
void operator=(int64_t value) { assign_if_changed(value); }
/// Replace the current value with the given set. See above.
void operator=(config::set value) { assign_if_changed(std::move(value)); }
/// Replace the current value with the given dict. See above. This often isn't needed
/// because of how other assignment operations work.
void operator=(config::dict value) { assign_if_changed(std::move(value)); }
/// Returns true if there is a value at the current key. If a template type T is given, it
/// only returns true if that value also is a `T`.
template <typename T = dict_value, typename = std::enable_if_t<is_dict_value<T>>>
bool exists() const {
return get_clean<T>() != nullptr;
}
// Alias for `exists<T>()`
template <typename T>
bool is() const {
return exists<T>();
}
/// Removes the value at the current location, regardless of what it currently is. This
/// does nothing if the current location does not have a value.
void erase() {
if (!_conf.is_dirty() && !get_clean())
return;
config::dict* data = &_conf.dirty().data();
for (const auto& key : _inter_keys) {
auto it = data->find(key);
data = it != data->end() ? std::get_if<config::dict>(&it->second) : nullptr;
if (!data)
return;
}
data->erase(_last_key);
}
/// Adds a value to the set at the current location. If the current value is not a set or
/// does not exist then dicts will be created to reach it and a new set will be created.
void set_insert(std::string_view value) {
insert_if_missing(config::scalar{std::string{value}});
}
void set_insert(int64_t value) { insert_if_missing(config::scalar{value}); }
/// Removes a value from the set at the current location. If the current value does not
/// exist then nothing happens. If it does exist, but is not a set, it will be replaced
/// with an empty set. Otherwise the given value will be removed from the set, if present.
void set_erase(std::string_view value) {
set_erase_impl(config::scalar{std::string{value}});
}
void set_erase(int64_t value) { set_erase_impl(scalar{value}); }
/// Emplaces a value at the current location. As with assignment, this creates dicts as
/// needed along the keys to reach the target. The existing value (if present) is destroyed
/// to make room for the new one.
template <
typename T,
typename... Args,
typename = std::enable_if_t<
is_one_of<T, config::set, config::dict, int64_t, std::string>>>
T& emplace(Args&&... args) {
if constexpr (is_one_of<T, int64_t, std::string>)
return get_dirty<scalar>().emplace<T>(std::forward<Args>(args)...);
return get_dirty().emplace<T>(std::forward<Args>(args)...);
}
};
/// Wrapper for the ConfigBase's root `data` field to provide data access. Only provides a []
/// that gets you into a DictFieldProxy.
class DictFieldRoot {
ConfigBase& _conf;
DictFieldRoot(DictFieldRoot&&) = delete;
DictFieldRoot(const DictFieldRoot&) = delete;
DictFieldRoot& operator=(DictFieldRoot&&) = delete;
DictFieldRoot& operator=(const DictFieldRoot&) = delete;
public:
DictFieldRoot(ConfigBase& b) : _conf{b} {}
/// Access a dict element. This returns a proxy object for accessing the value, but does
/// *not* auto-vivify the path (unless/until you assign to it).
DictFieldProxy operator[](std::string key) const& {
return DictFieldProxy{_conf, std::move(key)};
}
};
// Called when dumping to obtain any extra data that a subclass needs to store to reconstitute
// the object. The base implementation does nothing. The counterpart to this,
// `load_extra_data()`, is called when loading from a dump that has extra data; a subclass
// should either override both (if it needs to serialize extra data) or neither (if it needs no
// extra data). Internally this extra data (if non-empty) is stored in the "+" key of the dump.
virtual oxenc::bt_dict extra_data() const { return {}; }
// Called when constructing from a dump that has extra data. The base implementation does
// nothing.
virtual void load_extra_data(oxenc::bt_dict extra) {}
public:
virtual ~ConfigBase() = default;
// Proxy class providing read and write access to the contained config data.
const DictFieldRoot data{*this};
// Accesses the storage namespace where this config type is to be stored/loaded from. See
// namespaces.hpp for the underlying integer values.
virtual Namespace storage_namespace() const = 0;
// How many config lags should be used for this object; default to 5. Implementing subclasses
// can override to return a different constant if desired. More lags require more "diff"
// storage in the config messages, but also allow for a higher tolerance of simultaneous message
// conflicts.
virtual int config_lags() const { return 5; }
// This takes all of the messages pulled down from the server and does whatever is necessary to
// merge (or replace) the current values.
//
// After this call the caller should check `needs_push()` to see if the data on hand was updated
// and needs to be pushed to the server again.
//
// Will throw on serious error (i.e. if neither the current nor any of the given configs are
// parseable).
virtual void merge(const std::vector<std::string_view>& configs);
// Returns true if we are currently dirty (i.e. have made changes that haven't been serialized
// yet).
bool is_dirty() const { return _state == ConfigState::Dirty; }
// Returns true if we are curently clean (i.e. our current config is stored on the server and
// unmodified).
bool is_clean() const { return _state == ConfigState::Clean; }
// Returns true if this object contains updated data that has not yet been confirmed stored on
// the server. This will be true whenever `is_clean()` is false: that is, if we are currently
// "dirty" (i.e. have changes that haven't been pushed) or are still awaiting confirmation of
// storage of the most recent serialized push data.
virtual bool needs_push() const;
// Returns the data to push to the server along with the seqno value of the data. If the config
// is currently dirty (i.e. has previously unsent modifications) then this marks it as
// awaiting-confirmation instead of dirty so that any future change immediately increments the
// seqno.
virtual std::pair<std::string, seqno_t> push();
// Should be called after the push is confirmed stored on the storage server swarm to let the
// object know the data is stored. (Once this is called `needs_push` will start returning false
// until something changes). Takes the seqno that was pushed so that the object can ensure that
// the latest version was pushed (i.e. in case there have been other changes since the `push()`
// call that returned this seqno).
//
// It is safe to call this multiple times with the same seqno value, and with out-of-order
// seqnos (e.g. calling with seqno 122 after having called with 123; the duplicates and earlier
// ones will just be ignored).
virtual void confirm_pushed(seqno_t seqno);
// Returns a dump of the current state for storage in the database; this value would get passed
// into the constructor to reconstitute the object (including the push/not pushed status). This
// method is *not* virtual: if subclasses need to store extra data they should set it in the
// `subclass_data` field.
std::string dump();
// Returns true if something has changed since the last call to `dump()` that requires calling
// and saving the `dump()` data again.
virtual bool needs_dump() const { return _needs_dump; }
};
// The C++ struct we hold opaquely inside the C internals struct. This is designed so that any
// internals<T> has the same layout so that it doesn't matter whether we unbox to an
// internals<ConfigBase> or internals<SubType>.
template <
typename ConfigT = ConfigBase,
std::enable_if_t<std::is_base_of_v<ConfigBase, ConfigT>, int> = 0>
struct internals final {
std::unique_ptr<ConfigBase> config;
std::string error;
// Dereferencing falls through to the ConfigBase object
ConfigT* operator->() {
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
return config.get();
else {
auto* c = dynamic_cast<ConfigT*>(config.get());
assert(c);
return c;
}
}
const ConfigT* operator->() const {
if constexpr (std::is_same_v<ConfigT, ConfigBase>)
return config.get();
else {
auto* c = dynamic_cast<ConfigT*>(config.get());
assert(c);
return c;
}
}
ConfigT& operator*() { return *operator->(); }
const ConfigT& operator*() const { return *operator->(); }
};
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
inline internals<T>& unbox(config_object* conf) {
return *static_cast<internals<T>*>(conf->internals);
}
template <typename T = ConfigBase, std::enable_if_t<std::is_base_of_v<ConfigBase, T>, int> = 0>
inline const internals<T>& unbox(const config_object* conf) {
return *static_cast<const internals<T>*>(conf->internals);
}
// Sets an error message in the internals.error string and updates the last_error pointer in the
// outer (C) config_object struct to point at it.
void set_error(config_object* conf, std::string e);
// Same as above, but gets the error string out of an exception and passed through a return value.
// Intended to simplify catch-and-return-error such as:
// try {
// whatever();
// } catch (const std::exception& e) {
// return set_error(conf, LIB_SESSION_ERR_OHNOES, e);
// }
inline int set_error(config_object* conf, int errcode, const std::exception& e) {
set_error(conf, e.what());
return errcode;
}
// Copies a value contained in a string into a new malloced char buffer, returning the buffer and
// size via the two pointer arguments.
void copy_out(const std::string& data, char** out, size_t* outlen);
} // namespace session::config

View File

@ -0,0 +1,23 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
enum config_error {
/// Value returned for no error
SESSION_ERR_NONE = 0,
/// Error indicating that initialization failed because the dumped data being loaded is invalid.
SESSION_ERR_INVALID_DUMP = 1,
/// Error indicated a bad value, e.g. if trying to set something invalid in a config field.
SESSION_ERR_BAD_VALUE = 2,
};
// Returns a generic string for a given integer error code as returned by some functions. Depending
// on the call, a more details error string may be available in the config_object's `last_error`
// field.
const char* config_errstr(int err);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,12 @@
#pragma once
#include <cstdint>
namespace session::config {
enum class Namespace : std::int16_t {
UserProfile = 2,
ClosedGroupInfo = 11,
};
} // namespace session::config

View File

@ -0,0 +1,51 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
#include "base.h"
/// Constructs a user profile config object and sets a pointer to it in `conf`. To restore an
/// existing dump produced by a past instantiation's call to `dump()` pass the dump value via `dump`
/// and `dumplen`; to construct a new, empty profile pass NULL and 0.
///
/// `error` must either be NULL or a pointer to a buffer of at least 256 bytes.
///
/// Returns 0 on success; returns a non-zero error code and sets error (if not NULL) to the
/// exception message on failure.
///
/// When done with the object the `config_object` must be destroyed by passing the pointer to
/// config_free() (in `session/config/base.h`).
int user_profile_init(config_object** conf, const char* dump, size_t dumplen, char* error)
__attribute__((warn_unused_result));
/// Returns a pointer to the currently-set name (null-terminated), or NULL if there is no name at
/// all. Should be copied right away as the pointer may not remain valid beyond other API calls.
const char* user_profile_get_name(const config_object* conf);
/// Sets the user profile name to the null-terminated C string. Returns 0 on success, non-zero on
/// error (and sets the config_object's error string).
int user_profile_set_name(config_object* conf, const char* name);
typedef struct user_profile_pic {
// Null-terminated C string containing the uploaded URL of the pic. Will be NULL if there is no
// profile pic.
const char* url;
// The profile pic decryption key, in bytes. This is a byte buffer of length `keylen`, *not* a
// null-terminated C string. Will be NULL if there is no profile pic.
const char* key;
size_t keylen;
} user_profile_pic;
// Obtains the current profile pic. The pointers in the returned struct will be NULL if a profile
// pic is not currently set, and otherwise should be copied right away (they will not be valid
// beyond other API calls on this config object).
user_profile_pic user_profile_get_pic(const config_object* conf);
// Sets a user profile
int user_profile_set_pic(config_object* conf, user_profile_pic pic);
#ifdef __cplusplus
} // extern "C"
#endif

View File

@ -0,0 +1,43 @@
#pragma once
#include <memory>
#include <session/config.hpp>
#include "base.hpp"
#include "namespaces.hpp"
namespace session::config {
/// keys used in this config, either currently or in the past (so that we don't reuse):
///
/// n - user profile name
/// p - user profile url
/// q - user profile decryption key (binary)
class UserProfile final : public ConfigBase {
public:
/// Constructs a new, blank user profile.
UserProfile() = default;
/// Constructs a user profile from existing data
explicit UserProfile(std::string_view dumped) : ConfigBase{dumped} {}
Namespace storage_namespace() const override { return Namespace::UserProfile; }
/// Returns the user profile name, or nullptr if there is no profile name set.
const std::string* get_name() const;
/// Sets the user profile name
void set_name(std::string_view new_name);
/// Gets the user's current profile pic URL and decryption key. Returns nullptr for *both*
/// values if *either* value is unset or empty in the config data.
std::pair<const std::string*, const std::string*> get_profile_pic() const;
/// Sets the user's current profile pic to a new URL and decryption key. Clears both if either
/// one is empty.
void set_profile_pic(std::string url, std::string key);
};
} // namespace session::config

View File

@ -0,0 +1,43 @@
#pragma once
#include <array>
#include <chrono>
#include <cstdint>
#include <string>
namespace session {
using namespace std::literals;
/// An uploaded file is its URL + decryption key
struct Uploaded {
std::string url;
std::string key;
};
/// A conversation disappearing messages setting
struct Disappearing {
/// The possible modes of a disappearing messages setting.
enum class Mode : int { None = 0, AfterSend = 1, AfterRead = 2 };
/// The mode itself
Mode mode = Mode::None;
/// The timer value; this is only used when mode is not None.
std::chrono::seconds timer = 0s;
};
/// A Session ID: an x25519 pubkey, with a 05 identifying prefix. On the wire we send just the
/// 32-byte pubkey value (i.e. not hex, without the prefix).
struct SessionID {
/// The fixed session netid, 0x05
static constexpr unsigned char netid = 0x05;
/// The raw x25519 pubkey, as bytes
std::array<unsigned char, 32> pubkey;
/// Returns the full pubkey in hex, including the netid prefix.
std::string hex() const;
};
} // namespace session

View File

@ -0,0 +1,34 @@
#pragma once
#ifdef __cplusplus
extern "C" {
#endif
/// XEd25519-signed a message given a curve25519 privkey and message. Writes the 64-byte signature
/// to `sig` on success and returns 0. Returns non-zero on failure.
__attribute__((warn_unused_result)) int session_xed25519_sign(
unsigned char* signature /* 64 byte buffer */,
const unsigned char* curve25519_privkey /* 32 bytes */,
const unsigned char* msg,
const unsigned int msg_len);
/// Verifies an XEd25519-signed message given a 64-byte signature, 32-byte curve25519 pubkey, and
/// message. Returns 0 if the signature verifies successfully, non-zero on failure.
__attribute__((warn_unused_result)) int session_xed25519_verify(
const unsigned char* signature /* 64 bytes */,
const unsigned char* pubkey /* 32-bytes */,
const unsigned char* msg,
const unsigned int msg_len);
/// Given a curve25519 pubkey, this writes the associated XEd25519-derived Ed25519 pubkey into
/// ed25519_pubkey. Note, however, that there are *two* possible Ed25519 pubkeys that could result
/// in a given curve25519 pubkey: this always returns the positive value. You can get the other
/// possibility (the negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
/// Returns 0 on success, non-0 on failure.
__attribute__((warn_unused_result)) int session_xed25519_pubkey(
unsigned char* ed25519_pubkey /* 32-byte output buffer */,
const unsigned char* curve25519_pubkey /* 32 bytes */);
#ifdef __cplusplus
}
#endif

View File

@ -0,0 +1,38 @@
#pragma once
#include <array>
#include <string>
#include <string_view>
namespace session::xed25519 {
using ustring_view = std::basic_string_view<unsigned char>;
/// XEd25519-signs a message given the curve25519 privkey and message.
std::array<unsigned char, 64> sign(
ustring_view curve25519_privkey /* 32 bytes */, ustring_view msg);
/// "Softer" version that takes and returns strings of regular chars
std::string sign(std::string_view curve25519_privkey /* 32 bytes */, std::string_view msg);
/// Verifies a curve25519 message allegedly signed by the given curve25519 pubkey
[[nodiscard]] bool verify(
ustring_view signature /* 64 bytes */,
ustring_view curve25519_pubkey /* 32 bytes */,
ustring_view msg);
/// "Softer" version that takes strings of regular chars
[[nodiscard]] bool verify(
std::string_view signature /* 64 bytes */,
std::string_view curve25519_pubkey /* 32 bytes */,
std::string_view msg);
/// Given a curve25519 pubkey, this returns the associated XEd25519-derived Ed25519 pubkey. Note,
/// however, that there are *two* possible Ed25519 pubkeys that could result in a given curve25519
/// pubkey: this always returns the positive value. You can get the other possibility (the
/// negative) by flipping the sign bit, i.e. `returned_pubkey[31] |= 0x80`.
std::array<unsigned char, 32> pubkey(ustring_view curve25519_pubkey);
/// "Softer" version that takes/returns strings of regular chars
std::string pubkey(std::string_view curve25519_pubkey);
} // namespace session::xed25519

View File

@ -0,0 +1,121 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionUtilitiesKit
internal extension OpenGroupAPI {
struct BatchRequest: Encodable {
let requests: [Child]
init(requests: [Info]) {
self.requests = requests.map { $0.child }
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(requests)
}
// MARK: - BatchRequest.Info
struct Info {
public let endpoint: any EndpointType
public let responseType: Codable.Type
fileprivate let child: Child
public init<T: Encodable, E: EndpointType, R: Codable>(request: Request<T, E>, responseType: R.Type) {
self.endpoint = request.endpoint
self.responseType = HTTP.BatchSubResponse<R>.self
self.child = Child(request: request)
}
public init<T: Encodable, E: EndpointType>(request: Request<T, E>) {
self.init(
request: request,
responseType: NoResponse.self
)
}
}
// MARK: - BatchRequest.Child
struct Child: Encodable {
enum CodingKeys: String, CodingKey {
case method
case path
case headers
case json
case b64
case bytes
}
let method: HTTPMethod
let path: String
let headers: [String: String]?
/// The `jsonBodyEncoder` is used to avoid having to make `Child` a generic type (haven't found a good way
/// to keep `Child` encodable using protocols unfortunately so need this work around)
private let jsonBodyEncoder: ((inout KeyedEncodingContainer<CodingKeys>, CodingKeys) throws -> ())?
private let b64: String?
private let bytes: [UInt8]?
internal init<T: Encodable, E: EndpointType>(request: Request<T, E>) {
self.method = request.method
self.path = request.urlPathAndParamsString
self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders())
// 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
}
}
func encode(to encoder: Encoder) throws {
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
try container.encode(method, forKey: .method)
try container.encode(path, forKey: .path)
try container.encodeIfPresent(headers, forKey: .headers)
try jsonBodyEncoder?(&container, .json)
try container.encodeIfPresent(b64, forKey: .b64)
try container.encodeIfPresent(bytes, forKey: .bytes)
}
}
}
}
// MARK: - Convenience
internal extension Promise where T == HTTP.BatchResponse {
func map<E: EndpointType>(
requests: [OpenGroupAPI.BatchRequest.Info],
toHashMapFor endpointType: E.Type
) -> Promise<[E: (ResponseInfoType, Codable?)]> {
return self.map { result in
result.enumerated()
.reduce(into: [:]) { prev, next in
guard let endpoint: E = requests[next.offset].endpoint as? E else { return }
prev[endpoint] = next.element
}
}
}
}

View File

@ -0,0 +1,327 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionUtil
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: QuickSpec {
// MARK: - Spec
override func spec() {
it("behaves correctly") {
// 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, 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 = user_profile_get_name(conf)
expect(namePtr).to(beNil())
var toPush: UnsafeMutablePointer<CChar>? = nil
var toPushLen: Int = 0
// We don't need to push since we haven't changed anything, so this call is mainly just for
// testing:
let seqno: Int64 = config_push(conf, &toPush, &toPushLen)
expect(toPush).toNot(beNil())
expect(seqno).to(equal(0))
expect(String(cString: toPush!)).to(equal("d1:#i0e1:&de1:<le1:=dee"))
// This should also be unset:
let pic = user_profile_get_pic(conf)
expect(pic.url).to(beNil())
expect(pic.key).to(beNil())
expect(pic.keylen).to(equal(0))
// Now let's go set a profile name and picture:
expect(user_profile_set_name(conf, "Kallie")).to(equal(0))
let profileUrl: [CChar] = "http://example.org/omg-pic-123.bmp"
.bytes
.map { CChar(bitPattern: $0) }
let profileKey: [CChar] = "secretNOTSECRET"
.bytes
.map { CChar(bitPattern: $0) }
let p: user_profile_pic = profileUrl.withUnsafeBufferPointer { profileUrlPtr in
profileKey.withUnsafeBufferPointer { profileKeyPtr in
user_profile_pic(
url: profileUrlPtr.baseAddress,
key: profileKeyPtr.baseAddress,
keylen: 6
)
}
}
expect(user_profile_set_pic(conf, p)).to(equal(0))
// Retrieve them just to make sure they set properly:
let namePtr2 = user_profile_get_name(conf)
expect(namePtr2).toNot(beNil())
expect(String(cString: namePtr2!)).to(equal("Kallie"))
let pic2 = user_profile_get_pic(conf);
expect(pic2.url).toNot(beNil())
expect(pic2.key).toNot(beNil())
expect(pic2.keylen).to(equal(6))
expect(String(cString: pic2.url!)).to(equal("http://example.org/omg-pic-123.bmp"))
expect(String(cString: pic2.key!)).to(equal("secret"))
// 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())
var toPush2: UnsafeMutablePointer<CChar>? = nil
var toPush2Len: Int = 0
let seqno2 = config_push(conf, &toPush2, &toPush2Len);
// incremented since we made changes (this only increments once between
// dumps; even though we changed two fields here).
expect(seqno2).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: [CChar] = Data(hex: "ea173b57beca8af18c3519a7bbf69c3e7a05d1c049fa9558341d8ebb48b0c965")
.bytes
.map { CChar(bitPattern: $0) }
// .withUnsafeBufferPointer { profileKeyPtr in
// String(cString: profileKeyPtr.baseAddress!)
// }
// The data to be actually pushed, expanded like this to make it somewhat human-readable:
let expPush1: [CChar] = ["""
d
1:#i1e
1:& d
1:n 6:Kallie
1:p 34:http://example.org/omg-pic-123.bmp
1:q 6:secret
e
1:< l
l i0e 32:
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
.map { CChar(bitPattern: $0) },
expHash0,
"""
de e
e
1:= d
1:n 0:
1:p 0:
1:q 0:
e
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines) // For readability
.bytes
.map { CChar(bitPattern: $0) },
// [CChar()] // Need to null-terminate the string or it'll crash
]
.flatMap { $0 }
expect(String(cString: toPush2!))
// Need to null-terminate the string or it'll crash
.to(equal(String(cString: expPush1.appending(CChar()))))
// 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<CChar>? = 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:$ \(expPush1.count):
"""
.removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) },
expPush1,
"""
e
""".removeCharacters(characterSet: CharacterSet.whitespacesAndNewlines)
.bytes
.map { CChar(bitPattern: $0) }
]
.flatMap { $0 }
expect(String(cString: dump1!))
.to(equal(String(cString: expDump1.appending(CChar()))))// Need to null-terminate the string or it'll crash
// So now imagine we got back confirmation from the swarm that the push has been stored:
config_confirm_pushed(conf, seqno2)
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<CChar>? = nil
var dump2Len: Int = 0
config_dump(conf, &dump2, &dump2Len)
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, nil, 0, error2)).to(equal(0))
expect(config_needs_dump(conf2)).to(beFalse())
// Now imagine we just pulled down the `exp_push1` string from the swarm; we merge it into
// conf2:
let mergeData: [[CChar]] = [expPush1]
var mergeDataPtr = mergeData.map { value in
let cStringCopy = UnsafeMutableBufferPointer<CChar>.allocate(capacity: value.count)
_ = cStringCopy.initialize(from: value)
let ptr = UnsafePointer(cStringCopy.baseAddress)
return UnsafePointer(cStringCopy.baseAddress)
}
var mergeSize: [Int] = [expPush1.count]
config_merge(conf2, &mergeDataPtr, &mergeSize, 1)
// Our state has changed, so we need to dump:
expect(config_needs_dump(conf2)).to(beTrue())
var dump3: UnsafeMutablePointer<CChar>? = nil
var dump3Len: Int = 0
config_dump(conf, &dump3, &dump3Len)
// (store in db)
expect(config_needs_dump(conf2)).to(beFalse())// TODO: This one is broken now!!!
// 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 profile2Url: [CChar] = "http://new.example.com/pic"
.bytes
.map { CChar(bitPattern: $0) }
let profile2Key: [CChar] = "qwert\0yuio"
.bytes
.map { CChar(bitPattern: $0) }
let p2: user_profile_pic = profile2Url.withUnsafeBufferPointer { profile2UrlPtr in
profile2Key.withUnsafeBufferPointer { profile2KeyPtr in
user_profile_pic(
url: profile2UrlPtr.baseAddress,
key: profile2KeyPtr.baseAddress,
keylen: 10
)
}
}
user_profile_set_pic(conf2, p2)
// Both have changes, so push need a push
expect(config_needs_push(conf)).to(beTrue())
expect(config_needs_push(conf2)).to(beTrue())
var toPush3: UnsafeMutablePointer<CChar>? = nil
var toPush3Len: Int = 0
let seqno3 = config_push(conf, &toPush3, &toPush3Len)
expect(seqno3).to(equal(2)) // incremented, since we made a field change
var toPush4: UnsafeMutablePointer<CChar>? = nil
var toPush4Len: Int = 0
let seqno4 = config_push(conf2, &toPush4, &toPush4Len)
expect(seqno4).to(equal(2)) // incremented, since we made a field change
var dump4: UnsafeMutablePointer<CChar>? = nil
var dump4Len: Int = 0
config_dump(conf, &dump4, &dump4Len);
var dump5: UnsafeMutablePointer<CChar>? = nil
var dump5Len: Int = 0
config_dump(conf2, &dump5, &dump5Len);
// (store in db)
// Since we set different things, we're going to get back different serialized data to be
// pushed:
expect(String(cString: toPush3!)).toNot(equal(String(cString: toPush4!)))
// 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).
let mergeData2: [String] = [String(cString: toPush3!)]
var mergeData2Ptr = mergeData2.map { value in
value.bytes.map { CChar(bitPattern: $0) }.withUnsafeBufferPointer { valuePtr in
valuePtr.baseAddress
}
}
var mergeSize2: [Int] = [toPush3Len]
config_merge(conf2, &mergeData2Ptr, &mergeSize2, 1)
let mergeData3: [String] = [String(cString: toPush4!)]
var mergeData3Ptr = mergeData3.map { value in
value.bytes.map { CChar(bitPattern: $0) }.withUnsafeBufferPointer { valuePtr in
valuePtr.baseAddress
}
}
var mergeSize3: [Int] = [toPush4Len]
config_merge(conf, &mergeData3Ptr, &mergeSize3, 1)
// 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 seqno5 = config_push(conf, &toPush3, &toPush3Len);
let seqno6 = config_push(conf2, &toPush4, &toPush4Len);
expect(seqno5).to(equal(3))
expect(seqno6).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_get_pic(conf)
expect(pic3.url).toNot(beNil())
expect(String(cString: pic3.url!)).to(equal("http://new.example.com/pic"))
expect(pic3.key).toNot(beNil())
expect(String(cString: pic3.key!)).to(equal("qwert\0yuio"))
let pic4 = user_profile_get_pic(conf2)
expect(pic4.url).toNot(beNil())
expect(String(cString: pic4.url!)).to(equal("http://new.example.com/pic"))
expect(pic4.key).toNot(beNil())
expect(String(cString: pic4.key!)).to(equal("qwert\0yuio"))
config_confirm_pushed(conf, seqno5)
config_confirm_pushed(conf2, seqno6)
var dump6: UnsafeMutablePointer<CChar>? = nil
var dump6Len: Int = 0
config_dump(conf, &dump6, &dump6Len);
var dump7: UnsafeMutablePointer<CChar>? = nil
var dump7Len: Int = 0
config_dump(conf2, &dump7, &dump7Len);
// (store in db)
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())
}
}
}

View File

@ -21,12 +21,12 @@ class BatchRequestInfoSpec: QuickSpec {
override func spec() {
// MARK: - BatchSubRequest
describe("a BatchSubRequest") {
var subRequest: OpenGroupAPI.BatchSubRequest!
describe("a BatchRequest.Child") {
var subRequest: OpenGroupAPI.BatchRequest.Child!
context("when initializing") {
it("sets the headers to nil if there aren't any") {
subRequest = OpenGroupAPI.BatchSubRequest(
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
server: "testServer",
endpoint: .batch
@ -37,7 +37,7 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("converts the headers to HTTP headers") {
subRequest = OpenGroupAPI.BatchSubRequest(
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<NoBody, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
@ -54,7 +54,7 @@ class BatchRequestInfoSpec: QuickSpec {
context("when encoding") {
it("successfully encodes a string body") {
subRequest = OpenGroupAPI.BatchSubRequest(
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<String, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
@ -72,7 +72,7 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("successfully encodes a byte body") {
subRequest = OpenGroupAPI.BatchSubRequest(
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<[UInt8], OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
@ -90,7 +90,7 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("successfully encodes a JSON body") {
subRequest = OpenGroupAPI.BatchSubRequest(
subRequest = OpenGroupAPI.BatchRequest.Child(
request: Request<TestType, OpenGroupAPI.Endpoint>(
method: .get,
server: "testServer",
@ -109,112 +109,9 @@ class BatchRequestInfoSpec: QuickSpec {
}
}
// MARK: - BatchSubResponse<T>
describe("a BatchSubResponse<T>") {
context("when decoding") {
it("decodes correctly") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": {
"stringValue": "testValue"
}
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).toNot(beNil())
}
it("decodes with invalid body data") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
}
it("flags invalid body data as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beTrue())
}
it("does not flag a missing or invalid optional body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<TestType?>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<TestType?>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
it("does not flag a NoResponse body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: OpenGroupAPI.BatchSubResponse<NoResponse>? = try? JSONDecoder().decode(
OpenGroupAPI.BatchSubResponse<NoResponse>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
}
}
// MARK: - BatchRequestInfo<T, R>
describe("a BatchRequestInfo<T, R>") {
describe("a BatchRequest.Info") {
var request: Request<TestType, OpenGroupAPI.Endpoint>!
beforeEach {
@ -229,26 +126,26 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("initializes correctly when given a request") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
request: request
)
expect(requestInfo.request).to(equal(request))
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<NoResponse>.self).to(beTrue())
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
expect(requestInfo.responseType == HTTP.BatchSubResponse<NoResponse>.self).to(beTrue())
}
it("initializes correctly when given a request and a response type") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
request: request,
responseType: TestType.self
)
expect(requestInfo.request).to(equal(request))
expect(requestInfo.responseType == OpenGroupAPI.BatchSubResponse<TestType>.self).to(beTrue())
expect(requestInfo.endpoint.path).to(equal(request.endpoint.path))
expect(requestInfo.responseType == HTTP.BatchSubResponse<TestType>.self).to(beTrue())
}
it("exposes the endpoint correctly") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
let requestInfo: OpenGroupAPI.BatchRequest.Info = OpenGroupAPI.BatchRequest.Info(
request: request
)
@ -256,130 +153,17 @@ class BatchRequestInfoSpec: QuickSpec {
}
it("generates a sub request correctly") {
let requestInfo: OpenGroupAPI.BatchRequestInfo<TestType> = OpenGroupAPI.BatchRequestInfo(
request: request
let batchRequest: OpenGroupAPI.BatchRequest = OpenGroupAPI.BatchRequest(
requests: [
OpenGroupAPI.BatchRequest.Info(
request: request
)
]
)
let subRequest: OpenGroupAPI.BatchSubRequest = requestInfo.toSubRequest()
expect(subRequest.method).to(equal(request.method))
expect(subRequest.path).to(equal(request.urlPathAndParamsString))
expect(subRequest.headers).to(beNil())
}
}
// MARK: - Convenience
// MARK: --Decodable
describe("a Decodable") {
it("decodes correctly") {
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
let result: TestType? = try? TestType.decoded(from: jsonData)
expect(result).to(equal(TestType(stringValue: "testValue")))
}
}
// MARK: - --Promise
describe("an (OnionRequestResponseInfoType, Data?) Promise") {
var responseInfo: OnionRequestResponseInfoType!
var capabilities: OpenGroupAPI.Capabilities!
var pinnedMessage: OpenGroupAPI.PinnedMessage!
var data: Data!
beforeEach {
responseInfo = OnionRequestAPI.ResponseInfo(code: 200, headers: [:])
capabilities = OpenGroupAPI.Capabilities(capabilities: [], missing: nil)
pinnedMessage = OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 123, pinnedBy: "test")
data = """
[\([
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: capabilities,
failedToParseBody: false
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: pinnedMessage,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ","))]
""".data(using: .utf8)!
}
it("decodes valid data correctly") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.value).toNot(beNil())
expect((result.value?[0].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>)?.body)
.to(equal(capabilities))
expect((result.value?[1].1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>)?.body)
.to(equal(pinnedMessage))
}
it("fails if there is no data") {
let result = Promise.value((responseInfo, nil)).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the data is not JSON") {
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the data is not a JSON array") {
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if the JSON array does not have the same number of items as the expected types") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
}
it("fails if one of the JSON array values fails to decode") {
data = """
[\([
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
code: 200,
headers: [:],
body: capabilities,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ",")),{"test": "test"}]
""".data(using: .utf8)!
let result = Promise.value((responseInfo, data))
.decoded(as: [
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>.self,
OpenGroupAPI.BatchSubResponse<OpenGroupAPI.PinnedMessage>.self
])
expect(result.error?.localizedDescription).to(equal(HTTP.Error.parsingFailed.localizedDescription))
expect(batchRequest.requests[0].method).to(equal(request.method))
expect(batchRequest.requests[0].path).to(equal(request.urlPathAndParamsString))
expect(batchRequest.requests[0].headers).to(beNil())
}
}
}

View File

@ -106,7 +106,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
it("errors if the data is not a base64 encoded string") {
@ -128,7 +128,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
it("errors if the signature is not a base64 encoded string") {
@ -150,7 +150,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
it("errors if the dependencies are not provided to the JSONDecoder") {
@ -159,7 +159,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
it("errors if the session_id value is not valid") {
@ -181,7 +181,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
@ -239,7 +239,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
}
@ -274,7 +274,7 @@ class SOGSMessageSpec: QuickSpec {
expect {
try decoder.decode(OpenGroupAPI.Message.self, from: messageData)
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
}
}

View File

@ -25,8 +25,8 @@ class OpenGroupAPISpec: QuickSpec {
var mockNonce24Generator: MockNonce24Generator!
var dependencies: SMKDependencies!
var response: (OnionRequestResponseInfoType, Codable)? = nil
var pollResponse: [OpenGroupAPI.Endpoint: (OnionRequestResponseInfoType, Codable?)]?
var response: (ResponseInfoType, Codable)? = nil
var pollResponse: [OpenGroupAPI.Endpoint: (ResponseInfoType, Codable?)]?
var error: Error?
describe("an OpenGroupAPI") {
@ -136,7 +136,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
@ -144,7 +144,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: try! JSONDecoder().decode(
@ -163,7 +163,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: [OpenGroupAPI.Message](),
@ -212,7 +212,6 @@ class OpenGroupAPISpec: QuickSpec {
let requestData: TestOnionRequestAPI.RequestData? = (pollResponse?[.capabilities]?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.urlString).to(equal("testserver/batch"))
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
}
@ -369,7 +368,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
@ -377,7 +376,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: try! JSONDecoder().decode(
@ -396,7 +395,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: [OpenGroupAPI.Message](),
@ -404,7 +403,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: [OpenGroupAPI.DirectMessage](),
@ -412,7 +411,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: [OpenGroupAPI.DirectMessage](),
@ -575,7 +574,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
@ -583,7 +582,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
@ -591,7 +590,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
@ -626,9 +625,9 @@ class OpenGroupAPISpec: QuickSpec {
)
expect(error?.localizedDescription).to(beNil())
let capabilitiesResponse: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>? = (pollResponse?[.capabilities]?.1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.Capabilities>)
let pollInfoResponse: OpenGroupAPI.BatchSubResponse<OpenGroupAPI.RoomPollInfo>? = (pollResponse?[.roomPollInfo("testRoom", 0)]?.1 as? OpenGroupAPI.BatchSubResponse<OpenGroupAPI.RoomPollInfo>)
let messagesResponse: OpenGroupAPI.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>? = (pollResponse?[.roomMessagesRecent("testRoom")]?.1 as? OpenGroupAPI.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>)
let capabilitiesResponse: HTTP.BatchSubResponse<OpenGroupAPI.Capabilities>? = (pollResponse?[.capabilities]?.1 as? HTTP.BatchSubResponse<OpenGroupAPI.Capabilities>)
let pollInfoResponse: HTTP.BatchSubResponse<OpenGroupAPI.RoomPollInfo>? = (pollResponse?[.roomPollInfo("testRoom", 0)]?.1 as? HTTP.BatchSubResponse<OpenGroupAPI.RoomPollInfo>)
let messagesResponse: HTTP.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>? = (pollResponse?[.roomMessagesRecent("testRoom")]?.1 as? HTTP.BatchSubResponse<[Failable<OpenGroupAPI.Message>]>)
expect(capabilitiesResponse?.failedToParseBody).to(beFalse())
expect(pollInfoResponse?.failedToParseBody).to(beTrue())
expect(messagesResponse?.failedToParseBody).to(beTrue())
@ -651,7 +650,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -680,7 +679,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -709,7 +708,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -738,7 +737,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -750,7 +749,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.Capabilities(capabilities: [], missing: nil),
@ -758,7 +757,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: try! JSONDecoder().decode(
@ -799,7 +798,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -819,7 +818,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities)?
mockStorage
.read { db in
@ -846,7 +845,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/capabilities"))
}
}
@ -890,7 +888,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (info: OnionRequestResponseInfoType, data: [OpenGroupAPI.Room])?
var response: (info: ResponseInfoType, data: [OpenGroupAPI.Room])?
mockStorage
.read { db in
@ -917,7 +915,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/rooms"))
}
}
@ -960,7 +957,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -968,7 +965,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomData,
@ -982,7 +979,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
mockStorage
.read { db in
@ -1011,7 +1008,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.capabilities.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/sequence"))
}
}
@ -1024,7 +1020,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -1038,7 +1034,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
mockStorage
.read { db in
@ -1056,7 +1052,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -1096,7 +1092,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomData,
@ -1110,7 +1106,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
mockStorage
.read { db in
@ -1128,7 +1124,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -1169,7 +1165,7 @@ class OpenGroupAPISpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -1177,7 +1173,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomData,
@ -1185,7 +1181,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: OpenGroupAPI.PinnedMessage(id: 1, pinnedAt: 1, pinnedBy: ""),
@ -1199,7 +1195,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (capabilities: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?))?
var response: (capabilities: (info: ResponseInfoType, data: OpenGroupAPI.Capabilities?), room: (info: ResponseInfoType, data: OpenGroupAPI.Room?))?
mockStorage
.read { db in
@ -1216,7 +1212,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(100)
)
@ -1258,7 +1254,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("correctly sends the message") {
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1291,7 +1287,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testServer"))
expect(requestData?.urlString).to(equal("testServer/room/testRoom/message"))
}
@ -1304,7 +1299,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("signs the message correctly") {
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1344,7 +1339,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try OpenGroup.deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1379,7 +1374,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try Identity.filter(id: .x25519PrivateKey).deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1412,7 +1407,7 @@ class OpenGroupAPISpec: QuickSpec {
mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset
mockEd25519.when { try $0.sign(data: anyArray(), keyPair: any()) }.thenReturn(nil)
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1452,7 +1447,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("signs the message correctly") {
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1492,7 +1487,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try OpenGroup.deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1527,7 +1522,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1568,7 +1563,7 @@ class OpenGroupAPISpec: QuickSpec {
}
.thenReturn(nil)
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1621,7 +1616,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Message)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.Message)?
mockStorage
.read { db in
@ -1651,7 +1646,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
}
}
@ -1674,7 +1668,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("correctly sends the update") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1703,7 +1697,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("PUT"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
}
@ -1716,7 +1709,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("signs the message correctly") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1755,7 +1748,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try OpenGroup.deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1789,7 +1782,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try Identity.filter(id: .x25519PrivateKey).deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1821,7 +1814,7 @@ class OpenGroupAPISpec: QuickSpec {
mockEd25519.reset() // The 'keyPair' value doesn't equate so have to explicitly reset
mockEd25519.when { try $0.sign(data: anyArray(), keyPair: any()) }.thenReturn(nil)
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1860,7 +1853,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("signs the message correctly") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1899,7 +1892,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try OpenGroup.deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1933,7 +1926,7 @@ class OpenGroupAPISpec: QuickSpec {
_ = try Identity.filter(id: .ed25519SecretKey).deleteAll(db)
}
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -1973,7 +1966,7 @@ class OpenGroupAPISpec: QuickSpec {
}
.thenReturn(nil)
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -2010,7 +2003,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
mockStorage
.read { db in
@ -2037,13 +2030,12 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("DELETE"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/message/123"))
}
}
context("when deleting all messages for a user") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
beforeEach {
class TestApi: TestOnionRequestAPI {
@ -2082,7 +2074,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("DELETE"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/all/testUserId"))
}
}
@ -2096,7 +2087,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: OnionRequestResponseInfoType?
var response: ResponseInfoType?
mockStorage
.read { db in
@ -2123,7 +2114,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/pin/123"))
}
}
@ -2135,7 +2125,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: OnionRequestResponseInfoType?
var response: ResponseInfoType?
mockStorage
.read { db in
@ -2162,7 +2152,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/unpin/123"))
}
}
@ -2174,7 +2163,7 @@ class OpenGroupAPISpec: QuickSpec {
}
dependencies = dependencies.with(onionApi: TestApi.self)
var response: OnionRequestResponseInfoType?
var response: ResponseInfoType?
mockStorage
.read { db in
@ -2200,7 +2189,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/unpin/all"))
}
}
@ -2241,7 +2229,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/file"))
}
@ -2277,7 +2264,7 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.headers[Header.contentDisposition.rawValue])
expect(requestData?.headers[HTTPHeader.contentDisposition])
.toNot(contain("filename"))
}
@ -2314,7 +2301,7 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName"))
expect(requestData?.headers[HTTPHeader.contentDisposition]).to(contain("TestFileName"))
}
}
@ -2352,7 +2339,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/room/testRoom/file/1"))
}
}
@ -2383,7 +2369,7 @@ class OpenGroupAPISpec: QuickSpec {
}
it("correctly sends the message request") {
var response: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)?
var response: (info: ResponseInfoType, data: OpenGroupAPI.SendDirectMessageResponse)?
mockStorage
.read { db in
@ -2413,7 +2399,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate signature headers
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/inbox/testUserId"))
}
}
@ -2421,7 +2406,7 @@ class OpenGroupAPISpec: QuickSpec {
// MARK: - Users
context("when banning a user") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
beforeEach {
class TestApi: TestOnionRequestAPI {
@ -2461,7 +2446,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/user/testUserId/ban"))
}
@ -2531,7 +2515,7 @@ class OpenGroupAPISpec: QuickSpec {
}
context("when unbanning a user") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
beforeEach {
class TestApi: TestOnionRequestAPI {
@ -2570,7 +2554,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/user/testUserId/unban"))
}
@ -2638,7 +2621,7 @@ class OpenGroupAPISpec: QuickSpec {
}
context("when updating a users permissions") {
var response: (info: OnionRequestResponseInfoType, data: Data?)?
var response: (info: ResponseInfoType, data: Data?)?
beforeEach {
class TestApi: TestOnionRequestAPI {
@ -2680,7 +2663,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.info as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/user/testUserId/moderator"))
}
@ -2773,7 +2755,7 @@ class OpenGroupAPISpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.generic.localizedDescription),
equal(HTTPError.generic.localizedDescription),
timeout: .milliseconds(100)
)
@ -2782,14 +2764,14 @@ class OpenGroupAPISpec: QuickSpec {
}
context("when banning and deleting all messages for a user") {
var response: [OnionRequestResponseInfoType]?
var response: [ResponseInfoType]?
beforeEach {
class TestApi: TestOnionRequestAPI {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse<NoResponse>(
HTTP.BatchSubResponse<NoResponse>(
code: 200,
headers: [:],
body: nil,
@ -2797,7 +2779,7 @@ class OpenGroupAPISpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse<NoResponse>(
HTTP.BatchSubResponse<NoResponse>(
code: 200,
headers: [:],
body: nil,
@ -2842,7 +2824,6 @@ class OpenGroupAPISpec: QuickSpec {
// Validate request data
let requestData: TestOnionRequestAPI.RequestData? = (response?.first as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.httpMethod).to(equal("POST"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.urlString).to(equal("testserver/sequence"))
}
@ -3013,14 +2994,13 @@ class OpenGroupAPISpec: QuickSpec {
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.urlString).to(equal("testserver/rooms"))
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue])
expect(requestData?.headers[HTTPHeader.sogsPubKey])
.to(equal("00bac6e71efd7dfa4a83c98ed24f254ab2c267f9ccdb172a5280a0444ad24e89cc"))
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSignature".bytes.toBase64()))
expect(requestData?.headers[HTTPHeader.sogsTimestamp]).to(equal("1234567890"))
expect(requestData?.headers[HTTPHeader.sogsNonce]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[HTTPHeader.sogsSignature]).to(equal("TestSignature".bytes.toBase64()))
}
it("fails when the signature is not generated") {
@ -3083,13 +3063,12 @@ class OpenGroupAPISpec: QuickSpec {
let requestData: TestOnionRequestAPI.RequestData? = (response?.0 as? TestOnionRequestAPI.ResponseInfo)?.requestData
expect(requestData?.urlString).to(equal("testserver/rooms"))
expect(requestData?.httpMethod).to(equal("GET"))
expect(requestData?.server).to(equal("testserver"))
expect(requestData?.publicKey).to(equal("88672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers).to(haveCount(4))
expect(requestData?.headers[Header.sogsPubKey.rawValue]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers[Header.sogsTimestamp.rawValue]).to(equal("1234567890"))
expect(requestData?.headers[Header.sogsNonce.rawValue]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[Header.sogsSignature.rawValue]).to(equal("TestSogsSignature".bytes.toBase64()))
expect(requestData?.headers[HTTPHeader.sogsPubKey]).to(equal("1588672ccb97f40bb57238989226cf429b575ba355443f47bc76c5ab144a96c65b"))
expect(requestData?.headers[HTTPHeader.sogsTimestamp]).to(equal("1234567890"))
expect(requestData?.headers[HTTPHeader.sogsNonce]).to(equal("pK6YRtQApl4NhECGizF0Cg=="))
expect(requestData?.headers[HTTPHeader.sogsSignature]).to(equal("TestSogsSignature".bytes.toBase64()))
}
it("fails when the blindedKeyPair is not generated") {

View File

@ -47,7 +47,7 @@ class OpenGroupManagerSpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -55,7 +55,7 @@ class OpenGroupManagerSpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomData,
@ -917,7 +917,7 @@ class OpenGroupManagerSpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.parsingFailed.localizedDescription),
equal(HTTPError.parsingFailed.localizedDescription),
timeout: .milliseconds(50)
)
}
@ -1953,7 +1953,7 @@ class OpenGroupManagerSpec: QuickSpec {
var didComplete: Bool = false // Prevent multi-threading test bugs
mockOGMCache.when { $0.groupImagePromises }
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise(error: HTTP.Error.generic)])
.thenReturn([OpenGroup.idFor(roomToken: "testRoom", server: "testServer"): Promise(error: HTTPError.generic)])
testPollInfo = OpenGroupAPI.RoomPollInfo(
token: "testRoom",
@ -3194,7 +3194,7 @@ class OpenGroupManagerSpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -3202,7 +3202,7 @@ class OpenGroupManagerSpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomsData,
@ -3349,7 +3349,7 @@ class OpenGroupManagerSpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.invalidResponse.localizedDescription),
equal(HTTPError.invalidResponse.localizedDescription),
timeout: .milliseconds(50)
)
expect(TestRoomsApi.callCounter).to(equal(9)) // First attempt + 8 retries
@ -3369,7 +3369,7 @@ class OpenGroupManagerSpec: QuickSpec {
expect(error?.localizedDescription)
.toEventually(
equal(HTTP.Error.invalidResponse.localizedDescription),
equal(HTTPError.invalidResponse.localizedDescription),
timeout: .milliseconds(50)
)
expect(mockOGMCache)
@ -3414,7 +3414,7 @@ class OpenGroupManagerSpec: QuickSpec {
override class var mockResponse: Data? {
let responses: [Data] = [
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: capabilitiesData,
@ -3422,7 +3422,7 @@ class OpenGroupManagerSpec: QuickSpec {
)
),
try! JSONEncoder().encode(
OpenGroupAPI.BatchSubResponse(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: roomsData,
@ -3568,12 +3568,12 @@ class OpenGroupManagerSpec: QuickSpec {
it("adds the image retrieval promise to the cache") {
class TestNeverReturningApi: OnionRequestAPIType {
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
return Promise<(OnionRequestResponseInfoType, Data?)>.pending().promise
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(ResponseInfoType, Data?)> {
return Promise<(ResponseInfoType, Data?)>.pending().promise
}
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
return Promise.value(Data())
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> Promise<(ResponseInfoType, Data?)> {
return Promise.value((HTTP.ResponseInfo(code: 200, headers: [:]), Data()))
}
}
dependencies = dependencies.with(onionApi: TestNeverReturningApi.self)

View File

@ -13,14 +13,18 @@ class TestOnionRequestAPI: OnionRequestAPIType {
let urlString: String?
let httpMethod: String
let headers: [String: String]
let snodeMethod: String?
let body: Data?
let destination: OnionRequestAPIDestination
let server: String
let version: OnionRequestAPIVersion
let publicKey: String?
var publicKey: String? {
switch destination {
case .snode: return nil
case .server(_, _, let x25519PublicKey, _, _): return x25519PublicKey
}
}
}
class ResponseInfo: OnionRequestResponseInfoType {
class ResponseInfo: ResponseInfoType {
let requestData: RequestData
let code: Int
let headers: [String: String]
@ -34,18 +38,20 @@ class TestOnionRequestAPI: OnionRequestAPIType {
class var mockResponse: Data? { return nil }
static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
static func sendOnionRequest(_ request: URLRequest, to server: String, with x25519PublicKey: String) -> Promise<(ResponseInfoType, Data?)> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: request.url?.absoluteString,
httpMethod: (request.httpMethod ?? "GET"),
headers: (request.allHTTPHeaderFields ?? [:]),
snodeMethod: nil,
body: request.httpBody,
server: server,
version: version,
publicKey: x25519PublicKey
destination: OnionRequestAPIDestination.server(
host: request.url!.host!,
target: OnionRequestAPIVersion.v4.rawValue,
x25519PublicKey: x25519PublicKey,
scheme: request.url!.scheme,
port: request.url!.port.map { UInt16($0) }
)
),
code: 200,
headers: [:]
@ -54,7 +60,19 @@ class TestOnionRequestAPI: OnionRequestAPIType {
return Promise.value((responseInfo, mockResponse))
}
static func sendOnionRequest(to snode: Snode, invoking method: SnodeAPIEndpoint, with parameters: JSON, associatedWith publicKey: String?) -> Promise<Data> {
return Promise.value(mockResponse!)
static func sendOnionRequest(_ payload: Data, to snode: Snode) -> Promise<(ResponseInfoType, Data?)> {
let responseInfo: ResponseInfo = ResponseInfo(
requestData: RequestData(
urlString: "\(snode.address):\(snode.port)/onion_req/v2",
httpMethod: "POST",
headers: [:],
body: payload,
destination: OnionRequestAPIDestination.snode(snode)
),
code: 200,
headers: [:]
)
return Promise.value((responseInfo, mockResponse))
}
}

View File

@ -2,7 +2,7 @@
import Foundation
public enum OnionRequestAPIDestination: CustomStringConvertible {
public enum OnionRequestAPIDestination: CustomStringConvertible, Codable {
case snode(Snode)
case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?)

View File

@ -393,7 +393,11 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
/// Sends an onion request to `server`. Builds new paths as needed.
public static func sendOnionRequest(_ request: URLRequest, to server: String, using version: OnionRequestAPIVersion = .v4, with x25519PublicKey: String) -> Promise<(OnionRequestResponseInfoType, Data?)> {
public static func sendOnionRequest(
_ request: URLRequest,
to server: String, // TODO: Remove this 'server' value (unused)
with x25519PublicKey: String
) -> Promise<(ResponseInfoType, Data?)> {
guard let url = request.url, let host = request.url?.host else {
return Promise(error: OnionRequestAPIError.invalidURL)
}

View File

@ -0,0 +1,34 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import GRDB
import SessionUtilitiesKit
open class SSKDependencies: Dependencies {
public var _onionApi: Atomic<OnionRequestAPIType.Type?>
public var onionApi: OnionRequestAPIType.Type {
get { Dependencies.getValueSettingIfNull(&_onionApi) { OnionRequestAPI.self } }
set { _onionApi.mutate { $0 = newValue } }
}
// MARK: - Initialization
public init(
onionApi: OnionRequestAPIType.Type? = nil,
generalCache: Atomic<GeneralCacheType>? = nil,
storage: Storage? = nil,
scheduler: ValueObservationScheduler? = nil,
standardUserDefaults: UserDefaultsType? = nil,
date: Date? = nil
) {
_onionApi = Atomic(onionApi)
super.init(
generalCache: generalCache,
storage: storage,
scheduler: scheduler,
standardUserDefaults: standardUserDefaults,
date: date
)
}
}

View File

@ -195,7 +195,7 @@ class ThreadDisappearingMessagesViewModelSpec: QuickSpec {
)
)
viewModel.settingsData.first?.elements.last?.onTap?(nil)
viewModel.tableData.first?.elements.last?.onTap?()
}
it("shows the save button") {

View File

@ -97,7 +97,7 @@ class ThreadSettingsViewModelSpec: QuickSpec {
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .searchConversation })?
.onTap?(nil)
.onTap?()
expect(didTriggerSearchCallbackTriggered).to(beTrue())
}
@ -106,8 +106,8 @@ class ThreadSettingsViewModelSpec: QuickSpec {
viewModel.settingsData
.first(where: { $0.model == .content })?
.elements
.first(where: { $0.id == .notificationMute })?
.onTap?(nil)
.first(where: { $0.id == .notifications })?
.onTap?()
expect(
mockStorage
@ -208,15 +208,11 @@ class ThreadSettingsViewModelSpec: QuickSpec {
context("when entering edit mode") {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first?
.elements.first?
.leftAccessory
switch leftAccessory {
case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestNew")
default: break
}
viewModel.textChanged("TestNew", for: .nickname)
// TODO: Enter edit mode by pressing on the first item
// viewModel.tableData.first?
// .elements.first?
// .onTap?()
}
it("enters the editing state") {
@ -341,15 +337,12 @@ class ThreadSettingsViewModelSpec: QuickSpec {
context("when entering edit mode") {
beforeEach {
viewModel.rightNavItems.firstValue()??.first?.action?()
viewModel.textChanged("TestUserNew", for: .nickname)
let leftAccessory: SessionCell.Accessory? = viewModel.settingsData.first?
.elements.first?
.leftAccessory
switch leftAccessory {
case .threadInfo(_, _, _, _, let titleChanged): titleChanged?("TestUserNew")
default: break
}
// TODO: Enter edit mode by pressing on the first item
// viewModel.tableData.first?
// .elements.first?
// .onTap?()
}
it("enters the editing state") {

View File

@ -132,7 +132,7 @@ class NotificationContentViewModelSpec: QuickSpec {
context("when tapping an item") {
it("updates the saved preference") {
viewModel.settingsData.first?.elements.last?.onTap?(nil)
viewModel.tableData.first?.elements.last?.onTap?()
expect(mockStorage[.preferencesNotificationPreviewType])
.to(equal(Preferences.NotificationPreviewType.noNameNoPreview))
@ -147,7 +147,7 @@ class NotificationContentViewModelSpec: QuickSpec {
receiveCompletion: { _ in },
receiveValue: { _ in didDismissScreen = true }
)
viewModel.settingsData.first?.elements.last?.onTap?(nil)
viewModel.tableData.first?.elements.last?.onTap?()
expect(didDismissScreen).to(beTrue())
}

View File

@ -0,0 +1,106 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
public extension HTTP {
// MARK: - Convenience Aliases
typealias BatchResponse = [(ResponseInfoType, Codable?)]
typealias BatchResponseTypes = [Codable.Type]
// MARK: - BatchSubResponse<T>
struct BatchSubResponse<T: Codable>: Codable {
/// The numeric http response code (e.g. 200 for success)
public let code: Int32
/// Any headers returned by the request
public let headers: [String: String]
/// The body of the request; will be plain json if content-type is `application/json`, otherwise it will be base64 encoded data
public let body: T?
/// A flag to indicate that there was a body but it failed to parse
public let failedToParseBody: Bool
public init(
code: Int32,
headers: [String: String] = [:],
body: T? = nil,
failedToParseBody: Bool = false
) {
self.code = code
self.headers = headers
self.body = body
self.failedToParseBody = failedToParseBody
}
}
}
public extension HTTP.BatchSubResponse {
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)
self = HTTP.BatchSubResponse(
code: try container.decode(Int32.self, forKey: .code),
headers: ((try? container.decode([String: String].self, forKey: .headers)) ?? [:]),
body: body,
failedToParseBody: (
body == nil &&
T.self != NoResponse.self &&
!(T.self is ExpressibleByNilLiteral.Type)
)
)
}
}
// 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)
}
}
public extension Promise where T == (ResponseInfoType, Data?) {
func decoded(as types: HTTP.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<HTTP.BatchResponse> {
self.map(on: queue) { responseInfo, maybeData -> HTTP.BatchResponse in
// Need to split the data into an array of data so each item can be Decoded correctly
guard let data: Data = maybeData 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 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) }),
resultsArray.count == types.count
else { throw HTTPError.parsingFailed }
dataArray = resultsArray
default: throw HTTPError.parsingFailed
}
do {
return try zip(dataArray, types)
.map { data, type in try type.decoded(from: data, using: dependencies) }
.map { data in (responseInfo, data) }
}
catch {
throw HTTPError.parsingFailed
}
}
}
}

View File

@ -0,0 +1,243 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Quick
import Nimble
@testable import SessionUtilitiesKit
class BatchRequestInfoSpec: QuickSpec {
struct TestType: Codable, Equatable {
let stringValue: String
}
struct TestType2: Codable, Equatable {
let intValue: Int
let stringValue2: String
}
// MARK: - Spec
override func spec() {
// MARK: - HTTP.BatchSubResponse<T>
describe("an HTTP.BatchSubResponse<T>") {
context("when decoding") {
it("decodes correctly") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": {
"stringValue": "testValue"
}
}
"""
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
HTTP.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).toNot(beNil())
}
it("decodes with invalid body data") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
HTTP.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
}
it("flags invalid body data as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
"body": "Hello!!!"
}
"""
let subResponse: HTTP.BatchSubResponse<TestType>? = try? JSONDecoder().decode(
HTTP.BatchSubResponse<TestType>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beTrue())
}
it("does not flag a missing or invalid optional body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: HTTP.BatchSubResponse<TestType?>? = try? JSONDecoder().decode(
HTTP.BatchSubResponse<TestType?>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
it("does not flag a NoResponse body as invalid") {
let jsonString: String = """
{
"code": 200,
"headers": {
"testKey": "testValue"
},
}
"""
let subResponse: HTTP.BatchSubResponse<NoResponse>? = try? JSONDecoder().decode(
HTTP.BatchSubResponse<NoResponse>.self,
from: jsonString.data(using: .utf8)!
)
expect(subResponse).toNot(beNil())
expect(subResponse?.body).to(beNil())
expect(subResponse?.failedToParseBody).to(beFalse())
}
}
}
// MARK: - Convenience
// MARK: --Decodable
describe("a Decodable") {
it("decodes correctly") {
let jsonData: Data = "{\"stringValue\":\"testValue\"}".data(using: .utf8)!
let result: TestType? = try? TestType.decoded(from: jsonData)
expect(result).to(equal(TestType(stringValue: "testValue")))
}
}
// MARK: - --Promise
describe("a (ResponseInfoType, Data?) Promise") {
var responseInfo: ResponseInfoType!
var testType: TestType!
var testType2: TestType2!
var data: Data!
beforeEach {
responseInfo = HTTP.ResponseInfo(code: 200, headers: [:])
testType = TestType(stringValue: "Test")
testType2 = TestType2(intValue: 1, stringValue2: "Test2")
data = """
[\([
try! JSONEncoder().encode(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: testType,
failedToParseBody: false
)
),
try! JSONEncoder().encode(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: testType2,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ","))]
""".data(using: .utf8)!
}
it("decodes valid data correctly") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self
])
expect(result.value).toNot(beNil())
expect((result.value?[0].1 as? HTTP.BatchSubResponse<TestType>)?.body)
.to(equal(testType))
expect((result.value?[1].1 as? HTTP.BatchSubResponse<TestType2>)?.body)
.to(equal(testType2))
}
it("fails if there is no data") {
let result = Promise.value((responseInfo, nil)).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the data is not JSON") {
let result = Promise.value((responseInfo, Data([1, 2, 3]))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the data is not a JSON array") {
let result = Promise.value((responseInfo, "{}".data(using: .utf8))).decoded(as: [])
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if the JSON array does not have the same number of items as the expected types") {
let result = Promise.value((responseInfo, data))
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self,
HTTP.BatchSubResponse<TestType2>.self
])
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
}
it("fails if one of the JSON array values fails to decode") {
data = """
[\([
try! JSONEncoder().encode(
HTTP.BatchSubResponse(
code: 200,
headers: [:],
body: testType,
failedToParseBody: false
)
)
]
.map { String(data: $0, encoding: .utf8)! }
.joined(separator: ",")),{"test": "test"}]
""".data(using: .utf8)!
let result = Promise.value((responseInfo, data))
.decoded(as: [
HTTP.BatchSubResponse<TestType>.self,
HTTP.BatchSubResponse<TestType2>.self
])
expect(result.error?.localizedDescription).to(equal(HTTPError.parsingFailed.localizedDescription))
}
}
}
}

View File

@ -5,7 +5,7 @@ import Foundation
import Quick
import Nimble
@testable import SessionMessagingKit
@testable import SessionUtilitiesKit
class HeaderSpec: QuickSpec {
// MARK: - Spec
@ -13,7 +13,8 @@ class HeaderSpec: QuickSpec {
override func spec() {
describe("a Dictionary of Header to String values") {
it("can be converted into a dictionary of String to String values") {
expect([Header.authorization: "test"].toHTTPHeaders()).to(equal(["Authorization": "test"]))
expect([HTTPHeader.authorization: "test"].toHTTPHeaders())
.to(equal(["Authorization": "test"]))
}
}
}

View File

@ -4,11 +4,22 @@ import Foundation
import Quick
import Nimble
import SessionUtilitiesKit
@testable import SessionMessagingKit
@testable import SessionUtilitiesKit
class RequestSpec: QuickSpec {
enum TestEndpoint: EndpointType {
case test1
case testParams(String, Int)
var path: String {
switch self {
case .test1: return "test1"
case .testParams(let str, let int): return "testParams/\(str)/int/\(int)"
}
}
}
struct TestType: Codable, Equatable {
let stringValue: String
}
@ -18,9 +29,9 @@ class RequestSpec: QuickSpec {
override func spec() {
describe("a Request") {
it("is initialized with the correct default values") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch
endpoint: .test1
)
expect(request.method.rawValue).to(equal("GET"))
@ -31,42 +42,42 @@ class RequestSpec: QuickSpec {
context("when generating a URL") {
it("adds a leading forward slash to the endpoint path") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch
endpoint: .test1
)
expect(request.urlPathAndParamsString).to(equal("/batch"))
expect(request.urlPathAndParamsString).to(equal("/test1"))
}
it("creates a valid URL with no query parameters") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch
endpoint: .test1
)
expect(request.urlPathAndParamsString).to(equal("/batch"))
expect(request.urlPathAndParamsString).to(equal("/test1"))
}
it("creates a valid URL when query parameters are provided") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
queryParameters: [
.limit: "123"
]
)
expect(request.urlPathAndParamsString).to(equal("/batch?limit=123"))
expect(request.urlPathAndParamsString).to(equal("/test1?limit=123"))
}
}
context("when generating a URLRequest") {
it("sets all the values correctly") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
method: .delete,
server: "testServer",
endpoint: .batch,
endpoint: .test1,
headers: [
.authorization: "test"
]
@ -79,22 +90,22 @@ class RequestSpec: QuickSpec {
}
it("throws an error if the URL is invalid") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .roomPollInfo("!!%%", 123)
endpoint: .testParams("!!%%", 123)
)
expect {
try request.generateUrlRequest()
}
.to(throwError(HTTP.Error.invalidURL))
.to(throwError(HTTPError.invalidURL))
}
context("with a base64 string body") {
it("successfully encodes the body") {
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
let request: Request<String, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
body: "TestMessage".data(using: .utf8)!.base64EncodedString()
)
@ -106,24 +117,24 @@ class RequestSpec: QuickSpec {
}
it("throws an error if the body is not base64 encoded") {
let request: Request<String, OpenGroupAPI.Endpoint> = Request(
let request: Request<String, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
body: "TestMessage"
)
expect {
try request.generateUrlRequest()
}
.to(throwError(HTTP.Error.parsingFailed))
.to(throwError(HTTPError.parsingFailed))
}
}
context("with a byte body") {
it("successfully encodes the body") {
let request: Request<[UInt8], OpenGroupAPI.Endpoint> = Request(
let request: Request<[UInt8], TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
body: [1, 2, 3]
)
@ -135,9 +146,9 @@ class RequestSpec: QuickSpec {
context("with a JSON body") {
it("successfully encodes the body") {
let request: Request<TestType, OpenGroupAPI.Endpoint> = Request(
let request: Request<TestType, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
body: TestType(stringValue: "test")
)
@ -151,9 +162,9 @@ class RequestSpec: QuickSpec {
}
it("successfully encodes no body") {
let request: Request<NoBody, OpenGroupAPI.Endpoint> = Request(
let request: Request<NoBody, TestEndpoint> = Request(
server: "testServer",
endpoint: .batch,
endpoint: .test1,
body: nil
)