From 1c474955de05caf3b0689f7576177180173a6a15 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 4 Mar 2022 13:33:06 +1100 Subject: [PATCH] File upload working, further code cleanup Got the updated file upload working Removed the legacy 'room' header Consolidated a number of types between SOGS, FileServer and general requests Updated the OnionRequestAPI to deal with a Data payload (rather than encoding it to a string and then back to data) --- Session.xcodeproj/project.pbxproj | 44 +++-- .../ConversationVC+Interaction.swift | 2 +- .../Open Groups/OpenGroupSuggestionGrid.swift | 2 +- Session/Settings/SettingsVC.swift | 3 +- .../Models/FileDownloadResponse.swift | 4 +- .../Models/LegacyFileDownloadResponse.swift | 4 +- .../Common Networking/QueryParam.swift | 1 + .../Common Networking/Request.swift | 106 +++++++++++ .../Database/Storage+Jobs.swift | 7 +- .../File Server/FileServerAPIV2.swift | 162 ++++++----------- .../File Server/Types/FSEndpoint.swift | 19 ++ .../Jobs/AttachmentDownloadJob.swift | 2 +- .../Jobs/AttachmentUploadJob.swift | 91 +++++++--- SessionMessagingKit/Jobs/MessageSendJob.swift | 4 +- .../Messages/Message+Destination.swift | 6 +- .../Open Groups/Models/BatchRequestInfo.swift | 30 ++-- .../Models/LegacyAuthTokenResponse.swift | 2 +- .../Models/LegacyOpenGroupMessageV2.swift | 4 +- .../Open Groups/Models/Room.swift | 4 +- .../{OGMessage.swift => SOGSMessage.swift} | 10 +- .../Models/SendMessageRequest.swift | 4 +- .../Open Groups/OpenGroupAPI.swift | 166 ++++++++---------- .../Open Groups/OpenGroupManager.swift | 12 +- .../Open Groups/Types/Request.swift | 94 ---------- .../{Endpoint.swift => SOGSEndpoint.swift} | 8 +- .../Types/{Error.swift => SOGSError.swift} | 6 - .../Sending & Receiving/MessageSender.swift | 5 +- SessionMessagingKit/Storage.swift | 1 + .../Utilities/Promise+Utilities.swift | 13 +- .../Open Groups/OpenGroupAPISpec.swift | 17 +- .../OnionRequestAPI+Encryption.swift | 10 +- SessionSnodeKit/OnionRequestAPI.swift | 49 ++---- SessionUtilitiesKit/Networking/HTTP.swift | 15 +- .../MessageSender+Convenience.swift | 123 ++++++++----- 34 files changed, 534 insertions(+), 496 deletions(-) create mode 100644 SessionMessagingKit/Common Networking/Request.swift create mode 100644 SessionMessagingKit/File Server/Types/FSEndpoint.swift rename SessionMessagingKit/Open Groups/Models/{OGMessage.swift => SOGSMessage.swift} (92%) delete mode 100644 SessionMessagingKit/Open Groups/Types/Request.swift rename SessionMessagingKit/Open Groups/Types/{Endpoint.swift => SOGSEndpoint.swift} (97%) rename SessionMessagingKit/Open Groups/Types/{Error.swift => SOGSError.swift} (69%) diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 4951ea3c6..a9142c3b6 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -793,6 +793,8 @@ FD83B9C527CF3E2A005E1583 /* OpenGroupSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */; }; FD83B9C727CF3F10005E1583 /* CapabilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */; }; FD83B9C927D0487A005E1583 /* SendDirectMessageResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */; }; + FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */; }; + FD83B9CE27D17A04005E1583 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; }; FD859EF227BF6BA200510D0C /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF127BF6BA200510D0C /* Data+Utilities.swift */; }; FD859EF427C2F49200510D0C /* TestSodium.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF327C2F49200510D0C /* TestSodium.swift */; }; FD859EF627C2F52C00510D0C /* TestSign.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EF527C2F52C00510D0C /* TestSign.swift */; }; @@ -802,13 +804,12 @@ FD859F0027C4691300510D0C /* MockDataGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD859EFF27C4691300510D0C /* MockDataGenerator.swift */; }; FD88BAD927A7439C00BBC442 /* MessageRequestsCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */; }; FD88BADB27A750F200BBC442 /* MessageRequestsMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */; }; - FDC4380927B31D4E00C60D73 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* Error.swift */; }; - FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380A27B31D7E00C60D73 /* Request.swift */; }; + FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380827B31D4E00C60D73 /* SOGSError.swift */; }; FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; FDC4381A27B34EBA00C60D73 /* LegacyCompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */; }; FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */; }; - FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* Endpoint.swift */; }; + FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; }; FDC4382627B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */; }; FDC4382827B37FD300C60D73 /* LegacyModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */; }; FDC4382A27B3802D00C60D73 /* LegacyRoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */; }; @@ -832,7 +833,7 @@ FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; }; FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; }; FDC4386127B4CDDF00C60D73 /* FileResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386027B4CDDF00C60D73 /* FileResponse.swift */; }; - FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* OGMessage.swift */; }; + FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */; }; FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */; }; FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386627B4E10E00C60D73 /* Capabilities.swift */; }; FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386827B4E6B700C60D73 /* String+Utlities.swift */; }; @@ -1947,6 +1948,8 @@ FD83B9C427CF3E2A005E1583 /* OpenGroupSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupSpec.swift; sourceTree = ""; }; FD83B9C627CF3F10005E1583 /* CapabilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CapabilitiesSpec.swift; sourceTree = ""; }; FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendDirectMessageResponse.swift; sourceTree = ""; }; + FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FSEndpoint.swift; sourceTree = ""; }; + FD83B9CD27D17A04005E1583 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; FD859EEF27BF207700510D0C /* SessionProtos.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = SessionProtos.proto; sourceTree = ""; }; FD859EF027BF207C00510D0C /* WebSocketResources.proto */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.protobuf; path = WebSocketResources.proto; sourceTree = ""; }; FD859EF127BF6BA200510D0C /* Data+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = ""; }; @@ -1959,13 +1962,12 @@ FD88BAD827A7439C00BBC442 /* MessageRequestsCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsCell.swift; sourceTree = ""; }; FD88BADA27A750F200BBC442 /* MessageRequestsMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsMigration.swift; sourceTree = ""; }; FD9039443F7CB729CF71350E /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-SessionNotificationServiceExtension.debug.xcconfig"; sourceTree = ""; }; - FDC4380827B31D4E00C60D73 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; - FDC4380A27B31D7E00C60D73 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; + FDC4380827B31D4E00C60D73 /* SOGSError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSError.swift; sourceTree = ""; }; FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollBody.swift; sourceTree = ""; }; FDC4381B27B354AC00C60D73 /* LegacyPublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPublicKeyBody.swift; sourceTree = ""; }; - FDC4381F27B36ADC00C60D73 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; + FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = ""; }; FDC4382527B37F6900C60D73 /* LegacyDeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyDeletedMessagesResponse.swift; sourceTree = ""; }; FDC4382727B37FD300C60D73 /* LegacyModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyModeratorsResponse.swift; sourceTree = ""; }; FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyRoomsResponse.swift; sourceTree = ""; }; @@ -1989,7 +1991,7 @@ FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = ""; }; FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PinnedMessage.swift; sourceTree = ""; }; FDC4386027B4CDDF00C60D73 /* FileResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileResponse.swift; sourceTree = ""; }; - FDC4386227B4D94E00C60D73 /* OGMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OGMessage.swift; sourceTree = ""; }; + FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSMessage.swift; sourceTree = ""; }; FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfo.swift; sourceTree = ""; }; FDC4386627B4E10E00C60D73 /* Capabilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Capabilities.swift; sourceTree = ""; }; FDC4386827B4E6B700C60D73 /* String+Utlities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utlities.swift"; sourceTree = ""; }; @@ -3420,6 +3422,7 @@ isa = PBXGroup; children = ( FDC4383227B385B200C60D73 /* Models */, + FD83B9CA27D179AF005E1583 /* Types */, B87EF17026367CF800124B3C /* FileServerAPIV2.swift */, ); path = "File Server"; @@ -3905,6 +3908,14 @@ path = Models; sourceTree = ""; }; + FD83B9CA27D179AF005E1583 /* Types */ = { + isa = PBXGroup; + children = ( + FD83B9CB27D179BC005E1583 /* FSEndpoint.swift */, + ); + path = Types; + sourceTree = ""; + }; FD88BAD727A7438E00BBC442 /* Views */ = { isa = PBXGroup; children = ( @@ -3916,9 +3927,8 @@ FDC4380727B31D3A00C60D73 /* Types */ = { isa = PBXGroup; children = ( - FDC4380A27B31D7E00C60D73 /* Request.swift */, - FDC4381F27B36ADC00C60D73 /* Endpoint.swift */, - FDC4380827B31D4E00C60D73 /* Error.swift */, + FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */, + FDC4380827B31D4E00C60D73 /* SOGSError.swift */, FDC4381627B32EC700C60D73 /* Personalization.swift */, FDC4381427B329CE00C60D73 /* NonceGenerator.swift */, FDC438C027BB4E6800C60D73 /* Dependencies.swift */, @@ -3940,7 +3950,7 @@ FDC4386027B4CDDF00C60D73 /* FileResponse.swift */, FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, FDC438CA27BB7DB100C60D73 /* UpdateMessageRequest.swift */, - FDC4386227B4D94E00C60D73 /* OGMessage.swift */, + FDC4386227B4D94E00C60D73 /* SOGSMessage.swift */, FDC438C627BB6DF000C60D73 /* DirectMessage.swift */, FDC438C827BB706500C60D73 /* SendDirectMessageRequest.swift */, FD83B9C827D0487A005E1583 /* SendDirectMessageResponse.swift */, @@ -3988,6 +3998,7 @@ FDC4385527B484AE00C60D73 /* Models */, FDC4384E27B4804F00C60D73 /* Header.swift */, FDC4385027B4807400C60D73 /* QueryParam.swift */, + FD83B9CD27D17A04005E1583 /* Request.swift */, ); path = "Common Networking"; sourceTree = ""; @@ -5287,7 +5298,6 @@ C3D9E3BF25676AD70040E4F3 /* TSAttachmentStream.m in Sources */, C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */, B8B32021258B1A650020074B /* Contact.swift in Sources */, - FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */, FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */, C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, @@ -5337,11 +5347,12 @@ B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, FDC4381C27B354AC00C60D73 /* LegacyPublicKeyBody.swift in Sources */, FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */, - FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */, + FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */, C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */, C300A5BD2554B00D00555489 /* ReadReceipt.swift in Sources */, + FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */, C32C5AB5256DBE8F003C73A2 /* TSOutgoingMessage+Conversion.swift in Sources */, FDC4385927B484E800C60D73 /* FileUploadBody.swift in Sources */, C32C5E5B256DDF45003C73A2 /* OWSStorage.m in Sources */, @@ -5377,8 +5388,8 @@ B8D0A25925E367AC00C1835E /* Notification+MessageReceiver.swift in Sources */, FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */, C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */, - FDC4380927B31D4E00C60D73 /* Error.swift in Sources */, - FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */, + FDC4380927B31D4E00C60D73 /* SOGSError.swift in Sources */, + FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */, C3DB66CC260AF1F3001EFC55 /* OpenGroupAPI+ObjC.swift in Sources */, C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */, C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */, @@ -5403,6 +5414,7 @@ C300A5E72554B07300555489 /* ExpirationTimerUpdate.swift in Sources */, B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */, C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */, + FD83B9CE27D17A04005E1583 /* Request.swift in Sources */, FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */, diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 99b9b8370..5f5769205 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -568,7 +568,7 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc let attachment = mediaView.attachment if let pointer = attachment as? TSAttachmentPointer { if pointer.state == .failed { - // TODO: Tapped a failed incoming attachment + // TODO: Tapped a failed incoming attachment (Note: This is generally a permanent failure - see `AttachmentDownloadJob`) } } guard let stream = attachment as? TSAttachmentStream else { return } diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 58e7633d7..f0e4fd077 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -182,7 +182,7 @@ extension OpenGroupSuggestionGrid { label.text = room.name - if let imageId: Int64 = room.imageId { + if let imageId: UInt64 = room.imageId { let promise = OpenGroupManager.roomImage(imageId, for: room.token, on: OpenGroupAPI.defaultServer) imageView.image = given(promise.value) { UIImage(data: $0)! } imageView.isHidden = (imageView.image == nil) diff --git a/Session/Settings/SettingsVC.swift b/Session/Settings/SettingsVC.swift index 9641f53dd..ea52be018 100644 --- a/Session/Settings/SettingsVC.swift +++ b/Session/Settings/SettingsVC.swift @@ -1,4 +1,5 @@ import UIKit +import SessionUtilitiesKit final class SettingsVC : BaseVC, AvatarViewHelperDelegate { private var profilePictureToBeUploaded: UIImage? @@ -382,7 +383,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate { DispatchQueue.main.async { modalActivityIndicator.dismiss { var isMaxFileSizeExceeded = false - if let error = error as? FileServerAPIV2.Error { + if let error = error as? HTTP.Error { isMaxFileSizeExceeded = (error == .maxFileSizeExceeded) } let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile" diff --git a/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift index 45f7c1989..9ca228205 100644 --- a/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift +++ b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift @@ -37,9 +37,7 @@ extension FileDownloadResponse { let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) - guard let data = Data(base64Encoded: base64EncodedData) else { - throw FileServerAPIV2.Error.parsingFailed - } + guard let data = Data(base64Encoded: base64EncodedData) else { throw HTTP.Error.parsingFailed } self = FileDownloadResponse( fileName: try container.decode(String.self, forKey: .fileName), diff --git a/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift b/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift index d05a3e251..7ce1c0da7 100644 --- a/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift +++ b/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift @@ -24,9 +24,7 @@ extension LegacyFileDownloadResponse { let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) - guard let data = Data(base64Encoded: base64EncodedData) else { - throw FileServerAPIV2.Error.parsingFailed - } + guard let data = Data(base64Encoded: base64EncodedData) else { throw HTTP.Error.parsingFailed } self = LegacyFileDownloadResponse( data: data diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 81e9d849e..7a1fe0f18 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -8,4 +8,5 @@ enum QueryParam: String { case required = "required" case limit // For messages - number between 1 and 256 (default is 100) + case platform // For file server session version check } diff --git a/SessionMessagingKit/Common Networking/Request.swift b/SessionMessagingKit/Common Networking/Request.swift new file mode 100644 index 000000000..a5dff15a7 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Request.swift @@ -0,0 +1,106 @@ +import Foundation +import SessionUtilitiesKit + +// MARK: - Convenience Types + +struct Empty: Codable {} + +typealias NoBody = Empty +typealias NoResponse = Empty + +protocol EndpointType: Hashable { + var path: String { get } +} + +// MARK: - Request + +struct Request { + let method: HTTP.Verb + let server: String + let endpoint: Endpoint + let queryParameters: [QueryParam: String] + let headers: [Header: String] + /// This is the body value sent during the request + /// + /// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there + /// is custom handling for certain data types + let body: T? + let isAuthRequired: Bool + /// Always `true` under normal circumstances. You might want to disable + /// this when running over Lokinet. + let useOnionRouting: Bool + + // MARK: - Initialization + + init( + method: HTTP.Verb = .get, + server: String, + endpoint: Endpoint, + queryParameters: [QueryParam: String] = [:], + headers: [Header: String] = [:], + body: T? = nil, + isAuthRequired: Bool = true, + useOnionRouting: Bool = true + ) { + self.method = method + self.server = server + self.endpoint = endpoint + self.queryParameters = queryParameters + self.headers = headers + self.body = body + self.isAuthRequired = isAuthRequired + self.useOnionRouting = useOnionRouting + } + + // MARK: - Internal Methods + + private var url: URL? { + return URL(string: "\(server)\(urlPathAndParamsString)") + } + + private func bodyData() throws -> Data? { + // 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 body { + case let bodyString as String: + // The only acceptable string body is a base64 encoded one + guard let encodedData: Data = Data(base64Encoded: bodyString) else { throw HTTP.Error.parsingFailed } + + return encodedData + + case let bodyBytes as [UInt8]: + return Data(bodyBytes) + + default: + // Having no body is fine so just return nil + guard let body: T = body else { return nil } + + return try JSONEncoder().encode(body) + } + } + + // MARK: - Request Generation + + var urlPathAndParamsString: String { + return [ + "/\(endpoint.path)", + queryParameters + .map { key, value in "\(key.rawValue)=\(value)" } + .joined(separator: "&") + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: "?") + } + + func generateUrlRequest() throws -> URLRequest { + guard let url: URL = url else { throw HTTP.Error.invalidURL } + + var urlRequest: URLRequest = URLRequest(url: url) + urlRequest.httpMethod = method.rawValue + urlRequest.allHTTPHeaderFields = headers.toHTTPHeaders() + urlRequest.httpBody = try bodyData() + + return urlRequest + } +} diff --git a/SessionMessagingKit/Database/Storage+Jobs.swift b/SessionMessagingKit/Database/Storage+Jobs.swift index fe3f31615..9928c1831 100644 --- a/SessionMessagingKit/Database/Storage+Jobs.swift +++ b/SessionMessagingKit/Database/Storage+Jobs.swift @@ -1,3 +1,4 @@ +import YapDatabase extension Storage { @@ -96,10 +97,14 @@ extension Storage { public func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? { var result: MessageSendJob? Storage.read { transaction in - result = transaction.object(forKey: messageSendJobID, inCollection: MessageSendJob.collection) as? MessageSendJob + result = self.getMessageSendJob(for: messageSendJobID, using: transaction) } return result } + + public func getMessageSendJob(for messageSendJobID: String, using transaction: Any) -> MessageSendJob? { + return (transaction as! YapDatabaseReadTransaction).object(forKey: messageSendJobID, inCollection: MessageSendJob.collection) as? MessageSendJob + } public func resumeMessageSendJobIfNeeded(_ messageSendJobID: String) { guard let job = getMessageSendJob(for: messageSendJobID) else { return } diff --git a/SessionMessagingKit/File Server/FileServerAPIV2.swift b/SessionMessagingKit/File Server/FileServerAPIV2.swift index 7ea5ea93f..f2bff83ed 100644 --- a/SessionMessagingKit/File Server/FileServerAPIV2.swift +++ b/SessionMessagingKit/File Server/FileServerAPIV2.swift @@ -4,7 +4,8 @@ import SessionSnodeKit @objc(SNFileServerAPIV2) public final class FileServerAPIV2 : NSObject { - // MARK: Settings + // MARK: - Settings + @objc public static let oldServer = "http://88.99.175.227" public static let oldServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69" @objc public static let server = "http://filev2.getsession.org" @@ -18,92 +19,12 @@ public final class FileServerAPIV2 : NSObject { /// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds. public static let fileSizeORMultiplier: Double = 2 - // MARK: Initialization + // MARK: - Initialization + private override init() { } - // MARK: Error - public enum Error: LocalizedError { - case parsingFailed - case invalidURL - case maxFileSizeExceeded - - public var errorDescription: String? { - switch self { - case .parsingFailed: return "Invalid response." - case .invalidURL: return "Invalid URL." - case .maxFileSizeExceeded: return "Maximum file size exceeded." - } - } - } + // MARK: - File Storage - // MARK: Request - private struct Request { - let verb: HTTP.Verb - let endpoint: String - let queryParameters: [QueryParam: String] - let body: Data? - let headers: [Header: String] - /// Always `true` under normal circumstances. You might want to disable - /// this when running over Lokinet. - let useOnionRouting: Bool - - init(verb: HTTP.Verb, endpoint: String, queryParameters: [QueryParam: String] = [:], body: Data? = nil, - headers: [Header: String] = [:], useOnionRouting: Bool = true) { - self.verb = verb - self.endpoint = endpoint - self.queryParameters = queryParameters - self.body = body - self.headers = headers - self.useOnionRouting = useOnionRouting - } - } - - // MARK: - Convenience - - private static func send(_ request: Request, useOldServer: Bool) -> Promise { - let server = useOldServer ? oldServer : server - let serverPublicKey = useOldServer ? oldServerPublicKey : serverPublicKey - var urlRequest: URLRequest - // TODO: Combine this 'Request' with the the pattern in OpenGroupServerV2? - switch request.verb { - case .get: - var rawURL = "\(server)/\(request.endpoint)" - - if !request.queryParameters.isEmpty { - let queryString = request.queryParameters.map { key, value in "\(key)=\(value)" }.joined(separator: "&") - rawURL += "?\(queryString)" - } - - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - - urlRequest = URLRequest(url: url) - - case .post, .put, .delete: - let rawURL = "\(server)/\(request.endpoint)" - - guard let url = URL(string: rawURL) else { return Promise(error: Error.invalidURL) } - - urlRequest = URLRequest(url: url) - urlRequest.httpMethod = request.verb.rawValue - urlRequest.httpBody = request.body - } - - urlRequest.allHTTPHeaderFields = request.headers.toHTTPHeaders() - - guard request.useOnionRouting else { - preconditionFailure("It's currently not allowed to send non onion routed requests.") - } - - // TODO: Upgrade this to use the V4 onion requests once supported. - return OnionRequestAPI.sendOnionRequest(urlRequest, to: server, using: .v3, with: serverPublicKey) - .map2 { _, response in - guard let response: Data = response else { throw Error.parsingFailed } - - return response - } - } - - // MARK: File Storage @objc(upload:) public static func objc_upload(file: Data) -> AnyPromise { return AnyPromise.from(upload(file).map { String($0) }) @@ -112,41 +33,72 @@ public final class FileServerAPIV2 : NSObject { public static func upload(_ file: Data) -> Promise { let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString()) - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Promise(error: HTTP.Error.invalidJSON) - } + let request = Request( + method: .post, + server: server, + endpoint: Endpoint.files, + body: requestBody + ) - let request = Request(verb: .post, endpoint: "files", body: body) - return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in - let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed) - - return response.fileId - } + return send(request, serverPublicKey: serverPublicKey) + .decoded(as: LegacyFileUploadResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed) + .map { response in response.fileId } } @objc(download:useOldServer:) public static func objc_download(file: String, useOldServer: Bool) -> AnyPromise { - guard let id = UInt64(file) else { return AnyPromise.from(Promise(error: Error.invalidURL)) } + guard let id = UInt64(file) else { return AnyPromise.from(Promise(error: HTTP.Error.invalidURL)) } return AnyPromise.from(download(id, useOldServer: useOldServer)) } public static func download(_ file: UInt64, useOldServer: Bool) -> Promise { - let request = Request(verb: .get, endpoint: "files/\(file)") + let serverPublicKey: String = (useOldServer ? oldServerPublicKey : serverPublicKey) + let request = Request( + server: (useOldServer ? oldServer : server), + endpoint: .file(fileId: file) + ) - return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { data in - let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed) - - return response.data - } + return send(request, serverPublicKey: serverPublicKey) + .decoded(as: LegacyFileDownloadResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed) + .map { response in response.data } } public static func getVersion(_ platform: String) -> Promise { - let request = Request(verb: .get, endpoint: "session_version?platform=\(platform)") + let request = Request( + server: server, + endpoint: .sessionVersion, + queryParameters: [ + .platform: platform + ] + ) - return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in - let response: VersionResponse = try data.decoded(as: VersionResponse.self, customError: Error.parsingFailed) - - return response.version + return send(request, serverPublicKey: serverPublicKey) + .decoded(as: VersionResponse.self, on: .global(qos: .userInitiated), error: HTTP.Error.parsingFailed) + .map { response in response.version } + } + + // MARK: - Convenience + + private static func send(_ request: Request, serverPublicKey: String) -> Promise { + guard request.useOnionRouting else { + preconditionFailure("It's currently not allowed to send non onion routed requests.") } + + let urlRequest: URLRequest + + do { + urlRequest = try request.generateUrlRequest() + } + catch { + return Promise(error: error) + } + + // TODO: Rename file to be 'FileServerAPI' (drop the 'V2') + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, with: serverPublicKey) + .map2 { _, response in + guard let response: Data = response else { throw HTTP.Error.parsingFailed } + + return response + } } } diff --git a/SessionMessagingKit/File Server/Types/FSEndpoint.swift b/SessionMessagingKit/File Server/Types/FSEndpoint.swift new file mode 100644 index 000000000..06cdea310 --- /dev/null +++ b/SessionMessagingKit/File Server/Types/FSEndpoint.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension FileServerAPIV2 { + public enum Endpoint: EndpointType { + case files + case file(fileId: UInt64) + case sessionVersion + + var path: String { + switch self { + case .files: return "files" + case .file(let fileId): return "files/\(fileId)" + case .sessionVersion: return "session_version" + } + } + } +} diff --git a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift index 15d821fb7..70cf5bfcb 100644 --- a/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentDownloadJob.swift @@ -98,7 +98,7 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject } } if let tsMessage = TSMessage.fetch(uniqueId: tsMessageID), let openGroup = storage.getOpenGroup(for: tsMessage.uniqueThreadId) { - guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else { + guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let fileId = UInt64(fileAsString) else { return handleFailure(Error.invalidURL) } // TODO: Upgrade this to use the non-legacy version diff --git a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift index 425d1c18c..e069bb481 100644 --- a/SessionMessagingKit/Jobs/AttachmentUploadJob.swift +++ b/SessionMessagingKit/Jobs/AttachmentUploadJob.swift @@ -66,25 +66,33 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N guard let stream = TSAttachment.fetch(uniqueId: attachmentID) as? TSAttachmentStream else { return handleFailure(error: Error.noAttachment) } - guard !stream.isUploaded else { return handleSuccess() } // Should never occur + guard !stream.isUploaded else { return handleSuccess(stream.serverId) } // Should never occur + let storage = SNMessagingKitConfiguration.shared.storage if let openGroup = storage.getOpenGroup(for: threadID) { AttachmentUploadJob.upload( stream, using: { data in - // TODO: Upgrade this to use the non-legacy version - return OpenGroupAPI.legacyUpload(data, to: openGroup.room, on: openGroup.server) + OpenGroupAPI.uploadFile(data.bytes, to: openGroup.room, on: openGroup.server) + .map { _, response -> UInt64 in response.id } }, encrypt: false, - onSuccess: handleSuccess, + onSuccess: { [weak self] fileId in self?.handleSuccess(fileId) }, + onFailure: handleFailure + ) + } + else { + AttachmentUploadJob.upload( + stream, + using: FileServerAPIV2.upload, + encrypt: true, + onSuccess: { [weak self] fileId in self?.handleSuccess(fileId) }, onFailure: handleFailure ) - } else { - AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure) } } - public static func upload(_ stream: TSAttachmentStream, using upload: (Data) -> Promise, encrypt: Bool, onSuccess: (() -> Void)?, onFailure: ((Swift.Error) -> Void)?) { + public static func upload(_ stream: TSAttachmentStream, using upload: (Data) -> Promise, encrypt: Bool, onSuccess: ((UInt64) -> Void)?, onFailure: ((Swift.Error) -> Void)?) { // Get the attachment guard var data = try? stream.readDataFromFile() else { SNLog("Couldn't read attachment from disk.") @@ -105,37 +113,74 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N // Check the file size SNLog("File size: \(data.count) bytes.") if Double(data.count) > Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier { - onFailure?(FileServerAPIV2.Error.maxFileSizeExceeded); return + onFailure?(HTTP.Error.maxFileSizeExceeded) + return } + // Send the request stream.isUploaded = false stream.save() - upload(data).done(on: DispatchQueue.global(qos: .userInitiated)) { fileID in - let downloadURL = "\(FileServerAPIV2.server)/files/\(fileID)" - stream.serverId = fileID + upload(data).done(on: DispatchQueue.global(qos: .userInitiated)) { fileId in + let downloadURL = "\(FileServerAPIV2.server)/files/\(fileId)" + stream.serverId = fileId stream.isUploaded = true stream.downloadURL = downloadURL stream.save() - onSuccess?() + onSuccess?(fileId) }.catch { error in onFailure?(error) } } - private func handleSuccess() { + private func handleSuccess(_ fileId: UInt64) { SNLog("Attachment uploaded successfully.") delegate?.handleJobSucceeded(self) - SNMessagingKitConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobID) - Storage.shared.write(with: { transaction in - var message: TSMessage? - let transaction = transaction as! YapDatabaseReadWriteTransaction - TSDatabaseSecondaryIndexes.enumerateMessages(withTimestamp: self.message.sentTimestamp!, with: { _, key, _ in - message = TSMessage.fetch(uniqueId: key, transaction: transaction) - }, using: transaction) - if let message = message { - MessageInvalidator.invalidate(message, with: transaction) + + let messageSendJobId: String = messageSendJobID + + Storage.shared.write( + with: { transaction in + // Get the existing MessageSendJob and replace it with one that has it's destination updated + // to include the returned fileId + if let oldJob: MessageSendJob = SNMessagingKitConfiguration.shared.storage.getMessageSendJob(for: messageSendJobId, using: transaction) { + switch oldJob.destination { + case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, let oldFileIds): + let job: MessageSendJob = MessageSendJob( + message: oldJob.message, + destination: .openGroup( + roomToken: roomToken, + server: server, + whisperTo: whisperTo, + whisperMods: whisperMods, + fileIds: (oldFileIds ?? []) + [fileId] + ) + ) + job.id = oldJob.id // Use the existing id so it gets overwritten + job.delegate = oldJob.delegate + job.failureCount = oldJob.failureCount + + // This method just writes the job directly and doesn't generate a new id (as we want) + SNMessagingKitConfiguration.shared.storage.persist(job, using: transaction) + + default: break + } + } + }, + completion: { + SNMessagingKitConfiguration.shared.storage.resumeMessageSendJobIfNeeded(messageSendJobId) + + Storage.shared.write(with: { transaction in + var message: TSMessage? + let transaction = transaction as! YapDatabaseReadWriteTransaction + TSDatabaseSecondaryIndexes.enumerateMessages(withTimestamp: self.message.sentTimestamp!, with: { _, key, _ in + message = TSMessage.fetch(uniqueId: key, transaction: transaction) + }, using: transaction) + if let message = message { + MessageInvalidator.invalidate(message, with: transaction) + } + }, completion: { }) } - }, completion: { }) + ) } private func handlePermanentFailure(error: Swift.Error) { diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index 0d6d11070..e5a877618 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -6,7 +6,7 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi public let message: Message public let destination: Message.Destination public var delegate: JobDelegate? - public var id: String? + public var id: String? // This should only get set in either `JobQueue` or `AttachmentUploadJob` public var failureCount: UInt = 0 // MARK: - Settings @@ -100,7 +100,7 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi .replacingOccurrences(of: "]", with: "") .split(separator: "|") .map { String($0) } - let fileIds: [Int64]? = (fileIdStrings.isEmpty ? nil : fileIdStrings.compactMap { Int64($0) }) + let fileIds: [UInt64]? = (fileIdStrings.isEmpty ? nil : fileIdStrings.compactMap { UInt64($0) }) destination = .openGroup( roomToken: roomToken, diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index 8df03ee80..34f99ca25 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -10,11 +10,11 @@ public extension Message { server: String, whisperTo: String? = nil, whisperMods: Bool = false, - fileIds: [Int64]? = nil // TODO: Handle 'fileIds' + fileIds: [UInt64]? = nil ) case openGroupInbox(server: String, openGroupPublicKey: String, blindedPublicKey: String) - static func from(_ thread: TSThread) -> Message.Destination { + static func from(_ thread: TSThread, fileIds: [UInt64]? = nil) -> Message.Destination { if let thread = thread as? TSContactThread { if SessionId.Prefix(from: thread.contactSessionID()) == .blinded { guard let server: String = thread.originalOpenGroupServer, let publicKey: String = thread.originalOpenGroupPublicKey else { @@ -40,7 +40,7 @@ public extension Message { if let thread = thread as? TSGroupThread, thread.isOpenGroup { let openGroup: OpenGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)! - return .openGroup(roomToken: openGroup.room, server: openGroup.server) + return .openGroup(roomToken: openGroup.room, server: openGroup.server, fileIds: fileIds) } preconditionFailure("TODO: Handle legacy closed groups.") diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift index ef789bebc..2d961910c 100644 --- a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift @@ -28,7 +28,7 @@ extension OpenGroupAPI { private let b64: String? private let bytes: [UInt8]? - init(request: Request) { + init(request: Request) { self.method = request.method self.path = request.urlPathAndParamsString self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders()) @@ -86,17 +86,17 @@ extension OpenGroupAPI { // MARK: - BatchRequestInfo struct BatchRequestInfo: BatchRequestInfoType { - let request: Request + let request: Request let responseType: Codable.Type var endpoint: Endpoint { request.endpoint } - init(request: Request, responseType: R.Type) { + init(request: Request, responseType: R.Type) { self.request = request self.responseType = BatchSubResponse.self } - init(request: Request) { + init(request: Request) { self.init( request: request, responseType: NoResponse.self @@ -124,7 +124,7 @@ extension OpenGroupAPI.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 != OpenGroupAPI.NoResponse.self && !(T.self is ExpressibleByNilLiteral.Type)) + failedToParseBody: (body == nil && T.self != NoResponse.self && !(T.self is ExpressibleByNilLiteral.Type)) ) } } @@ -143,31 +143,31 @@ protocol BatchRequestInfoType { // MARK: - Convenience public extension Decodable { - static func decoded(from data: Data, customError: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self { - return try data.decoded(as: Self.self, customError: customError, using: dependencies) + static func decoded(from data: Data, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) throws -> Self { + return try data.decoded(as: Self.self, using: dependencies) } } extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { + func decoded(as types: OpenGroupAPI.BatchResponseTypes, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise { self.map(on: queue) { responseInfo, maybeData -> OpenGroupAPI.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 OpenGroupAPI.Error.parsingFailed } + guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } - guard let anyArray: [Any] = jsonObject as? [Any] else { throw OpenGroupAPI.Error.parsingFailed } + guard let anyArray: [Any] = jsonObject as? [Any] else { throw HTTP.Error.parsingFailed } let dataArray: [Data] = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) } - guard dataArray.count == types.count else { throw OpenGroupAPI.Error.parsingFailed } + guard dataArray.count == types.count else { throw HTTP.Error.parsingFailed } do { return try zip(dataArray, types) - .map { data, type in try type.decoded(from: data, customError: error, using: dependencies) } + .map { data, type in try type.decoded(from: data, using: dependencies) } .map { data in (responseInfo, data) } } - catch _ { - throw error + catch { + throw HTTP.Error.parsingFailed } } } diff --git a/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift index 1f02b8ac8..d15f21cc8 100644 --- a/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyAuthTokenResponse.swift @@ -28,7 +28,7 @@ extension OpenGroupAPI.LegacyAuthTokenResponse.Challenge { let base64EncodedEphemeralPublicKey: String = try container.decode(String.self, forKey: .ephemeralPublicKey) guard let ciphertext = Data(base64Encoded: base64EncodedCiphertext), let ephemeralPublicKey = Data(base64Encoded: base64EncodedEphemeralPublicKey) else { - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } self = OpenGroupAPI.LegacyAuthTokenResponse.Challenge( diff --git a/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift index 3c87036e0..17b0d5565 100644 --- a/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyOpenGroupMessageV2.swift @@ -49,7 +49,7 @@ extension LegacyOpenGroupMessageV2 { // Validate the message signature guard let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } let publicKey = Data(hex: sender.removingIdPrefixIfNeeded()) @@ -57,7 +57,7 @@ extension LegacyOpenGroupMessageV2 { guard isValid else { SNLog("Ignoring message with invalid signature.") - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } self = LegacyOpenGroupMessageV2( diff --git a/SessionMessagingKit/Open Groups/Models/Room.swift b/SessionMessagingKit/Open Groups/Models/Room.swift index 2a31baabe..06417848b 100644 --- a/SessionMessagingKit/Open Groups/Models/Room.swift +++ b/SessionMessagingKit/Open Groups/Models/Room.swift @@ -70,7 +70,7 @@ extension OpenGroupAPI { /// File ID of an uploaded file containing the room's image /// /// Omitted if there is no image - public let imageId: Int64? + public let imageId: UInt64? /// Array of pinned message information (omitted entirely if there are no pinned messages) public let pinnedMessages: [PinnedMessage]? @@ -160,7 +160,7 @@ extension OpenGroupAPI.Room { activeUsers: try container.decode(Int64.self, forKey: .activeUsers), activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff), - imageId: try? container.decode(Int64.self, forKey: .imageId), + imageId: try? container.decode(UInt64.self, forKey: .imageId), pinnedMessages: try? container.decode([OpenGroupAPI.PinnedMessage].self, forKey: .pinnedMessages), admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false), diff --git a/SessionMessagingKit/Open Groups/Models/OGMessage.swift b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift similarity index 92% rename from SessionMessagingKit/Open Groups/Models/OGMessage.swift rename to SessionMessagingKit/Open Groups/Models/SOGSMessage.swift index aba49f877..c50620960 100644 --- a/SessionMessagingKit/Open Groups/Models/OGMessage.swift +++ b/SessionMessagingKit/Open Groups/Models/SOGSMessage.swift @@ -45,10 +45,10 @@ extension OpenGroupAPI.Message { // If we have data and a signature (ie. the message isn't a deletion) then validate the signature if let base64EncodedData: String = maybeBase64EncodedData, let base64EncodedSignature: String = maybeBase64EncodedSignature { guard let sender: String = maybeSender, let data = Data(base64Encoded: base64EncodedData), let signature = Data(base64Encoded: base64EncodedSignature) else { - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } guard let dependencies: OpenGroupAPI.Dependencies = decoder.userInfo[OpenGroupAPI.Dependencies.userInfoKey] as? OpenGroupAPI.Dependencies else { - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } // Verify the signature based on the SessionId.Prefix type @@ -58,18 +58,18 @@ extension OpenGroupAPI.Message { case .blinded: guard dependencies.sign.verify(message: data.bytes, publicKey: publicKey.bytes, signature: signature.bytes) else { SNLog("Ignoring message with invalid signature.") - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } case .standard, .unblinded: guard (try? dependencies.ed25519.verifySignature(signature, publicKey: publicKey, data: data)) == true else { SNLog("Ignoring message with invalid signature.") - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } case .none: SNLog("Ignoring message with invalid sender.") - throw OpenGroupAPI.Error.parsingFailed + throw HTTP.Error.parsingFailed } } diff --git a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift index 9a53c14d6..734f07d2a 100644 --- a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift +++ b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift @@ -40,7 +40,7 @@ extension OpenGroupAPI { /// /// When submitting a message edit this field must contain the IDs of any newly uploaded files that are part of the edit; existing /// attachment IDs may also be included, but are not required - let fileIds: [Int64]? + let fileIds: [UInt64]? // MARK: - Initialization @@ -49,7 +49,7 @@ extension OpenGroupAPI { signature: Data, whisperTo: String? = nil, whisperMods: Bool? = nil, - fileIds: [Int64]? = nil + fileIds: [UInt64]? = nil ) { self.data = data self.signature = signature diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index 9a27e00d2..3c08c416b 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -59,7 +59,7 @@ public final class OpenGroupAPI: NSObject { // Generate the requests let requestResponseType: [BatchRequestInfoType] = [ BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: .capabilities ), @@ -85,14 +85,14 @@ public final class OpenGroupAPI: NSObject { return [ BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: .roomPollInfo(openGroup.room, openGroup.infoUpdates) ), responseType: RoomPollInfo.self ), BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: (shouldRetrieveRecentMessages ? .roomMessagesRecent(openGroup.room) : @@ -107,7 +107,7 @@ public final class OpenGroupAPI: NSObject { .appending([ // Inbox BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: (maybeLastInboxMessageId == nil ? .inbox : @@ -119,7 +119,7 @@ public final class OpenGroupAPI: NSObject { // Outbox BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: (maybeLastOutboxMessageId == nil ? .outbox : @@ -146,12 +146,12 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .batch, + endpoint: Endpoint.batch, body: requestBody ) return send(request, using: dependencies) - .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies) .map { result in result.enumerated() .reduce(into: [:]) { prev, next in @@ -176,13 +176,13 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .sequence, + endpoint: Endpoint.sequence, body: requestBody ) // TODO: Handle a `412` response (ie. a required capability isn't supported) return send(request, using: dependencies) - .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: responseTypes, on: OpenGroupAPI.workQueue, using: dependencies) .map { result in result.enumerated() .reduce(into: [:]) { prev, next in @@ -201,7 +201,7 @@ public final class OpenGroupAPI: NSObject { /// Eg. `GET /capabilities` could return `{"capabilities": ["sogs", "batch"]}` `GET /capabilities?required=magic,batch` /// could return: `{"capabilities": ["sogs", "batch"], "missing": ["magic"]}` public static func capabilities(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Capabilities)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .capabilities, queryParameters: [:] // TODO: Add any requirements '.required'. @@ -209,7 +209,7 @@ public final class OpenGroupAPI: NSObject { // TODO: Handle a `412` response (ie. a required capability isn't supported) return send(request, using: dependencies) - .decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: Capabilities.self, on: OpenGroupAPI.workQueue, using: dependencies) } // MARK: - Room @@ -218,13 +218,13 @@ public final class OpenGroupAPI: NSObject { /// /// Rooms to which the user does not have access (e.g. because they are banned, or the room has restricted access permissions) are not included public static func rooms(for server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Room])> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .rooms ) return send(request, using: dependencies) - .decoded(as: [Room].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [Room].self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Returns the details of a single room @@ -234,13 +234,13 @@ public final class OpenGroupAPI: NSObject { /// method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func room(for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Room)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .room(roomToken) ) return send(request, using: dependencies) - .decoded(as: Room.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: Room.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Polls a room for metadata updates @@ -253,13 +253,13 @@ public final class OpenGroupAPI: NSObject { /// method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, RoomPollInfo)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .roomPollInfo(roomToken, lastUpdated) ) return send(request, using: dependencies) - .decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: RoomPollInfo.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// This is a convenience method which constructs a `/sequence` of the `capabilities` and `room` requests, refer to those @@ -272,7 +272,7 @@ public final class OpenGroupAPI: NSObject { let requestResponseType: [BatchRequestInfoType] = [ // Get the latest capabilities for the server (in case it's a new server or the cached ones are stale) BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: .capabilities ), @@ -281,7 +281,7 @@ public final class OpenGroupAPI: NSObject { // And the room info BatchRequestInfo( - request: Request( + request: Request( server: server, endpoint: .room(roomToken) ), @@ -298,14 +298,14 @@ public final class OpenGroupAPI: NSObject { switch endpoint { case .capabilities: guard let responseData: BatchSubResponse = endpointResponse.data as? BatchSubResponse, let responseBody: Capabilities = responseData.body else { - throw Error.parsingFailed + throw HTTP.Error.parsingFailed } capabilities = (endpointResponse.info, responseBody) case .room: guard let responseData: OpenGroupAPI.BatchSubResponse = endpointResponse.data as? OpenGroupAPI.BatchSubResponse, let responseBody: OpenGroupAPI.Room = responseData.body else { - throw Error.parsingFailed + throw HTTP.Error.parsingFailed } room = (endpointResponse.info, responseBody) @@ -315,7 +315,7 @@ public final class OpenGroupAPI: NSObject { } guard let capabilities: (OnionRequestResponseInfoType, Capabilities?) = capabilities, let room: (OnionRequestResponseInfoType, Room?) = room else { - throw Error.parsingFailed + throw HTTP.Error.parsingFailed } return (capabilities, room) @@ -331,6 +331,7 @@ public final class OpenGroupAPI: NSObject { on server: String, whisperTo: String?, whisperMods: Bool, + fileIds: [UInt64]?, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Message)> { guard let signResult: (publicKey: String, signature: Bytes) = sign(plaintext.bytes, for: server, using: dependencies) else { @@ -342,18 +343,18 @@ public final class OpenGroupAPI: NSObject { signature: Data(signResult.signature), whisperTo: whisperTo, whisperMods: whisperMods, - fileIds: nil // TODO: Add support for 'fileIds'. + fileIds: fileIds ) let request = Request( method: .post, server: server, - endpoint: .roomMessage(roomToken), + endpoint: Endpoint.roomMessage(roomToken), body: requestBody ) return send(request, using: dependencies) - .decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: Message.self, on: OpenGroupAPI.workQueue, using: dependencies) .map { response, message in // Store the 'message.posted' timestamp to prevent the sent message getting duplicated when it is later retrieved dependencies.storage.write { transaction in @@ -367,13 +368,13 @@ public final class OpenGroupAPI: NSObject { /// Returns a single message by ID public static func message(_ id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Message)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .roomMessageIndividual(roomToken, id: id) ) return send(request, using: dependencies) - .decoded(as: Message.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: Message.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Edits a message, replacing its existing content with new content and a new signature @@ -400,7 +401,7 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .put, server: server, - endpoint: .roomMessageIndividual(roomToken, id: id), + endpoint: Endpoint.roomMessageIndividual(roomToken, id: id), body: requestBody ) @@ -414,7 +415,7 @@ public final class OpenGroupAPI: NSObject { on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Data?)> { - let request: Request = Request( + let request: Request = Request( method: .delete, server: server, endpoint: .roomMessageIndividual(roomToken, id: id) @@ -428,7 +429,7 @@ public final class OpenGroupAPI: NSObject { /// method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func recentMessages(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .roomMessagesRecent(roomToken) // TODO: Limit?. @@ -436,7 +437,7 @@ public final class OpenGroupAPI: NSObject { ) return send(request, using: dependencies) - .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies) } /// **Note:** This is the direct request to retrieve recent messages before a given message and is currently unused, in order to call this directly @@ -445,7 +446,7 @@ public final class OpenGroupAPI: NSObject { @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { // TODO: Do we need to be able to load old messages? - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .roomMessagesBefore(roomToken, id: messageId) // TODO: Limit?. @@ -453,7 +454,7 @@ public final class OpenGroupAPI: NSObject { ) return send(request, using: dependencies) - .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies) } /// **Note:** This is the direct request to retrieve messages since a given message `seqNo` so should be retrieved automatically from the @@ -461,7 +462,7 @@ public final class OpenGroupAPI: NSObject { /// `OpenGroupManager.handleMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [Message])> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) // TODO: Limit?. @@ -469,7 +470,7 @@ public final class OpenGroupAPI: NSObject { ) return send(request, using: dependencies) - .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [Message].self, on: OpenGroupAPI.workQueue, using: dependencies) } // MARK: - Pinning @@ -485,7 +486,7 @@ public final class OpenGroupAPI: NSObject { /// messages are returned in pinning-order this allows admins to order multiple pinned messages in a room by re-pinning (via this endpoint) in the /// order in which pinned messages should be displayed public static func pinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { - let request: Request = Request( + let request: Request = Request( method: .post, server: server, endpoint: .roomPinMessage(roomToken, id: id) @@ -499,7 +500,7 @@ public final class OpenGroupAPI: NSObject { /// /// The user must have `admin` (not just `moderator`) permissions in the room public static func unpinMessage(id: Int64, in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { - let request: Request = Request( + let request: Request = Request( method: .post, server: server, endpoint: .roomUnpinMessage(roomToken, id: id) @@ -513,7 +514,7 @@ public final class OpenGroupAPI: NSObject { /// /// The user must have `admin` (not just `moderator`) permissions in the room public static func unpinAll(in roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise { - let request: Request = Request( + let request: Request = Request( method: .post, server: server, endpoint: .roomUnpinAll(roomToken) @@ -529,7 +530,7 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .roomFile(roomToken), + endpoint: Endpoint.roomFile(roomToken), headers: [ .contentDisposition: [ "attachment", fileName.map { "filename=\"\($0)\"" } ] .compactMap{ $0 } @@ -540,50 +541,31 @@ public final class OpenGroupAPI: NSObject { ) return send(request, using: dependencies) - .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } - /// Warning: This approach is less efficient as it expects the data to be base64Encoded (with is 33% larger than binary), please use the binary approach - /// whenever possible - public static func uploadFile(_ base64EncodedString: String, fileName: String? = nil, to roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileUploadResponse)> { - let request: Request = Request( - method: .post, - server: server, - endpoint: .roomFileJson(roomToken), - headers: [ - .contentDisposition: [ "attachment", fileName.map { "filename=\"\($0)\"" } ] - .compactMap{ $0 } - .joined(separator: "; "), - ], - body: base64EncodedString - ) - - return send(request, using: dependencies) - .decoded(as: FileUploadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) - } - - public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> { - let request: Request = Request( + public static func downloadFile(_ fileId: UInt64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data)> { + let request: Request = Request( server: server, endpoint: .roomFileIndividual(roomToken, fileId) ) return send(request, using: dependencies) .map { responseInfo, maybeData in - guard let data: Data = maybeData else { throw Error.parsingFailed } + guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } return (responseInfo, data) } } - public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> { - let request: Request = Request( + public static func downloadFileJson(_ fileId: UInt64, from roomToken: String, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, FileDownloadResponse)> { + let request: Request = Request( server: server, endpoint: .roomFileIndividualJson(roomToken, fileId) ) // TODO: This endpoint is getting rewritten to return just data (properties would come through as headers). return send(request, using: dependencies) - .decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: FileDownloadResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } // MARK: - Inbox/Outbox (Message Requests) @@ -595,13 +577,13 @@ public final class OpenGroupAPI: NSObject { /// `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func inbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .inbox ) return send(request, using: dependencies) - .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Polls for any DMs received since the given id, this method will return a `304` with an empty response if there are no messages @@ -611,13 +593,13 @@ public final class OpenGroupAPI: NSObject { /// of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func inboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .inboxSince(id: id) ) return send(request, using: dependencies) - .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Delivers a direct message to a user via their blinded Session ID @@ -631,12 +613,12 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .inboxFor(sessionId: blindedSessionId), + endpoint: Endpoint.inboxFor(sessionId: blindedSessionId), body: requestBody ) return send(request, using: dependencies) - .decoded(as: SendDirectMessageResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: SendDirectMessageResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) .map { response, message in // Store the 'message.posted' timestamp to prevent the sent message getting duplicated when it is later retrieved dependencies.storage.write { transaction in @@ -655,13 +637,13 @@ public final class OpenGroupAPI: NSObject { /// this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func outbox(on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .outbox ) return send(request, using: dependencies) - .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies) } /// Polls for any DMs sent since the given id, this method will return a `304` with an empty response if there are no messages @@ -671,13 +653,13 @@ public final class OpenGroupAPI: NSObject { /// to route the response of this method to the `OpenGroupManager.handleDirectMessages` method to ensure things are processed correctly @available(*, unavailable, message: "Avoid using this directly, use the pre-built `poll()` method instead") public static func outboxSince(id: Int64, on server: String, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, [DirectMessage]?)> { - let request: Request = Request( + let request: Request = Request( server: server, endpoint: .outboxSince(id: id) ) return send(request, using: dependencies) - .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: [DirectMessage]?.self, on: OpenGroupAPI.workQueue, using: dependencies) } // MARK: - Users @@ -729,7 +711,7 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .userBan(sessionId), + endpoint: Endpoint.userBan(sessionId), body: requestBody ) @@ -774,7 +756,7 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .userUnban(sessionId), + endpoint: Endpoint.userUnban(sessionId), body: requestBody ) @@ -841,7 +823,9 @@ public final class OpenGroupAPI: NSObject { on server: String, using dependencies: Dependencies = Dependencies() ) -> Promise<(OnionRequestResponseInfoType, Data?)> { - guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { return Promise(error: Error.generic) } + guard (moderator != nil && admin == nil) || (moderator == nil && admin != nil) else { + return Promise(error: HTTP.Error.generic) + } let requestBody: UserModeratorRequest = UserModeratorRequest( rooms: roomTokens, @@ -854,7 +838,7 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .userModerator(sessionId), + endpoint: Endpoint.userModerator(sessionId), body: requestBody ) @@ -894,12 +878,12 @@ public final class OpenGroupAPI: NSObject { let request: Request = Request( method: .post, server: server, - endpoint: .userDeleteMessages(sessionId), + endpoint: Endpoint.userDeleteMessages(sessionId), body: requestBody ) return send(request, using: dependencies) - .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, error: Error.parsingFailed, using: dependencies) + .decoded(as: UserDeleteMessagesResponse.self, on: OpenGroupAPI.workQueue, using: dependencies) } // TODO: Need to test this once the API has been implemented @@ -1003,11 +987,7 @@ public final class OpenGroupAPI: NSObject { /// Get a hash of any body content let bodyHash: Bytes? = { - // Note: We need the `!body.isEmpty` check because of the default `Data()` value when trying to - // init data from the httpBodyStream - guard let body: Data = (request.httpBody ?? request.httpBodyStream.map { ((try? Data(from: $0)) ?? Data()) }), !body.isEmpty else { - return nil - } + guard let body: Data = request.httpBody else { return nil } return dependencies.genericHash.hash(message: body.bytes, outputLength: 64) }() @@ -1045,20 +1025,14 @@ public final class OpenGroupAPI: NSObject { // MARK: - Convenience - private static func send(_ request: Request, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { - guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } - - var urlRequest: URLRequest = URLRequest(url: url) - urlRequest.httpMethod = request.method.rawValue - urlRequest.allHTTPHeaderFields = request.headers - .setting(.room, request.room) // TODO: Is this needed anymore? Add at the request level?. - .toHTTPHeaders() + private static func send(_ request: Request, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, Data?)> { + let urlRequest: URLRequest do { - urlRequest.httpBody = try request.bodyData() + urlRequest = try request.generateUrlRequest() } catch { - return Promise(error: Error.parsingFailed) + return Promise(error: error) } if request.useOnionRouting { diff --git a/SessionMessagingKit/Open Groups/OpenGroupManager.swift b/SessionMessagingKit/Open Groups/OpenGroupManager.swift index 1726c15b8..d23ae1edd 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManager.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManager.swift @@ -46,7 +46,7 @@ public final class OpenGroupManager: NSObject { let groupId: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData("\(server).\(roomToken)") if OpenGroupManager.shared.pollers[server] != nil && TSGroupThread.fetch(uniqueId: TSGroupThread.threadId(fromGroupId: groupId), transaction: transaction) != nil { - SNLog("Ignoring join open group attempt, user initiated: \(!isConfigMessage)") + SNLog("Ignoring join open group attempt (already joined), user initiated: \(!isConfigMessage)") return Promise.value(()) } @@ -63,7 +63,7 @@ public final class OpenGroupManager: NSObject { .done(on: DispatchQueue.global(qos: .userInitiated)) { (capabilitiesResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Capabilities?), roomResponse: (info: OnionRequestResponseInfoType, data: OpenGroupAPI.Room?)) in guard let capabilities: OpenGroupAPI.Capabilities = capabilitiesResponse.data, let room: OpenGroupAPI.Room = roomResponse.data else { SNLog("Failed to join open group due to invalid data.") - seal.reject(OpenGroupAPI.Error.generic) + seal.reject(HTTP.Error.generic) return } @@ -254,7 +254,7 @@ public final class OpenGroupManager: NSObject { } // - Room image (if there is one) - if let imageId: Int64 = pollInfo.details?.imageId { + if let imageId: UInt64 = pollInfo.details?.imageId { OpenGroupManager.roomImage(imageId, for: roomToken, on: server) .done(on: DispatchQueue.global(qos: .userInitiated)) { data in dependencies.storage.write { transaction in @@ -493,8 +493,8 @@ public final class OpenGroupManager: NSObject { OpenGroupManager.defaultRoomsPromise? .done(on: OpenGroupAPI.workQueue) { items in items - .compactMap { room -> (Int64, String)? in - guard let imageId: Int64 = room.imageId else { return nil} + .compactMap { room -> (UInt64, String)? in + guard let imageId: UInt64 = room.imageId else { return nil} return (imageId, room.token) } @@ -511,7 +511,7 @@ public final class OpenGroupManager: NSObject { } public static func roomImage( - _ fileId: Int64, + _ fileId: UInt64, for roomToken: String, on server: String, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies() diff --git a/SessionMessagingKit/Open Groups/Types/Request.swift b/SessionMessagingKit/Open Groups/Types/Request.swift deleted file mode 100644 index 9c0efe01c..000000000 --- a/SessionMessagingKit/Open Groups/Types/Request.swift +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. - -import Foundation -import SessionUtilitiesKit - -extension OpenGroupAPI { - struct Empty: Codable {} - - typealias NoBody = Empty - typealias NoResponse = Empty - - struct Request { - let method: HTTP.Verb - let server: String - let room: String? // TODO: Remove this? - let endpoint: Endpoint - let queryParameters: [QueryParam: String] - let headers: [Header: String] - /// This is the body value sent during the request - /// - /// **Warning:** The `bodyData` value should be used to when making the actual request instead of this as there - /// is custom handling for certain data types - let body: T? - let isAuthRequired: Bool - /// Always `true` under normal circumstances. You might want to disable - /// this when running over Lokinet. - let useOnionRouting: Bool - - // MARK: - Initialization - - init( - method: HTTP.Verb = .get, - server: String, - room: String? = nil, - endpoint: Endpoint, - queryParameters: [QueryParam: String] = [:], - headers: [Header: String] = [:], - body: T? = nil, - isAuthRequired: Bool = true, - useOnionRouting: Bool = true - ) { - self.method = method - self.server = server - self.room = room - self.endpoint = endpoint - self.queryParameters = queryParameters - self.headers = headers - self.body = body - self.isAuthRequired = isAuthRequired - self.useOnionRouting = useOnionRouting - } - - // MARK: - Convenience - - var url: URL? { - return URL(string: "\(server)\(urlPathAndParamsString)") - } - - var urlPathAndParamsString: String { - return [ - "/\(endpoint.path)", - queryParameters - .map { key, value in "\(key.rawValue)=\(value)" } - .joined(separator: "&") - ] - .compactMap { $0 } - .filter { !$0.isEmpty } - .joined(separator: "?") - } - - func bodyData() throws -> Data? { - // 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 body { - case let bodyString as String: - // The only acceptable string body is a base64 encoded one - guard let encodedData: Data = Data(base64Encoded: bodyString) else { - throw OpenGroupAPI.Error.parsingFailed - } - - return encodedData - - case let bodyBytes as [UInt8]: - return Data(bodyBytes) - - default: - // Having no body is fine so just return nil - guard let body: T = body else { return nil } - - return try JSONEncoder().encode(body) - } - } - } -} diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift similarity index 97% rename from SessionMessagingKit/Open Groups/Types/Endpoint.swift rename to SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift index 9149aec5b..e8019e354 100644 --- a/SessionMessagingKit/Open Groups/Types/Endpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSEndpoint.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPI { - public enum Endpoint: Hashable { + public enum Endpoint: EndpointType { // Utility case onion @@ -34,9 +34,8 @@ extension OpenGroupAPI { // Files case roomFile(String) - case roomFileJson(String) - case roomFileIndividual(String, Int64) - case roomFileIndividualJson(String, Int64) + case roomFileIndividual(String, UInt64) + case roomFileIndividualJson(String, UInt64) // Inbox/Outbox (Message Requests) @@ -126,7 +125,6 @@ extension OpenGroupAPI { // Files case .roomFile(let roomToken): return "room/\(roomToken)/file" - case .roomFileJson(let roomToken): return "room/\(roomToken)/fileJSON" case .roomFileIndividual(let roomToken, let fileId): // Note: The 'fileName' value is ignored by the server and is only used to distinguish // this from the 'Json' variant diff --git a/SessionMessagingKit/Open Groups/Types/Error.swift b/SessionMessagingKit/Open Groups/Types/SOGSError.swift similarity index 69% rename from SessionMessagingKit/Open Groups/Types/Error.swift rename to SessionMessagingKit/Open Groups/Types/SOGSError.swift index 87eb8b255..2d1b7e660 100644 --- a/SessionMessagingKit/Open Groups/Types/Error.swift +++ b/SessionMessagingKit/Open Groups/Types/SOGSError.swift @@ -4,20 +4,14 @@ import Foundation extension OpenGroupAPI { public enum Error: LocalizedError { - case generic - case parsingFailed case decryptionFailed case signingFailed - case invalidURL case noPublicKey public var errorDescription: String? { switch self { - case .generic: return "An error occurred." - case .parsingFailed: return "Invalid response." case .decryptionFailed: return "Couldn't decrypt response." case .signingFailed: return "Couldn't sign message." - case .invalidURL: return "Invalid URL." case .noPublicKey: return "Couldn't find server public key." } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index d547cbeea..073a274f6 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -379,7 +379,7 @@ public final class MessageSender : NSObject { // Send the result - guard case .openGroup(let room, let server, let whisperTo, let whisperMods, _) = destination else { + guard case .openGroup(let room, let server, let whisperTo, let whisperMods, let fileIds) = destination else { preconditionFailure() } @@ -389,7 +389,8 @@ public final class MessageSender : NSObject { to: room, on: server, whisperTo: whisperTo, - whisperMods: whisperMods + whisperMods: whisperMods, + fileIds: fileIds ) .done(on: DispatchQueue.global(qos: .userInitiated)) { responseInfo, data in message.openGroupServerMessageID = UInt64(data.id) diff --git a/SessionMessagingKit/Storage.swift b/SessionMessagingKit/Storage.swift index bd37cdc49..92d3d12c9 100644 --- a/SessionMessagingKit/Storage.swift +++ b/SessionMessagingKit/Storage.swift @@ -45,6 +45,7 @@ public protocol SessionMessagingKitStorageProtocol { func getAllPendingJobs(of type: Job.Type) -> [Job] func getAttachmentUploadJob(for attachmentID: String) -> AttachmentUploadJob? func getMessageSendJob(for messageSendJobID: String) -> MessageSendJob? + func getMessageSendJob(for messageSendJobID: String, using transaction: Any) -> MessageSendJob? func resumeMessageSendJobIfNeeded(_ messageSendJobID: String) func isJobCanceled(_ job: Job) -> Bool diff --git a/SessionMessagingKit/Utilities/Promise+Utilities.swift b/SessionMessagingKit/Utilities/Promise+Utilities.swift index 91d25c937..40278446a 100644 --- a/SessionMessagingKit/Utilities/Promise+Utilities.swift +++ b/SessionMessagingKit/Utilities/Promise+Utilities.swift @@ -13,13 +13,16 @@ extension Promise where T == Data { } extension Promise where T == (OnionRequestResponseInfoType, Data?) { - func decoded(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> { + func decoded(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: OpenGroupAPI.Dependencies = OpenGroupAPI.Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> { self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in - guard let data: Data = maybeData else { - throw OpenGroupAPI.Error.parsingFailed - } + guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed } - return (responseInfo, try data.decoded(as: type, customError: error, using: dependencies)) + do { + return (responseInfo, try data.decoded(as: type, using: dependencies)) + } + catch { + throw HTTP.Error.parsingFailed + } } } } diff --git a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift index 31dd50c17..e846d8b23 100644 --- a/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift +++ b/SessionMessagingKitTests/Open Groups/OpenGroupAPISpec.swift @@ -252,7 +252,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -272,7 +272,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -292,7 +292,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -312,7 +312,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -364,7 +364,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -413,7 +413,7 @@ class OpenGroupAPISpec: QuickSpec { expect(error?.localizedDescription) .toEventually( - equal(OpenGroupAPI.Error.parsingFailed.localizedDescription), + equal(HTTP.Error.parsingFailed.localizedDescription), timeout: .milliseconds(100) ) @@ -449,7 +449,8 @@ class OpenGroupAPISpec: QuickSpec { expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) expect(requestData?.httpMethod).to(equal("POST")) expect(requestData?.headers).to(haveCount(4)) - expect(requestData?.headers.keys).toNot(contain(Header.fileName.rawValue)) + expect(requestData?.headers[Header.contentDisposition.rawValue]) + .toNot(contain("filename")) } it("adds a fileName header when provided") { @@ -477,7 +478,7 @@ class OpenGroupAPISpec: QuickSpec { expect(requestData?.urlString).to(equal("testServer/room/testRoom/file")) expect(requestData?.httpMethod).to(equal("POST")) expect(requestData?.headers).to(haveCount(5)) - expect(requestData?.headers[Header.fileName.rawValue]).to(equal("TestFileName")) + expect(requestData?.headers[Header.contentDisposition.rawValue]).to(contain("TestFileName")) } } diff --git a/SessionSnodeKit/OnionRequestAPI+Encryption.swift b/SessionSnodeKit/OnionRequestAPI+Encryption.swift index bcbb58a67..069d6530a 100644 --- a/SessionSnodeKit/OnionRequestAPI+Encryption.swift +++ b/SessionSnodeKit/OnionRequestAPI+Encryption.swift @@ -14,24 +14,22 @@ internal extension OnionRequestAPI { } /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request. - static func encrypt(_ payload: String, for destination: Destination, with version: Version) -> Promise { + static func encrypt(_ payload: Data, for destination: Destination, with version: Version) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { do { - guard let payloadAsData: Data = payload.data(using: .utf8) else { throw Error.invalidRequestInfo } - let data: Data switch version { case .v2, .v3: // Wrapping is only needed for snode requests switch destination { - case .snode: data = try encode(ciphertext: payloadAsData, json: [ "headers" : "" ]) - case .server: data = payloadAsData + case .snode: data = try encode(ciphertext: payload, json: [ "headers" : "" ]) + case .server: data = payload } case .v4: - data = payloadAsData + data = payload } let result = try encrypt(data, for: destination) diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index de36cc088..7a37d6e60 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -249,7 +249,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { } /// Builds an onion around `payload` and returns the result. - private static func buildOnion(around payload: String, targetedAt destination: Destination, version: Version) -> Promise { + private static func buildOnion(around payload: Data, targetedAt destination: Destination, version: Version) -> Promise { var guardSnode: Snode! var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination var encryptionResult: AESGCM.EncryptionResult! @@ -287,7 +287,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, using version: Version, associatedWith publicKey: String?) -> Promise { let payloadJson: JSON = [ "method": method.rawValue, "params": parameters ] - guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []), let payload: String = String(data: jsonData, encoding: .utf8) else { + guard let payload: Data = try? JSONSerialization.data(withJSONObject: payloadJson, options: []) else { return Promise(error: HTTP.Error.invalidJSON) } @@ -316,7 +316,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { let scheme: String? = url.scheme let port: UInt16? = url.port.map { UInt16($0) } - guard let payload: String = generatePayload(for: request, with: version) else { + guard let payload: Data = generatePayload(for: request, with: version) else { return Promise(error: Error.invalidRequestInfo) } @@ -328,7 +328,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { return promise } - public static func sendOnionRequest(with payload: String, to destination: Destination, version: Version) -> Promise<(OnionRequestResponseInfoType, Data?)> { + public static func sendOnionRequest(with payload: Data, to destination: Destination, version: Version) -> Promise<(OnionRequestResponseInfoType, Data?)> { let (promise, seal) = Promise<(OnionRequestResponseInfoType, Data?)>.pending() var guardSnode: Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` @@ -451,7 +451,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { // MARK: - Version Handling - private static func generatePayload(for request: URLRequest, with version: Version) -> String? { + private static func generatePayload(for request: URLRequest, with version: Version) -> Data? { guard let url = request.url else { return nil } switch version { @@ -475,10 +475,6 @@ public enum OnionRequestAPI: OnionRequestAPIType { headers["Content-Type"] = "application/json" // Assume data is JSON bodyAsString = (String(data: body, encoding: .utf8) ?? "null") } - else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream) { - headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] - bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" - } else { bodyAsString = "null" } @@ -492,7 +488,7 @@ public enum OnionRequestAPI: OnionRequestAPIType { guard let jsonData: Data = try? JSONSerialization.data(withJSONObject: payload, options: []) else { return nil } - return String(data: jsonData, encoding: .utf8) + return jsonData // V4 Onion Requests have a very different structure case .v4: @@ -502,12 +498,12 @@ public enum OnionRequestAPI: OnionRequestAPIType { .appending(url.query.map { value in "?\(value)" }) let requestInfo: RequestInfo = RequestInfo( - method: (request.httpMethod ?? "GET"), // Default (if nil) is 'GET' + method: (request.httpMethod ?? "GET"), // The default (if nil) is 'GET' endpoint: endpoint, headers: (request.allHTTPHeaderFields ?? [:]) .setting( "Content-Type", - (request.httpBody == nil && request.httpBodyStream == nil ? nil : + (request.httpBody == nil ? nil : // Default to JSON if not defined ((request.allHTTPHeaderFields ?? [:])["Content-Type"] ?? "application/json") ) @@ -515,26 +511,17 @@ public enum OnionRequestAPI: OnionRequestAPIType { .removingValue(forKey: "User-Agent") ) - guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo), let requestInfoString: String = String(data: requestInfoData, encoding: .ascii) else { + /// Generate the Bencoded payload in the form `l{requestInfoLength}:{requestInfo}{bodyLength}:{body}e` + guard let requestInfoData: Data = try? JSONEncoder().encode(requestInfo) else { return nil } + guard let prefixData: Data = "l\(requestInfoData.count):".data(using: .ascii), let suffixData: Data = "e".data(using: .ascii) else { return nil } - if let body: Data = request.httpBody { - guard let bodyString: String = String(data: body, encoding: .ascii) else { - return nil - } - - return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" - } - else if let inputStream: InputStream = request.httpBodyStream, let body: Data = try? Data(from: inputStream), let bodyString: String = String(data: body, encoding: .ascii) { - // TODO: Handle this properly - // headers["Content-Type"] = request.allHTTPHeaderFields!["Content-Type"] - // bodyAsString = "{ \"fileUpload\" : \"\(String(data: body.base64EncodedData(), encoding: .utf8) ?? "null")\" }" - return "l\(requestInfoString.count):\(requestInfoString)\(bodyString.count):\(bodyString)e" - } - else { - return "l\(requestInfoString.count):\(requestInfoString)e" + if let body: Data = request.httpBody, let bodyCountData: Data = "\(body.count):".data(using: .ascii) { + return (prefixData + requestInfoData + bodyCountData + body + suffixData) } + + return (prefixData + requestInfoData + suffixData) } } @@ -653,12 +640,6 @@ public enum OnionRequestAPI: OnionRequestAPIType { return seal.fulfill((responseInfo, nil)) } - // TODO: Is this going to be done anymore...??? -// if let timestamp = body["t"] as? Int64 { -// let offset = timestamp - Int64(NSDate.millisecondTimestamp()) -// SnodeAPI.clockOffset = offset -// } - // Extract the response data as well let dataString: String = String(responseString.suffix(from: infoStringEndIndex)) let dataStringParts: [String.SubSequence] = dataString.split(separator: ":") diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift index a13fffbb9..06c7b7f13 100644 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ b/SessionUtilitiesKit/Networking/HTTP.swift @@ -78,18 +78,23 @@ public enum HTTP { // MARK: - Error - public enum Error : LocalizedError { + public enum Error: LocalizedError, Equatable { case generic - case httpRequestFailed(statusCode: UInt, data: Data?) + case invalidURL case invalidJSON + case parsingFailed case invalidResponse - + case maxFileSizeExceeded + case httpRequestFailed(statusCode: UInt, data: Data?) + public var errorDescription: String? { switch self { case .generic: return "An error occurred." - case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." + case .invalidURL: return "Invalid URL." case .invalidJSON: return "Invalid JSON." - case .invalidResponse: return "Invalid Response" + case .parsingFailed, .invalidResponse: return "Invalid response." + case .maxFileSizeExceeded: return "Maximum file size exceeded." + case .httpRequestFailed(let statusCode, _): return "HTTP request failed with status code: \(statusCode)." } } } diff --git a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift index b73c88258..a94af27b5 100644 --- a/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift +++ b/SignalUtilitiesKit/Messaging/Sending & Receiving/MessageSender+Convenience.swift @@ -39,79 +39,108 @@ extension MessageSender { } public static func sendNonDurably(_ message: VisibleMessage, with attachmentIDs: [String], in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) -> Promise { - let attachments = attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0, transaction: transaction) as? TSAttachmentStream } + let attachments = attachmentIDs.compactMap { + TSAttachment.fetch(uniqueId: $0, transaction: transaction) as? TSAttachmentStream + } let attachmentsToUpload = attachments.filter { !$0.isUploaded } - let attachmentUploadPromises: [Promise] = attachmentsToUpload.map { stream in + let attachmentUploadPromises: [Promise] = attachmentsToUpload.map { stream in let storage = SNMessagingKitConfiguration.shared.storage - if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) { - let (promise, seal) = Promise.pending() + + if let threadId: String = thread.uniqueId, let openGroup = storage.getOpenGroup(for: threadId) { + let (promise, seal) = Promise.pending() AttachmentUploadJob.upload( stream, using: { data in - // TODO: Update to non-legacy version. - OpenGroupAPI.legacyUpload( - data, - to: openGroup.room, - on: openGroup.server - ) + OpenGroupAPI + .uploadFile( + data.bytes, + to: openGroup.room, + on: openGroup.server + ) + .map { _, response -> UInt64 in response.id } }, encrypt: false, - onSuccess: { seal.fulfill(()) }, + onSuccess: { fileId in seal.fulfill(fileId) }, onFailure: { seal.reject($0) } ) - return promise - } else { - let (promise, seal) = Promise.pending() - AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) }) return promise } + + let (promise, seal) = Promise.pending() + AttachmentUploadJob.upload( + stream, + using: FileServerAPIV2.upload, + encrypt: true, + onSuccess: { fileId in seal.fulfill(fileId) }, + onFailure: { seal.reject($0) } + ) + return promise } - return when(resolved: attachmentUploadPromises).then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise in - let errors = results.compactMap { result -> Swift.Error? in - if case .rejected(let error) = result { return error } else { return nil } + + return when(resolved: attachmentUploadPromises) + .then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise in + let errors = results.compactMap { result -> Swift.Error? in + if case .rejected(let error) = result { return error } else { return nil } + } + if let error = errors.first { return Promise(error: error) } + let fileIds: [UInt64] = results.compactMap { result -> UInt64? in + switch result { + case .fulfilled(let fileId): return fileId + default: return nil + } + } + + return sendNonDurably(message, in: thread, with: fileIds, using: transaction) } - if let error = errors.first { return Promise(error: error) } - return sendNonDurably(message, in: thread, using: transaction) - } } - public static func sendNonDurably(_ message: Message, in thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) -> Promise { + public static func sendNonDurably(_ message: Message, in thread: TSThread, with fileIds: [UInt64]? = nil, using transaction: YapDatabaseReadWriteTransaction) -> Promise { message.threadID = thread.uniqueId! - let destination = Message.Destination.from(thread) + let destination = Message.Destination.from(thread, fileIds: fileIds) return MessageSender.send(message, to: destination, using: transaction) } public static func sendNonDurably(_ message: VisibleMessage, with attachments: [SignalAttachment], in thread: TSThread) -> Promise { - Storage.writeSync{ transaction in + Storage.writeSync { transaction in prep(attachments, for: message, using: transaction) } let attachments = message.attachmentIDs.compactMap { TSAttachment.fetch(uniqueId: $0) as? TSAttachmentStream } let attachmentsToUpload = attachments.filter { !$0.isUploaded } - let attachmentUploadPromises: [Promise] = attachmentsToUpload.map { stream in + let attachmentUploadPromises: [Promise] = attachmentsToUpload.map { stream in let storage = SNMessagingKitConfiguration.shared.storage + if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) { - let (promise, seal) = Promise.pending() + let (promise, seal) = Promise.pending() + AttachmentUploadJob.upload( stream, using: { data in - // TODO: Update to non-legacy version - OpenGroupAPI.legacyUpload( - data, - to: openGroup.room, - on: openGroup.server - ) + OpenGroupAPI + .uploadFile( + data.bytes, + to: openGroup.room, + on: openGroup.server + ) + .map { _, response in response.id } }, encrypt: false, - onSuccess: { seal.fulfill(()) }, + onSuccess: { fileId in seal.fulfill(fileId) }, onFailure: { seal.reject($0) } ) return promise - } else { - let (promise, seal) = Promise.pending() - AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) }) - return promise } + + let (promise, seal) = Promise.pending() + AttachmentUploadJob.upload( + stream, + using: FileServerAPIV2.upload, + encrypt: true, + onSuccess: { fileId in seal.fulfill(fileId) }, + onFailure: { seal.reject($0) } + ) + + return promise } let (promise, seal) = Promise.pending() let results = when(resolved: attachmentUploadPromises).wait() @@ -119,13 +148,23 @@ extension MessageSender { if case .rejected(let error) = result { return error } else { return nil } } if let error = errors.first { seal.reject(error) } - Storage.write{ transaction in - sendNonDurably(message, in: thread, using: transaction).done { - seal.fulfill(()) - }.catch { error in - seal.reject(error) + let fileIds: [UInt64] = results.compactMap { result -> UInt64? in + switch result { + case .fulfilled(let fileId): return fileId + default: return nil } } + + Storage.write { transaction in + sendNonDurably(message, in: thread, with: fileIds, using: transaction) + .done { + seal.fulfill(()) + } + .catch { error in + seal.reject(error) + } + } + return promise } }