From 4f3900771efc6c34233889e5d022b354c7a10089 Mon Sep 17 00:00:00 2001 From: Morgan Pretty Date: Fri, 11 Feb 2022 16:48:16 +1100 Subject: [PATCH] More work on getting SOGS V4 integrated Updated the MessageSendJob to support V4 messages (V2 messages will be upgraded to V4 if they get re-encoded) Renamed the Message+Destination from 'openGroup' & 'openGroupV2' to 'legacyOpenGroup' and 'openGroup' Started plugging in more of the V4 APIs Renamed a number of the V2 APIs to start with 'legacy' --- Session.xcodeproj/project.pbxproj | 135 +- Session/Conversations/ConversationVC.swift | 3 +- .../Input View/MentionSelectionView.swift | 3 +- Session/Home/HomeVC.swift | 2 +- Session/Meta/AppDelegate.m | 2 +- Session/Open Groups/JoinOpenGroupVC.swift | 2 +- .../Open Groups/OpenGroupSuggestionGrid.swift | 13 +- .../Models/FileDownloadResponse.swift | 25 +- .../Models/FileUploadResponse.swift | 10 +- .../Models/LegacyFileDownloadResponse.swift | 35 + .../Models/LegacyFileUploadResponse.swift | 11 + .../Common Networking/QueryParam.swift | 4 + .../File Server/FileServerAPIV2.swift | 4 +- SessionMessagingKit/Jobs/MessageSendJob.swift | 149 +- .../Messages/Message+Destination.swift | 27 +- .../Open Groups/Models/BatchRequestInfo.swift | 90 ++ .../Open Groups/Models/Capabilities.swift | 40 + .../Open Groups/Models/FileResponse.swift | 19 + ...Body.swift => LegacyCompactPollBody.swift} | 2 +- ....swift => LegacyCompactPollResponse.swift} | 2 +- ...onse.swift => LegacyGetInfoResponse.swift} | 4 +- .../{RoomInfo.swift => LegacyRoomInfo.swift} | 2 +- ...sponse.swift => LegacyRoomsResponse.swift} | 4 +- .../Open Groups/Models/OGMessage.swift | 73 + .../Models/OpenGroupMessageV2.swift | 1 - .../Open Groups/Models/PinnedMessage.swift | 17 + .../Open Groups/Models/Room.swift | 107 ++ .../Open Groups/Models/RoomPollInfo.swift | 73 + .../Models/SendMessageRequest.swift | 67 + .../Open Groups/OpenGroupAPIV2+ObjC.swift | 6 +- .../Open Groups/OpenGroupAPIV2.swift | 1332 +++++++++++------ .../Open Groups/OpenGroupManagerV2.swift | 138 +- .../Open Groups/Types/Endpoint.swift | 166 +- .../Open Groups/Types/Request.swift | 39 +- .../Sending & Receiving/MessageSender.swift | 157 +- .../Utilities/Promise+Utilities.swift | 12 + .../Utilities/String+Utlities.swift | 11 + SessionSnodeKit/OnionRequestAPI.swift | 128 +- SessionUtilitiesKit/Networking/HTTP.swift | 2 +- 39 files changed, 2183 insertions(+), 734 deletions(-) create mode 100644 SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift create mode 100644 SessionMessagingKit/Common Networking/Models/LegacyFileUploadResponse.swift create mode 100644 SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift create mode 100644 SessionMessagingKit/Open Groups/Models/Capabilities.swift create mode 100644 SessionMessagingKit/Open Groups/Models/FileResponse.swift rename SessionMessagingKit/Open Groups/Models/{CompactPollBody.swift => LegacyCompactPollBody.swift} (94%) rename SessionMessagingKit/Open Groups/Models/{CompactPollResponse.swift => LegacyCompactPollResponse.swift} (92%) rename SessionMessagingKit/Open Groups/Models/{RoomsResponse.swift => LegacyGetInfoResponse.swift} (60%) rename SessionMessagingKit/Open Groups/Models/{RoomInfo.swift => LegacyRoomInfo.swift} (89%) rename SessionMessagingKit/Open Groups/Models/{GetInfoResponse.swift => LegacyRoomsResponse.swift} (60%) create mode 100644 SessionMessagingKit/Open Groups/Models/OGMessage.swift create mode 100644 SessionMessagingKit/Open Groups/Models/PinnedMessage.swift create mode 100644 SessionMessagingKit/Open Groups/Models/Room.swift create mode 100644 SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift create mode 100644 SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift create mode 100644 SessionMessagingKit/Utilities/Promise+Utilities.swift create mode 100644 SessionMessagingKit/Utilities/String+Utlities.swift diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index b20cbbf47..c5888779b 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -789,29 +789,43 @@ FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4380A27B31D7E00C60D73 /* Request.swift */; }; FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */; }; FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; }; - FDC4381A27B34EBA00C60D73 /* CompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */; }; + FDC4381A27B34EBA00C60D73 /* LegacyCompactPollBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */; }; FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */; }; FDC4382027B36ADC00C60D73 /* Endpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* Endpoint.swift */; }; FDC4382627B37F6900C60D73 /* DeletedMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */; }; FDC4382827B37FD300C60D73 /* ModeratorsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */; }; - FDC4382A27B3802D00C60D73 /* RoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* RoomsResponse.swift */; }; + FDC4382A27B3802D00C60D73 /* LegacyRoomsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */; }; FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */; }; FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */; }; FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383027B3841C00C60D73 /* RegisterResponse.swift */; }; FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; }; FDC4383A27B4696200C60D73 /* AuthTokenResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */; }; FDC4383E27B4708600C60D73 /* Atomic.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383D27B4708600C60D73 /* Atomic.swift */; }; - FDC4384027B4746D00C60D73 /* GetInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */; }; + FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */; }; FDC4384727B47F4D00C60D73 /* OpenGroupMessageV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */; }; - FDC4384827B47F4D00C60D73 /* RoomInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384427B47F4D00C60D73 /* RoomInfo.swift */; }; - FDC4384927B47F4D00C60D73 /* CompactPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */; }; + FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */; }; + FDC4384927B47F4D00C60D73 /* LegacyCompactPollResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */; }; FDC4384A27B47F4D00C60D73 /* Deletion.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384627B47F4D00C60D73 /* Deletion.swift */; }; FDC4384C27B47F7700C60D73 /* OpenGroupV2.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */; }; FDC4384F27B4804F00C60D73 /* Header.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* Header.swift */; }; FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* QueryParam.swift */; }; - FDC4385727B484B700C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385627B484B700C60D73 /* FileUploadResponse.swift */; }; + FDC4385727B484B700C60D73 /* LegacyFileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385627B484B700C60D73 /* LegacyFileUploadResponse.swift */; }; FDC4385927B484E800C60D73 /* FileUploadBody.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385827B484E800C60D73 /* FileUploadBody.swift */; }; - FDC4385B27B485DE00C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */; }; + FDC4385B27B485DE00C60D73 /* LegacyFileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385A27B485DE00C60D73 /* LegacyFileDownloadResponse.swift */; }; + 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 */; }; + 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 */; }; + FDC4386B27B4E88F00C60D73 /* BatchRequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */; }; + FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; platformFilter = ios; }; + FDC4386D27B4E90300C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + FDC4387227B5BB3B00C60D73 /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; + FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */; }; + FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */; }; + FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -927,6 +941,13 @@ remoteGlobalIDString = C33FD9AA255A548A00E217F9; remoteInfo = SignalUtilitiesKit; }; + FDC4386E27B4E90300C60D73 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = D221A080169C9E5E00537ABF /* Project object */; + proxyType = 1; + remoteGlobalIDString = C3C2A678255388CC00C340D1; + remoteInfo = SessionUtilitiesKit; + }; /* End PBXContainerItemProxy section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -957,6 +978,17 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; + FDC4387027B4E90300C60D73 /* Embed Frameworks */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + FDC4386D27B4E90300C60D73 /* SessionUtilitiesKit.framework in Embed Frameworks */, + ); + name = "Embed Frameworks"; + runOnlyForDeploymentPostprocessing = 0; + }; /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ @@ -1850,29 +1882,41 @@ FDC4380A27B31D7E00C60D73 /* Request.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Request.swift; sourceTree = ""; }; FDC4381427B329CE00C60D73 /* NonceGenerator16Byte.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator16Byte.swift; sourceTree = ""; }; FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = ""; }; - FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompactPollBody.swift; sourceTree = ""; }; + FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollBody.swift; sourceTree = ""; }; FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PublicKeyBody.swift; sourceTree = ""; }; FDC4381F27B36ADC00C60D73 /* Endpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Endpoint.swift; sourceTree = ""; }; FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeletedMessagesResponse.swift; sourceTree = ""; }; FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModeratorsResponse.swift; sourceTree = ""; }; - FDC4382927B3802D00C60D73 /* RoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomsResponse.swift; sourceTree = ""; }; + FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyRoomsResponse.swift; sourceTree = ""; }; FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemberCountResponse.swift; sourceTree = ""; }; FDC4382E27B383AF00C60D73 /* UnregisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnregisterResponse.swift; sourceTree = ""; }; FDC4383027B3841C00C60D73 /* RegisterResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RegisterResponse.swift; sourceTree = ""; }; FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = ""; }; FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthTokenResponse.swift; sourceTree = ""; }; FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = ""; }; - FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetInfoResponse.swift; sourceTree = ""; }; + FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGetInfoResponse.swift; sourceTree = ""; }; FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessageV2.swift; sourceTree = ""; }; - FDC4384427B47F4D00C60D73 /* RoomInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RoomInfo.swift; sourceTree = ""; }; - FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CompactPollResponse.swift; sourceTree = ""; }; + FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyRoomInfo.swift; sourceTree = ""; }; + FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LegacyCompactPollResponse.swift; sourceTree = ""; }; FDC4384627B47F4D00C60D73 /* Deletion.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Deletion.swift; sourceTree = ""; }; FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupV2.swift; sourceTree = ""; }; FDC4384E27B4804F00C60D73 /* Header.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Header.swift; sourceTree = ""; }; FDC4385027B4807400C60D73 /* QueryParam.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QueryParam.swift; sourceTree = ""; }; - FDC4385627B484B700C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; + FDC4385627B484B700C60D73 /* LegacyFileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFileUploadResponse.swift; sourceTree = ""; }; FDC4385827B484E800C60D73 /* FileUploadBody.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadBody.swift; sourceTree = ""; }; - FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = ""; }; + FDC4385A27B485DE00C60D73 /* LegacyFileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyFileDownloadResponse.swift; sourceTree = ""; }; + 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 = ""; }; + 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 = ""; }; + FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfo.swift; sourceTree = ""; }; + FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = ""; }; + FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Utilities.swift"; sourceTree = ""; }; + FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileDownloadResponse.swift; sourceTree = ""; }; + FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = ""; }; FEDBAE1B98C49BBE8C87F575 /* Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit/Pods-GlobalDependencies-FrameworkAndExtensionDependencies-ExtendedDependencies-SessionUtilitiesKit.debug.xcconfig"; sourceTree = ""; }; FF9BA33D021B115B1F5B4E46 /* Pods-SessionMessagingKit.app store release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionMessagingKit.app store release.xcconfig"; path = "Pods/Target Support Files/Pods-SessionMessagingKit/Pods-SessionMessagingKit.app store release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ @@ -1944,6 +1988,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + FDC4386C27B4E90300C60D73 /* SessionUtilitiesKit.framework in Frameworks */, C3C2A70B25539E1E00C340D1 /* SessionSnodeKit.framework in Frameworks */, 9B0A583E9B89FEF0916B793A /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_ExtendedDependencies_SessionMessagingKit.framework in Frameworks */, ); @@ -3294,7 +3339,9 @@ C33FDA6C255A57FA00E217F9 /* ProtoUtils.m */, C38EEF09255B49A8007E1867 /* SNProtoEnvelope+Conversion.swift */, C3E7134E251C867C009649BB /* Sodium+Utilities.swift */, + FDC4386827B4E6B700C60D73 /* String+Utlities.swift */, FD5D202127B1D74F00FEA984 /* ECKeyPair+Conversion.swift */, + FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */, C33FDB31255A580A00E217F9 /* SSKEnvironment.h */, C33FDAF4255A580600E217F9 /* SSKEnvironment.m */, C33FDB32255A580A00E217F9 /* SSKIncrementingIdFinder.swift */, @@ -3707,16 +3754,24 @@ children = ( FDC4384B27B47F7700C60D73 /* OpenGroupV2.swift */, FDC4381B27B354AC00C60D73 /* PublicKeyBody.swift */, - FDC4381927B34EBA00C60D73 /* CompactPollBody.swift */, - FDC4384527B47F4D00C60D73 /* CompactPollResponse.swift */, - FDC4383F27B4746D00C60D73 /* GetInfoResponse.swift */, - FDC4382927B3802D00C60D73 /* RoomsResponse.swift */, - FDC4384427B47F4D00C60D73 /* RoomInfo.swift */, + FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */, + FDC4385C27B4C18900C60D73 /* Room.swift */, + FDC4386427B4DE7600C60D73 /* RoomPollInfo.swift */, + FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */, + FDC4386027B4CDDF00C60D73 /* FileResponse.swift */, + FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */, + FDC4386227B4D94E00C60D73 /* OGMessage.swift */, FDC4382B27B380E300C60D73 /* MemberCountResponse.swift */, - FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */, FDC4382527B37F6900C60D73 /* DeletedMessagesResponse.swift */, FDC4384627B47F4D00C60D73 /* Deletion.swift */, + FDC4386627B4E10E00C60D73 /* Capabilities.swift */, FDC4382727B37FD300C60D73 /* ModeratorsResponse.swift */, + FDC4381927B34EBA00C60D73 /* LegacyCompactPollBody.swift */, + FDC4384527B47F4D00C60D73 /* LegacyCompactPollResponse.swift */, + FDC4383F27B4746D00C60D73 /* LegacyGetInfoResponse.swift */, + FDC4382927B3802D00C60D73 /* LegacyRoomsResponse.swift */, + FDC4384427B47F4D00C60D73 /* LegacyRoomInfo.swift */, + FDC4384327B47F4D00C60D73 /* OpenGroupMessageV2.swift */, FDC4383927B4696200C60D73 /* AuthTokenResponse.swift */, ); path = Models; @@ -3753,8 +3808,10 @@ isa = PBXGroup; children = ( FDC4385827B484E800C60D73 /* FileUploadBody.swift */, - FDC4385627B484B700C60D73 /* FileUploadResponse.swift */, - FDC4385A27B485DE00C60D73 /* FileDownloadResponse.swift */, + FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */, + FDC4387527B5BEF300C60D73 /* FileDownloadResponse.swift */, + FDC4385627B484B700C60D73 /* LegacyFileUploadResponse.swift */, + FDC4385A27B485DE00C60D73 /* LegacyFileDownloadResponse.swift */, ); path = Models; sourceTree = ""; @@ -4053,10 +4110,12 @@ C3C2A6EC25539DE700C340D1 /* Sources */, C3C2A6ED25539DE700C340D1 /* Frameworks */, C3C2A6EE25539DE700C340D1 /* Resources */, + FDC4387027B4E90300C60D73 /* Embed Frameworks */, ); buildRules = ( ); dependencies = ( + FDC4386F27B4E90300C60D73 /* PBXTargetDependency */, ); name = SessionMessagingKit; productName = SessionMessagingKit; @@ -4821,7 +4880,7 @@ buildActionMask = 2147483647; files = ( B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */, - FDC4382A27B3802D00C60D73 /* RoomsResponse.swift in Sources */, + FDC4382A27B3802D00C60D73 /* LegacyRoomsResponse.swift in Sources */, C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */, C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */, C3A3A156256E1B91004D228D /* ProtoUtils.m in Sources */, @@ -4832,11 +4891,14 @@ C3C2A74D2553A39700C340D1 /* VisibleMessage.swift in Sources */, C32C5AAD256DBE8F003C73A2 /* TSInfoMessage.m in Sources */, FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */, + FDC4386927B4E6B800C60D73 /* String+Utlities.swift in Sources */, C32C5A13256DB7A5003C73A2 /* PushNotificationAPI.swift in Sources */, + FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */, C32A026325A801AA000ED5D4 /* NSData+messagePadding.m in Sources */, - FDC4384927B47F4D00C60D73 /* CompactPollResponse.swift in Sources */, + FDC4384927B47F4D00C60D73 /* LegacyCompactPollResponse.swift in Sources */, C352A3932557883D00338F3E /* JobDelegate.swift in Sources */, C32C5B84256DC54F003C73A2 /* SSKEnvironment.m in Sources */, + FDC4386527B4DE7600C60D73 /* RoomPollInfo.swift in Sources */, C3A3A108256E1A5C004D228D /* OWSIncomingMessageFinder.m in Sources */, C352A31325574F5200338F3E /* MessageReceiveJob.swift in Sources */, C32C5BDD256DC88D003C73A2 /* OWSReadReceiptManager.m in Sources */, @@ -4844,7 +4906,7 @@ C3C2A7562553A3AB00C340D1 /* VisibleMessage+Quote.swift in Sources */, B8B32021258B1A650020074B /* Contact.swift in Sources */, FDC4380B27B31D7E00C60D73 /* Request.swift in Sources */, - FDC4384027B4746D00C60D73 /* GetInfoResponse.swift in Sources */, + FDC4384027B4746D00C60D73 /* LegacyGetInfoResponse.swift in Sources */, C32C5C89256DD0D2003C73A2 /* Storage+Jobs.swift in Sources */, C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */, 7B1581E2271E743B00848B49 /* OWSSounds.swift in Sources */, @@ -4853,9 +4915,10 @@ C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */, C352A3892557876500338F3E /* JobQueue.swift in Sources */, C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */, + FDC4386727B4E10E00C60D73 /* Capabilities.swift in Sources */, C32C59C1256DB41F003C73A2 /* TSGroupThread.m in Sources */, C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */, - FDC4381A27B34EBA00C60D73 /* CompactPollBody.swift in Sources */, + FDC4381A27B34EBA00C60D73 /* LegacyCompactPollBody.swift in Sources */, B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */, C32C5B9F256DC739003C73A2 /* OWSBlockingManager.m in Sources */, C3D9E52725677DF20040E4F3 /* OWSThumbnailService.swift in Sources */, @@ -4866,6 +4929,7 @@ C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */, B8856D34256F1192001CE70E /* Environment.m in Sources */, B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */, + FDC4387427B5BB9B00C60D73 /* Promise+Utilities.swift in Sources */, C32C5AB1256DBE8F003C73A2 /* TSIncomingMessage.m in Sources */, C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */, C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */, @@ -4873,6 +4937,7 @@ C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */, C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */, C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */, + FDC4386127B4CDDF00C60D73 /* FileResponse.swift in Sources */, C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */, C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */, B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */, @@ -4887,6 +4952,7 @@ FD5D202227B1D74F00FEA984 /* ECKeyPair+Conversion.swift in Sources */, FDC4381C27B354AC00C60D73 /* PublicKeyBody.swift in Sources */, FDC4382F27B383AF00C60D73 /* UnregisterResponse.swift in Sources */, + FDC4386327B4D94E00C60D73 /* OGMessage.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */, C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */, @@ -4904,9 +4970,11 @@ B8856CEE256F1054001CE70E /* OWSAudioPlayer.m in Sources */, FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */, FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */, + FDC4387827B5C35400C60D73 /* SendMessageRequest.swift in Sources */, C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */, FDC4381527B329CE00C60D73 /* NonceGenerator16Byte.swift in Sources */, C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */, + FDC4387627B5BEF300C60D73 /* FileDownloadResponse.swift in Sources */, C35D76DB26606304009AA5FB /* ThreadUpdateBatcher.swift in Sources */, FDC4382C27B380E300C60D73 /* MemberCountResponse.swift in Sources */, B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */, @@ -4920,6 +4988,7 @@ C32C5A02256DB658003C73A2 /* MessageSender+Convenience.swift in Sources */, B8566C6C256F60F50045A0B9 /* OWSUserProfile.m in Sources */, 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 */, @@ -4946,7 +5015,7 @@ C300A5E72554B07300555489 /* ExpirationTimerUpdate.swift in Sources */, B88A1AC725C90A4700E6D421 /* TypingIndicatorInteraction.swift in Sources */, C3D9E3C025676AD70040E4F3 /* TSAttachment.m in Sources */, - FDC4384827B47F4D00C60D73 /* RoomInfo.swift in Sources */, + FDC4384827B47F4D00C60D73 /* LegacyRoomInfo.swift in Sources */, C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */, C352A2FF25574B6300338F3E /* MessageSendJob.swift in Sources */, B8856D11256F112A001CE70E /* OWSAudioSession.swift in Sources */, @@ -4974,16 +5043,18 @@ C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */, C32C5AF8256DC051003C73A2 /* OWSDisappearingMessagesConfiguration.m in Sources */, C32C5EBA256DE130003C73A2 /* OWSQuotedReplyModel.m in Sources */, + FDC4386B27B4E88F00C60D73 /* BatchRequestInfo.swift in Sources */, FDC4385127B4807400C60D73 /* QueryParam.swift in Sources */, C32C5B62256DC333003C73A2 /* OWSDisappearingConfigurationUpdateInfoMessage.m in Sources */, C352A2F525574B4700338F3E /* Job.swift in Sources */, - FDC4385727B484B700C60D73 /* FileUploadResponse.swift in Sources */, - FDC4385B27B485DE00C60D73 /* FileDownloadResponse.swift in Sources */, + FDC4385727B484B700C60D73 /* LegacyFileUploadResponse.swift in Sources */, + FDC4385B27B485DE00C60D73 /* LegacyFileDownloadResponse.swift in Sources */, C32C5C01256DC9A0003C73A2 /* OWSIdentityManager.m in Sources */, C32C59C4256DB41F003C73A2 /* TSContactThread.m in Sources */, FDC4383127B3841C00C60D73 /* RegisterResponse.swift in Sources */, C32C5AB0256DBE8F003C73A2 /* TSOutgoingMessage.m in Sources */, B82A0C3826B9098200C1BCE3 /* MessageInvalidator.swift in Sources */, + FDC4385D27B4C18900C60D73 /* Room.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -5244,6 +5315,12 @@ target = C33FD9AA255A548A00E217F9 /* SignalUtilitiesKit */; targetProxy = C3D90A7025773A44002C9DF5 /* PBXContainerItemProxy */; }; + FDC4386F27B4E90300C60D73 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + platformFilter = ios; + target = C3C2A678255388CC00C340D1 /* SessionUtilitiesKit */; + targetProxy = FDC4386E27B4E90300C60D73 /* PBXContainerItemProxy */; + }; /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ diff --git a/Session/Conversations/ConversationVC.swift b/Session/Conversations/ConversationVC.swift index 1ad23de66..aeca31155 100644 --- a/Session/Conversations/ConversationVC.swift +++ b/Session/Conversations/ConversationVC.swift @@ -371,8 +371,9 @@ final class ConversationVC : BaseVC, ConversationViewModelDelegate, OWSConversat snInputView.text = draft } // Update member count if this is a V2 open group + // TODO: Non-legacy version (I assue this comes through room updates... 'activeUsers'? if let v2OpenGroup = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) { - OpenGroupAPIV2.getMemberCount(for: v2OpenGroup.room, on: v2OpenGroup.server).retainUntilComplete() + OpenGroupAPIV2.legacyGetMemberCount(for: v2OpenGroup.room, on: v2OpenGroup.server).retainUntilComplete() } } diff --git a/Session/Conversations/Input View/MentionSelectionView.swift b/Session/Conversations/Input View/MentionSelectionView.swift index 327f50ec7..1c33b00f6 100644 --- a/Session/Conversations/Input View/MentionSelectionView.swift +++ b/Session/Conversations/Input View/MentionSelectionView.swift @@ -174,7 +174,6 @@ private extension MentionSelectionView { // MARK: - Delegate -protocol MentionSelectionViewDelegate : class { - +protocol MentionSelectionViewDelegate: AnyObject { func handleMentionSelected(_ mention: Mention, from view: MentionSelectionView) } diff --git a/Session/Home/HomeVC.swift b/Session/Home/HomeVC.swift index b2a0e279f..72bc407a6 100644 --- a/Session/Home/HomeVC.swift +++ b/Session/Home/HomeVC.swift @@ -160,7 +160,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv let _ = IP2Country.shared.populateCacheIfNeeded() } // Get default open group rooms if needed - OpenGroupAPIV2.getDefaultRoomsIfNeeded() + OpenGroupAPIV2.legacyGetDefaultRoomsIfNeeded() } override func viewDidAppear(_ animated: Bool) { diff --git a/Session/Meta/AppDelegate.m b/Session/Meta/AppDelegate.m index 000af7460..e0ca1d383 100644 --- a/Session/Meta/AppDelegate.m +++ b/Session/Meta/AppDelegate.m @@ -383,7 +383,7 @@ static NSTimeInterval launchStartedAt; } if (CurrentAppContext().isMainApp) { - [SNOpenGroupAPIV2 getDefaultRoomsIfNeeded]; + [SNOpenGroupAPIV2 legacyGetDefaultRoomsIfNeeded]; } [[SNSnodeAPI getSnodePool] retainUntilComplete]; diff --git a/Session/Open Groups/JoinOpenGroupVC.swift b/Session/Open Groups/JoinOpenGroupVC.swift index 2f62101f7..7eb4bb567 100644 --- a/Session/Open Groups/JoinOpenGroupVC.swift +++ b/Session/Open Groups/JoinOpenGroupVC.swift @@ -238,7 +238,7 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate, return !suggestionGrid.frame.contains(location) } - func join(_ room: OpenGroupAPIV2.RoomInfo) { + func join(_ room: OpenGroupAPIV2.LegacyRoomInfo) { joinOpenGroupVC.joinV2OpenGroup(room: room.id, server: OpenGroupAPIV2.defaultServer, publicKey: OpenGroupAPIV2.defaultServerPublicKey) } diff --git a/Session/Open Groups/OpenGroupSuggestionGrid.swift b/Session/Open Groups/OpenGroupSuggestionGrid.swift index 5d0e5826a..2fb2d86df 100644 --- a/Session/Open Groups/OpenGroupSuggestionGrid.swift +++ b/Session/Open Groups/OpenGroupSuggestionGrid.swift @@ -3,7 +3,7 @@ import NVActivityIndicatorView final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UICollectionViewDelegateFlowLayout { private let maxWidth: CGFloat - private var rooms: [OpenGroupAPIV2.RoomInfo] = [] { didSet { update() } } + private var rooms: [OpenGroupAPIV2.LegacyRoomInfo] = [] { didSet { update() } } private var heightConstraint: NSLayoutConstraint! var delegate: OpenGroupSuggestionGridDelegate? @@ -60,9 +60,10 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl heightConstraint = set(.height, to: OpenGroupSuggestionGrid.cellHeight) widthAnchor.constraint(greaterThanOrEqualToConstant: OpenGroupSuggestionGrid.cellHeight).isActive = true if OpenGroupAPIV2.defaultRoomsPromise == nil { - OpenGroupAPIV2.getDefaultRoomsIfNeeded() + OpenGroupAPIV2.legacyGetDefaultRoomsIfNeeded() } - let _ = OpenGroupAPIV2.defaultRoomsPromise?.done { [weak self] rooms in + let _ = OpenGroupAPIV2.legacyDefaultRoomsPromise?.done { [weak self] rooms in + // TODO: Update this for the new rooms API self?.rooms = rooms } } @@ -104,7 +105,7 @@ final class OpenGroupSuggestionGrid : UIView, UICollectionViewDataSource, UIColl extension OpenGroupSuggestionGrid { fileprivate final class Cell : UICollectionViewCell { - var room: OpenGroupAPIV2.RoomInfo? { didSet { update() } } + var room: OpenGroupAPIV2.LegacyRoomInfo? { didSet { update() } } static let identifier = "OpenGroupSuggestionGridCell" @@ -172,7 +173,7 @@ extension OpenGroupSuggestionGrid { private func update() { guard let room = room else { return } - let promise = OpenGroupAPIV2.getGroupImage(for: room.id, on: OpenGroupAPIV2.defaultServer) + let promise = OpenGroupAPIV2.legacyGetGroupImage(for: room.id, on: OpenGroupAPIV2.defaultServer) imageView.image = given(promise.value) { UIImage(data: $0)! } imageView.isHidden = (imageView.image == nil) label.text = room.name @@ -183,5 +184,5 @@ extension OpenGroupSuggestionGrid { // MARK: Delegate protocol OpenGroupSuggestionGridDelegate { - func join(_ room: OpenGroupAPIV2.RoomInfo) + func join(_ room: OpenGroupAPIV2.LegacyRoomInfo) } diff --git a/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift index b18fda763..45f7c1989 100644 --- a/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift +++ b/SessionMessagingKit/Common Networking/Models/FileDownloadResponse.swift @@ -2,16 +2,29 @@ import Foundation -struct FileDownloadResponse: Codable { +// TODO: Update this (looks like it's getting changed to just be the data, the properties are send through as headers) +public struct FileDownloadResponse: Codable { enum CodingKeys: String, CodingKey { - case base64EncodedData = "result" + case fileName = "filename" + case size + case uploaded + case expires + case base64EncodedData = "result" // TODO: Confirm the name of this value } - let data: Data + public let fileName: String + public let size: Int64 + public let uploaded: TimeInterval + public let expires: TimeInterval? + public let data: Data public func encode(to encoder: Encoder) throws { var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + try container.encode(fileName, forKey: .fileName) + try container.encode(size, forKey: .size) + try container.encode(uploaded, forKey: .uploaded) + try container.encodeIfPresent(expires, forKey: .expires) try container.encode(data.base64EncodedString(), forKey: .base64EncodedData) } } @@ -19,7 +32,7 @@ struct FileDownloadResponse: Codable { // MARK: - Decoder extension FileDownloadResponse { - init(from decoder: Decoder) throws { + public init(from decoder: Decoder) throws { let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) @@ -29,6 +42,10 @@ extension FileDownloadResponse { } self = FileDownloadResponse( + fileName: try container.decode(String.self, forKey: .fileName), + size: try container.decode(Int64.self, forKey: .size), + uploaded: try container.decode(TimeInterval.self, forKey: .uploaded), + expires: try? container.decode(TimeInterval.self, forKey: .expires), data: data ) } diff --git a/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift b/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift index ba59e65d0..b787b0ceb 100644 --- a/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift +++ b/SessionMessagingKit/Common Networking/Models/FileUploadResponse.swift @@ -1,11 +1,5 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. -import Foundation - -struct FileUploadResponse: Codable { - enum CodingKeys: String, CodingKey { - case fileId = "result" - } - - public let fileId: UInt64 +public struct FileUploadResponse: Codable { + public let id: UInt64 } diff --git a/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift b/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift new file mode 100644 index 000000000..d05a3e251 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Models/LegacyFileDownloadResponse.swift @@ -0,0 +1,35 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct LegacyFileDownloadResponse: Codable { + enum CodingKeys: String, CodingKey { + case base64EncodedData = "result" + } + + let data: Data + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .base64EncodedData) + } +} + +// MARK: - Decoder + +extension LegacyFileDownloadResponse { + init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + let base64EncodedData: String = try container.decode(String.self, forKey: .base64EncodedData) + + guard let data = Data(base64Encoded: base64EncodedData) else { + throw FileServerAPIV2.Error.parsingFailed + } + + self = LegacyFileDownloadResponse( + data: data + ) + } +} diff --git a/SessionMessagingKit/Common Networking/Models/LegacyFileUploadResponse.swift b/SessionMessagingKit/Common Networking/Models/LegacyFileUploadResponse.swift new file mode 100644 index 000000000..fd22f5799 --- /dev/null +++ b/SessionMessagingKit/Common Networking/Models/LegacyFileUploadResponse.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +struct LegacyFileUploadResponse: Codable { + enum CodingKeys: String, CodingKey { + case fileId = "result" + } + + public let fileId: UInt64 +} diff --git a/SessionMessagingKit/Common Networking/QueryParam.swift b/SessionMessagingKit/Common Networking/QueryParam.swift index 5c71ae852..611b30eb8 100644 --- a/SessionMessagingKit/Common Networking/QueryParam.swift +++ b/SessionMessagingKit/Common Networking/QueryParam.swift @@ -5,4 +5,8 @@ import Foundation enum QueryParam: String { case publicKey = "public_key" case fromServerId = "from_server_id" + + case required = "required" + case fileName = "X-Filename" + case limit // For messages - number between 1 and 256 (default is 100) } diff --git a/SessionMessagingKit/File Server/FileServerAPIV2.swift b/SessionMessagingKit/File Server/FileServerAPIV2.swift index ce5404159..6e885823f 100644 --- a/SessionMessagingKit/File Server/FileServerAPIV2.swift +++ b/SessionMessagingKit/File Server/FileServerAPIV2.swift @@ -112,7 +112,7 @@ public final class FileServerAPIV2 : NSObject { let request = Request(verb: .post, endpoint: "files", body: body) return send(request, useOldServer: false).map(on: DispatchQueue.global(qos: .userInitiated)) { data in - let response: FileUploadResponse = try data.decoded(as: FileUploadResponse.self, customError: Error.parsingFailed) + let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed) return response.fileId } @@ -128,7 +128,7 @@ public final class FileServerAPIV2 : NSObject { let request = Request(verb: .get, endpoint: "files/\(file)") return send(request, useOldServer: useOldServer).map(on: DispatchQueue.global(qos: .userInitiated)) { data in - let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) + let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed) return response.data } diff --git a/SessionMessagingKit/Jobs/MessageSendJob.swift b/SessionMessagingKit/Jobs/MessageSendJob.swift index e1cbad17d..79ad93b93 100644 --- a/SessionMessagingKit/Jobs/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/MessageSendJob.swift @@ -9,49 +9,111 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi public var id: String? public var failureCount: UInt = 0 - // MARK: Settings + // MARK: - Settings + public class var collection: String { return "MessageSendJobCollection" } public static let maxFailureCount: UInt = 10 - // MARK: Initialization - @objc public convenience init(message: Message, publicKey: String) { self.init(message: message, destination: .contact(publicKey: publicKey)) } - @objc public convenience init(message: Message, groupPublicKey: String) { self.init(message: message, destination: .closedGroup(groupPublicKey: groupPublicKey)) } + // MARK: - Initialization + + @objc public convenience init(message: Message, publicKey: String) { + self.init(message: message, destination: .contact(publicKey: publicKey)) + } + + @objc public convenience init(message: Message, groupPublicKey: String) { + self.init(message: message, destination: .closedGroup(groupPublicKey: groupPublicKey)) + } public init(message: Message, destination: Message.Destination) { self.message = message self.destination = destination } - // MARK: Coding + // MARK: - Coding + public init?(coder: NSCoder) { - guard let message = coder.decodeObject(forKey: "message") as! Message?, - var rawDestination = coder.decodeObject(forKey: "destination") as! String?, - let id = coder.decodeObject(forKey: "id") as! String? else { return nil } - self.message = message - if rawDestination.removePrefix("contact(") { - guard rawDestination.removeSuffix(")") else { return nil } - let publicKey = rawDestination - destination = .contact(publicKey: publicKey) - } else if rawDestination.removePrefix("closedGroup(") { - guard rawDestination.removeSuffix(")") else { return nil } - let groupPublicKey = rawDestination - destination = .closedGroup(groupPublicKey: groupPublicKey) - } else if rawDestination.removePrefix("openGroup(") { - guard rawDestination.removeSuffix(")") else { return nil } - let components = rawDestination.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } - guard components.count == 2, let channel = UInt64(components[0]) else { return nil } - let server = components[1] - destination = .openGroup(channel: channel, server: server) - } else if rawDestination.removePrefix("openGroupV2(") { - guard rawDestination.removeSuffix(")") else { return nil } - let components = rawDestination.split(separator: ",").map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } - guard components.count == 2 else { return nil } - let room = components[0] - let server = components[1] - destination = .openGroupV2(room: room, server: server) - } else { + guard let message = coder.decodeObject(forKey: "message") as! Message?, var rawDestination = coder.decodeObject(forKey: "destination") as! String?, let id = coder.decodeObject(forKey: "id") as! String? else { return nil } + + self.message = message + + if rawDestination.removePrefix("contact(") { + guard rawDestination.removeSuffix(")") else { return nil } + + let publicKey = rawDestination + destination = .contact(publicKey: publicKey) + } + else if rawDestination.removePrefix("closedGroup(") { + guard rawDestination.removeSuffix(")") else { return nil } + + let groupPublicKey = rawDestination + destination = .closedGroup(groupPublicKey: groupPublicKey) + } + else if rawDestination.removePrefix("openGroup(") { + guard rawDestination.removeSuffix(")") else { return nil } + + let components = rawDestination + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + + guard components.count == 2, let channel = UInt64(components[0]) else { return nil } + + let server = components[1] + destination = .legacyOpenGroup(channel: channel, server: server) + } + else if rawDestination.removePrefix("openGroupV2(") { + guard rawDestination.removeSuffix(")") else { return nil } + + let components = rawDestination + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + + guard components.count == 2 else { return nil } + + let roomToken: String = components[0] + let server: String = components[1] + + destination = .openGroup( + roomToken: roomToken, + server: server + ) + } + else if rawDestination.removePrefix("openGroupV4(") { + guard rawDestination.removeSuffix(")") else { return nil } + + let components = rawDestination + .split(separator: ",") + .map { String($0).trimmingCharacters(in: .whitespacesAndNewlines) } + + guard components.count == 5 else { return nil } + + let roomToken: String = components[0] + let server: String = components[1] + let whisperTo: String? = (!components[2].isEmpty ? + components[2] : + nil + ) + let whisperMods: Bool = (components[3] == "true") + let fileIdStrings: [String] = components[4] + .replacingOccurrences(of: "[", with: "") + .replacingOccurrences(of: "]", with: "") + .split(separator: "|") + .map { String($0) } + let fileIds: [Int64]? = (fileIdStrings.isEmpty ? nil : fileIdStrings.compactMap { Int64($0) }) + + destination = .openGroup( + roomToken: roomToken, + server: server, + whisperTo: whisperTo, + whisperMods: whisperMods, + fileIds: fileIds + ) + } + else { + return nil + } + self.id = id self.failureCount = coder.decodeObject(forKey: "failureCount") as! UInt? ?? 0 } @@ -59,11 +121,28 @@ public final class MessageSendJob : NSObject, Job, NSCoding { // NSObject/NSCodi public func encode(with coder: NSCoder) { coder.encode(message, forKey: "message") switch destination { - case .contact(let publicKey): coder.encode("contact(\(publicKey))", forKey: "destination") - case .closedGroup(let groupPublicKey): coder.encode("closedGroup(\(groupPublicKey))", forKey: "destination") - case .openGroup(let channel, let server): coder.encode("openGroup(\(channel), \(server))", forKey: "destination") - case .openGroupV2(let room, let server): coder.encode("openGroupV2(\(room), \(server))", forKey: "destination") + case .contact(let publicKey): + coder.encode("contact(\(publicKey))", forKey: "destination") + + case .closedGroup(let groupPublicKey): + coder.encode("closedGroup(\(groupPublicKey))", forKey: "destination") + + case .legacyOpenGroup(let channel, let server): + coder.encode("openGroup(\(channel), \(server))", forKey: "destination") + + case .openGroup(let room, let server, let whisperTo, let whisperMods, let fileIds): + let whisperToString: String = (whisperTo ?? "") + let whisperModsString: String = (whisperMods ? "true" : "false") + let fileIdString: String = (fileIds ?? []) + .map { String($0) } + .joined(separator: "|") + + coder.encode( + "openGroupV4(\(room), \(server), \(whisperToString), \(whisperModsString), [\(fileIdString)])", + forKey: "destination" + ) } + coder.encode(id, forKey: "id") coder.encode(failureCount, forKey: "failureCount") } diff --git a/SessionMessagingKit/Messages/Message+Destination.swift b/SessionMessagingKit/Messages/Message+Destination.swift index 8b0252fa6..019ad1dd1 100644 --- a/SessionMessagingKit/Messages/Message+Destination.swift +++ b/SessionMessagingKit/Messages/Message+Destination.swift @@ -4,22 +4,33 @@ public extension Message { enum Destination { case contact(publicKey: String) case closedGroup(groupPublicKey: String) - case openGroup(channel: UInt64, server: String) - case openGroupV2(room: String, server: String) + case legacyOpenGroup(channel: UInt64, server: String) + case openGroup( + roomToken: String, + server: String, + whisperTo: String? = nil, + whisperMods: Bool = false, + fileIds: [Int64]? = nil // TODO: Handle 'fileIds' + ) static func from(_ thread: TSThread) -> Message.Destination { if let thread = thread as? TSContactThread { return .contact(publicKey: thread.contactSessionID()) - } else if let thread = thread as? TSGroupThread, thread.isClosedGroup { + } + + if let thread = thread as? TSGroupThread, thread.isClosedGroup { let groupID = thread.groupModel.groupId let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID) return .closedGroup(groupPublicKey: groupPublicKey) - } else if let thread = thread as? TSGroupThread, thread.isOpenGroup { - let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)! - return .openGroupV2(room: openGroupV2.room, server: openGroupV2.server) - } else { - preconditionFailure("TODO: Handle legacy closed groups.") } + + if let thread = thread as? TSGroupThread, thread.isOpenGroup { + let openGroup: OpenGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)! + + return .openGroup(roomToken: openGroup.room, server: openGroup.server) + } + + preconditionFailure("TODO: Handle legacy closed groups.") } } } diff --git a/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift new file mode 100644 index 000000000..ddbeeb7ec --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/BatchRequestInfo.swift @@ -0,0 +1,90 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import PromiseKit +import SessionUtilitiesKit + +extension OpenGroupAPIV2 { + // MARK: - BatchSubRequest + + struct BatchSubRequest: Codable { + let method: HTTP.Verb + let path: String + let headers: [String: String]? + let json: String? + let b64: String? + + init(request: Request) { + self.method = request.method + self.path = request.urlPathAndParamsString + self.headers = (request.headers.isEmpty ? nil : request.headers.toHTTPHeaders()) + + // TODO: Differentiate between JSON and b64 body + if let body: Data = request.body, let bodyString: String = String(data: body, encoding: .utf8) { + self.json = bodyString + } + else { + self.json = nil + } + + self.b64 = nil + } + } + + // MARK: - BatchSubResponse + + struct BatchSubResponse: Codable { + let code: Int32 + let headers: [String: String] + let body: T + } + + // MARK: - BatchRequestInfo + + struct BatchRequestInfo { + let request: Request + let responseType: Codable.Type + + init(request: Request, responseType: T.Type) { + self.request = request + self.responseType = BatchSubResponse.self + } + } + + // MARK: - BatchRequest + + typealias BatchRequest = [BatchSubRequest] + typealias BatchResponseTypes = [Codable.Type] + typealias BatchResponse = [Codable] +} + +// MARK: - Convenience + +public extension Decodable { + static func decoded(from data: Data) throws -> Self { + return try JSONDecoder().decode(Self.self, from: data) + } +} + +extension Promise where T == Data { + func decoded(as types: OpenGroupAPIV2.BatchResponseTypes, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise { + self.map(on: queue) { data -> OpenGroupAPIV2.BatchResponse in + // Need to split the data into an array of data so each item can be Decoded correctly + guard let jsonObject: Any = try? JSONSerialization.jsonObject(with: data, options: [.fragmentsAllowed]) else { + throw OpenGroupAPIV2.Error.parsingFailed + } + guard let anyArray: [Any] = jsonObject as? [Any] else { throw OpenGroupAPIV2.Error.parsingFailed } + + let dataArray: [Data] = anyArray.compactMap { try? JSONSerialization.data(withJSONObject: $0) } + guard dataArray.count == types.count else { throw OpenGroupAPIV2.Error.parsingFailed } + + do { + return try zip(dataArray, types) + .map { data, type in try type.decoded(from: data) } + } + catch let thrownError { + throw (error ?? thrownError) + } + } + } +} diff --git a/SessionMessagingKit/Open Groups/Models/Capabilities.swift b/SessionMessagingKit/Open Groups/Models/Capabilities.swift new file mode 100644 index 000000000..ab1cd3dab --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/Capabilities.swift @@ -0,0 +1,40 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct Capabilities: Codable { + enum Capability: CaseIterable, Codable { + static var allCases: [Capability] { + [.pysogs] + } + + case pysogs + + /// Fallback case if the capability isn't supported by this version of the app + case unsupported(String) + + // MARK: - Convenience + + var rawValue: String { + switch self { + case .unsupported(let originalValue): return originalValue + default: return "\(self)" + } + } + + // MARK: - Codable + + init(from decoder: Decoder) throws { + let container: SingleValueDecodingContainer = try decoder.singleValueContainer() + let valueString: String = try container.decode(String.self) + let maybeValue: Capability? = Capability.allCases.first { $0.rawValue == valueString } + + self = (maybeValue ?? .unsupported(valueString)) + } + } + + let capabilities: [Capability] + let missing: [Capability]? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/FileResponse.swift b/SessionMessagingKit/Open Groups/Models/FileResponse.swift new file mode 100644 index 000000000..6ec0f9888 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/FileResponse.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct FileResponse: Codable { + enum CodingKeys: String, CodingKey { + case fileName = "filename" + case size + case uploaded + case expires + } + + let fileName: String? + let size: Int64 + let uploaded: TimeInterval + let expires: TimeInterval? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/CompactPollBody.swift b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollBody.swift similarity index 94% rename from SessionMessagingKit/Open Groups/Models/CompactPollBody.swift rename to SessionMessagingKit/Open Groups/Models/LegacyCompactPollBody.swift index 0e26fd773..9180bcc8c 100644 --- a/SessionMessagingKit/Open Groups/Models/CompactPollBody.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollBody.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPIV2 { - struct CompactPollBody: Codable { + struct LegacyCompactPollBody: Codable { struct Room: Codable { enum CodingKeys: String, CodingKey { case id = "room_id" diff --git a/SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift similarity index 92% rename from SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift index 626108d84..8988a72b5 100644 --- a/SessionMessagingKit/Open Groups/Models/CompactPollResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyCompactPollResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPIV2 { - public struct CompactPollResponse: Codable { + public struct LegacyCompactPollResponse: Codable { public struct Result: Codable { enum CodingKeys: String, CodingKey { case room = "room_id" diff --git a/SessionMessagingKit/Open Groups/Models/RoomsResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyGetInfoResponse.swift similarity index 60% rename from SessionMessagingKit/Open Groups/Models/RoomsResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyGetInfoResponse.swift index e5ac33d23..fe00c940e 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomsResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyGetInfoResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPIV2 { - struct RoomsResponse: Codable { - let rooms: [RoomInfo] + struct LegacyGetInfoResponse: Codable { + let room: LegacyRoomInfo } } diff --git a/SessionMessagingKit/Open Groups/Models/RoomInfo.swift b/SessionMessagingKit/Open Groups/Models/LegacyRoomInfo.swift similarity index 89% rename from SessionMessagingKit/Open Groups/Models/RoomInfo.swift rename to SessionMessagingKit/Open Groups/Models/LegacyRoomInfo.swift index bef83f77f..1afce0b96 100644 --- a/SessionMessagingKit/Open Groups/Models/RoomInfo.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyRoomInfo.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPIV2 { - public struct RoomInfo: Codable { + public struct LegacyRoomInfo: Codable { enum CodingKeys: String, CodingKey { case id case name diff --git a/SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift b/SessionMessagingKit/Open Groups/Models/LegacyRoomsResponse.swift similarity index 60% rename from SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift rename to SessionMessagingKit/Open Groups/Models/LegacyRoomsResponse.swift index 6d637cddc..7251a9ce4 100644 --- a/SessionMessagingKit/Open Groups/Models/GetInfoResponse.swift +++ b/SessionMessagingKit/Open Groups/Models/LegacyRoomsResponse.swift @@ -3,7 +3,7 @@ import Foundation extension OpenGroupAPIV2 { - struct GetInfoResponse: Codable { - let room: RoomInfo + struct LegacyRoomsResponse: Codable { + let rooms: [LegacyRoomInfo] } } diff --git a/SessionMessagingKit/Open Groups/Models/OGMessage.swift b/SessionMessagingKit/Open Groups/Models/OGMessage.swift new file mode 100644 index 000000000..d748f3306 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/OGMessage.swift @@ -0,0 +1,73 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct Message: Codable { + enum CodingKeys: String, CodingKey { + case id + case sender = "session_id" + case posted + case edited + case seqNo = "seqno" + case whisper + case whisperMods = "whisper_mods" + case whisperTo = "whisper_to" + + case base64EncodedData = "data" + case base64EncodedSignature = "signature" + } + + public let id: Int64 + public let sender: String? + public let posted: TimeInterval + public let edited: TimeInterval? + public let seqNo: Int64 + public let whisper: Bool + public let whisperMods: Bool + public let whisperTo: String? + + public let base64EncodedData: String? + public let base64EncodedSignature: String? + } +} + +// MARK: - Decoder + +extension OpenGroupAPIV2.Message { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + let maybeSender: String? = try? container.decode(String.self, forKey: .sender) + let maybeBase64EncodedData: String? = try? container.decode(String.self, forKey: .base64EncodedData) + let maybeBase64EncodedSignature: String? = try? container.decode(String.self, forKey: .base64EncodedSignature) + + // 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 OpenGroupAPIV2.Error.parsingFailed + } + + let publicKey: Data = Data(hex: sender.removingIdPrefixIfNeeded()) + let isValid: Bool = ((try? Ed25519.verifySignature(signature, publicKey: publicKey, data: data)) ?? false) + + guard isValid else { + SNLog("Ignoring message with invalid signature.") + throw OpenGroupAPIV2.Error.parsingFailed + } + } + + self = OpenGroupAPIV2.Message( + id: try container.decode(Int64.self, forKey: .id), + sender: try? container.decode(String.self, forKey: .sender), + posted: try container.decode(TimeInterval.self, forKey: .posted), + edited: try? container.decode(TimeInterval.self, forKey: .edited), + seqNo: try container.decode(Int64.self, forKey: .seqNo), + whisper: ((try? container.decode(Bool.self, forKey: .whisper)) ?? false), + whisperMods: ((try? container.decode(Bool.self, forKey: .whisperMods)) ?? false), + whisperTo: try? container.decode(String.self, forKey: .whisperTo), + base64EncodedData: maybeBase64EncodedData, + base64EncodedSignature: maybeBase64EncodedSignature + ) + } +} diff --git a/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift b/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift index 76a0b11b6..4f7bf2163 100644 --- a/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift +++ b/SessionMessagingKit/Open Groups/Models/OpenGroupMessageV2.swift @@ -20,7 +20,6 @@ public struct OpenGroupMessageV2: Codable { public let base64EncodedSignature: String? public func sign(with publicKey: String) -> OpenGroupMessageV2? { - // TODO: Swap to use blinded key guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return nil } guard let data = Data(base64Encoded: base64EncodedData) else { return nil } guard let signature = try? Ed25519.sign(data, with: userKeyPair) else { diff --git a/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift b/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift new file mode 100644 index 000000000..610daa5db --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/PinnedMessage.swift @@ -0,0 +1,17 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct PinnedMessage: Codable { + enum CodingKeys: String, CodingKey { + case id + case pinnedAt = "pinned_at" + case pinnedBy = "pinned_by" + } + + let id: Int64 + let pinnedAt: TimeInterval + let pinnedBy: String + } +} diff --git a/SessionMessagingKit/Open Groups/Models/Room.swift b/SessionMessagingKit/Open Groups/Models/Room.swift new file mode 100644 index 000000000..4e116cb29 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/Room.swift @@ -0,0 +1,107 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct Room: Codable { + enum CodingKeys: String, CodingKey { + case token + case created + case name + case description + case imageId = "image_id" + + case infoUpdates = "info_updates" + case messageSequence = "message_sequence" + case activeUsers = "active_users" + case activeUsersCutoff = "active_users_cutoff" + case pinnedMessages = "pinned_messages" + + case admin + case globalAdmin = "global_admin" + case admins + case hiddenAdmins = "hidden_admins" + + case moderator + case globalModerator = "global_moderator" + case moderators + case hiddenModerators = "hidden_moderators" + + case read + case defaultRead = "default_read" + case write + case defaultWrite = "default_write" + case upload + case defaultUpload = "default_upload" + } + + public let token: String + public let created: TimeInterval + public let name: String + public let description: String? + public let imageId: Int64? + + public let infoUpdates: Int64 + public let messageSequence: Int64 + public let activeUsers: Int64 + public let activeUsersCutoff: Int64 + public let pinnedMessages: [PinnedMessage]? + + public let admin: Bool + public let globalAdmin: Bool + public let admins: [String] + public let hiddenAdmins: [String]? + + public let moderator: Bool + public let globalModerator: Bool + public let moderators: [String] + public let hiddenModerators: [String]? + + public let read: Bool + public let defaultRead: Bool + public let write: Bool + public let defaultWrite: Bool + public let upload: Bool + public let defaultUpload: Bool + } +} + +// MARK: - Decoding + +extension OpenGroupAPIV2.Room { + public init(from decoder: Decoder) throws { + let container: KeyedDecodingContainer = try decoder.container(keyedBy: CodingKeys.self) + + self = OpenGroupAPIV2.Room( + token: try container.decode(String.self, forKey: .token), + created: try container.decode(TimeInterval.self, forKey: .created), + name: try container.decode(String.self, forKey: .name), + description: try? container.decode(String.self, forKey: .description), + imageId: try? container.decode(Int64.self, forKey: .imageId), + + infoUpdates: try container.decode(Int64.self, forKey: .infoUpdates), + messageSequence: try container.decode(Int64.self, forKey: .messageSequence), + activeUsers: try container.decode(Int64.self, forKey: .activeUsers), + activeUsersCutoff: try container.decode(Int64.self, forKey: .activeUsersCutoff), + pinnedMessages: try? container.decode([OpenGroupAPIV2.PinnedMessage].self, forKey: .pinnedMessages), + + admin: ((try? container.decode(Bool.self, forKey: .admin)) ?? false), + globalAdmin: ((try? container.decode(Bool.self, forKey: .globalAdmin)) ?? false), + admins: try container.decode([String].self, forKey: .admins), + hiddenAdmins: try? container.decode([String].self, forKey: .hiddenAdmins), + + moderator: ((try? container.decode(Bool.self, forKey: .moderator)) ?? false), + globalModerator: ((try? container.decode(Bool.self, forKey: .globalModerator)) ?? false), + moderators: try container.decode([String].self, forKey: .moderators), + hiddenModerators: try? container.decode([String].self, forKey: .hiddenModerators), + + read: try container.decode(Bool.self, forKey: .read), + defaultRead: ((try? container.decode(Bool.self, forKey: .defaultRead)) ?? false), + write: try container.decode(Bool.self, forKey: .write), + defaultWrite: ((try? container.decode(Bool.self, forKey: .defaultWrite)) ?? false), + upload: try container.decode(Bool.self, forKey: .upload), + defaultUpload: ((try? container.decode(Bool.self, forKey: .defaultUpload)) ?? false) + ) + } +} + diff --git a/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift new file mode 100644 index 000000000..d48bc24a9 --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/RoomPollInfo.swift @@ -0,0 +1,73 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + /// This only contains ephemeral data + public struct RoomPollInfo: Codable { + enum CodingKeys: String, CodingKey { + case token + case created + case name + case description + case imageId = "image_id" + + case infoUpdates = "info_updates" + case messageSequence = "message_sequence" + case activeUsers = "active_users" + case activeUsersCutoff = "active_users_cutoff" + case pinnedMessages = "pinned_messages" + + case admin + case globalAdmin = "global_admin" + case admins + case hiddenAdmins = "hidden_admins" + + case moderator + case globalModerator = "global_moderator" + case moderators + case hiddenModerators = "hidden_moderators" + + case read + case defaultRead = "default_read" + case write + case defaultWrite = "default_write" + case upload + case defaultUpload = "default_upload" + + case details + } + + public let token: String? + public let created: TimeInterval? + public let name: String? + public let description: String? + public let imageId: Int64? + + public let infoUpdates: Int64? + public let messageSequence: Int64? + public let activeUsers: Int64? + public let activeUsersCutoff: Int64? + public let pinnedMessages: [PinnedMessage]? + + public let admin: Bool? + public let globalAdmin: Bool? + public let admins: [String]? + public let hiddenAdmins: [String]? + + public let moderator: Bool? + public let globalModerator: Bool? + public let moderators: [String]? + public let hiddenModerators: [String]? + + public let read: Bool? + public let defaultRead: Bool? + public let write: Bool? + public let defaultWrite: Bool? + public let upload: Bool? + public let defaultUpload: Bool? + + /// Only populated and different if the `info_updates` counter differs from the provided `info_updated` value + public let details: Room? + } +} diff --git a/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift new file mode 100644 index 000000000..848e677bf --- /dev/null +++ b/SessionMessagingKit/Open Groups/Models/SendMessageRequest.swift @@ -0,0 +1,67 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +extension OpenGroupAPIV2 { + public struct SendMessageRequest: Codable { + enum CodingKeys: String, CodingKey { + case data + case signature + case whisperTo = "whisper_to" + case whisperMods = "whisper_mods" + case fileIds = "files" + } + + let data: Data + let signature: Data + let whisperTo: String? + let whisperMods: Bool + let fileIds: [Int64]? + + // MARK: - Initialization + + init( + data: Data, + signature: Data, + whisperTo: String? = nil, + whisperMods: Bool = false, + fileIds: [Int64]? = nil + ) { + self.data = data + self.signature = signature + self.whisperTo = whisperTo + self.whisperMods = whisperMods + self.fileIds = fileIds + } + + // MARK: - Encodable + + public func encode(to encoder: Encoder) throws { + var container: KeyedEncodingContainer = encoder.container(keyedBy: CodingKeys.self) + + try container.encode(data.base64EncodedString(), forKey: .data) + try container.encode(signature.base64EncodedString(), forKey: .signature) + try container.encodeIfPresent(whisperTo, forKey: .whisperTo) + try container.encode(whisperMods, forKey: .whisperMods) + try container.encodeIfPresent(fileIds, forKey: .fileIds) + } + + // MARK: - Signing + + public static func sign(message: Data, for idType: IdPrefix, with publicKey: String) -> (data: Data, signature: Data)? { + guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { + return nil + } + guard let targetKeyPair: ECKeyPair = try? userKeyPair.convert(to: idType, with: publicKey) else { + return nil + } + + guard let signature = try? Ed25519.sign(message, with: targetKeyPair) else { + SNLog("Failed to sign open group message.") + return nil + } + + return (message, signature) + } + } +} diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift index dd9a57b18..6eb84e145 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2+ObjC.swift @@ -12,8 +12,8 @@ extension OpenGroupAPIV2 { return isUserModerator(publicKey, for: room, on: server) } - @objc(getDefaultRoomsIfNeeded) - public static func objc_getDefaultRoomsIfNeeded() { - return getDefaultRoomsIfNeeded() + @objc(legacyGetDefaultRoomsIfNeeded) + public static func objc_legacyGetDefaultRoomsIfNeeded() { + return legacyGetDefaultRoomsIfNeeded() } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift index e447a6832..6741050ca 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPIV2.swift @@ -18,7 +18,7 @@ public final class OpenGroupAPIV2: NSObject { private static var hasUpdatedLastOpenDate = false public static let workQueue = DispatchQueue(label: "OpenGroupAPIV2.workQueue", qos: .userInitiated) // It's important that this is a serial queue public static var moderators: [String: [String: Set]] = [:] // Server URL to room ID to set of moderator IDs - public static var defaultRoomsPromise: Promise<[RoomInfo]>? + public static var defaultRoomsPromise: Promise<[Room]>? public static var groupImagePromises: [String: Promise] = [:] private static let timeSinceLastOpen: TimeInterval = { @@ -27,73 +27,94 @@ public final class OpenGroupAPIV2: NSObject { return Date().timeIntervalSince(lastOpen) }() - // MARK: - Convenience + // MARK: - Batching & Polling - private static func send(_ request: Request) -> Promise { - guard let url: URL = request.url else { return Promise(error: Error.invalidURL) } + public static func poll(_ server: String) -> Promise { + // TODO: Remove comments + // Capabilities + // Fetch each room + // Poll Info + // /room//pollInfo/ instead? + // Fetch messages for each room + // /room/{roomToken}/messages/since/{messageSequence}: + // Fetch deletions for each room (included in messages) - var urlRequest: URLRequest = URLRequest(url: url) - urlRequest.httpMethod = request.verb.rawValue - urlRequest.allHTTPHeaderFields = request.headers - .setting(.room, request.room) // TODO: Is this needed anymore? Add at the request level? - .toHTTPHeaders() - urlRequest.httpBody = request.body + // old compact_poll data +// public let room: String +// public let statusCode: UInt +// public let messages: [OpenGroupMessageV2]? +// public let deletions: [Deletion]? +// public let moderators: [String]? - if request.useOnionRouting { - guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { - return Promise(error: Error.noPublicKey) - } - - if request.isAuthRequired { - // Determine if we should be using legacy auth for this endpoint - // TODO: Might need to store this at an OpenGroup level (so all requests can use the appropriate method) - if request.endpoint.useLegacyAuth { - // Because legacy auth happens on a per-room basis, we need to have a room to - // make an authenticated request - guard let room = request.room else { - return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) - } + let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage + let requestResponseType: [BatchRequestInfo] = [ + BatchRequestInfo( + request: Request( + server: server, + endpoint: .capabilities, + queryParameters: [:] // TODO: Add any requirements '.required' + ), + responseType: Capabilities.self + ) + ] + .appending( + storage.getAllV2OpenGroups().values + .filter { $0.server == server } + .flatMap { openGroup -> [BatchRequestInfo] in + let lastSeqNo: Int64? = storage.getLastMessageServerID(for: openGroup.room, on: server) + let targetSeqNo: Int64 = (lastSeqNo ?? 0) - return getAuthToken(for: room, on: request.server) - .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in - urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue) - - let promise = OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) - promise.catch(on: OpenGroupAPIV2.workQueue) { error in - // A 401 means that we didn't provide a (valid) auth token for a route - // that required one. We use this as an indication that the token we're - // using has expired. Note that a 403 has a different meaning; it means - // that we provided a valid token but it doesn't have a high enough - // permission level for the route in question. - if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 { - let storage = SNMessagingKitConfiguration.shared.storage - - storage.writeSync { transaction in - storage.removeAuthToken(for: room, on: request.server, using: transaction) - } - } - } - - return promise - } + return [ + BatchRequestInfo( + request: Request( + server: server, + // TODO: Source the '0' from the open group (will need to add a new field and default to 0) + endpoint: .roomPollInfo(openGroup.room, 0) + ), + responseType: RoomPollInfo.self + ), + BatchRequestInfo( + request: Request( + server: server, + endpoint: (lastSeqNo == nil ? + .roomMessagesRecent(openGroup.room) : + .roomMessagesSince(openGroup.room, seqNo: targetSeqNo) + ) + ), + responseType: [Message].self + ) + ] } - - // Attempt to sign the request with the new auth - guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else { - return Promise(error: Error.signingFailed) - } - - // TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`) - return OnionRequestAPI.sendOnionRequest(signedRequest, to: request.server, using: publicKey) - } - - return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) - } + ) - preconditionFailure("It's currently not allowed to send non onion routed requests.") + // TODO: Handle response (maybe in the poller or the OpenGroupManagerV2?) + return batch(server, requests: requestResponseType) + .map { _ in () } } - public static func compactPoll(_ server: String) -> Promise { + private static func batch(_ server: String, requests: [BatchRequestInfo]) -> Promise { + let requestBody: BatchRequest = requests.map { BatchSubRequest(request: $0.request) } + let responseTypes = requests.map { $0.responseType } + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request: Request = Request( + method: .post, + server: server, + endpoint: .batch, + body: body + ) + + return send(request) + .decoded(as: responseTypes, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + .map { result in + return "" + } + } + + public static func compactPoll(_ server: String) -> Promise { let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage let rooms: [String] = storage.getAllV2OpenGroups().values .filter { $0.server == server } @@ -107,10 +128,10 @@ public final class OpenGroupAPIV2: NSObject { hasUpdatedLastOpenDate = true } - let requestBody: CompactPollBody = CompactPollBody( + let requestBody: LegacyCompactPollBody = LegacyCompactPollBody( requests: rooms - .map { roomId -> CompactPollBody.Room in - CompactPollBody.Room( + .map { roomId -> LegacyCompactPollBody.Room in + LegacyCompactPollBody.Room( id: roomId, fromMessageServerId: (useMessageLimit ? nil : storage.getLastMessageServerID(for: roomId, on: server) @@ -128,22 +149,20 @@ public final class OpenGroupAPIV2: NSObject { } let request = Request( - verb: .post, - room: nil, + method: .post, server: server, endpoint: .legacyCompactPoll(legacyAuth: false), - body: body, - isAuthRequired: true + body: body ) return send(request) - .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in - let response: CompactPollResponse = try data.decoded(as: CompactPollResponse.self, customError: Error.parsingFailed) + .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in + let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed) return when( fulfilled: response.results - .map { (result: CompactPollResponse.Result) in - process(messages: result.messages, for: result.room, on: server) + .map { (result: LegacyCompactPollResponse.Result) in + legacyProcess(messages: result.messages, for: result.room, on: server) .then(on: OpenGroupAPIV2.workQueue) { _ in process(deletions: result.deletions, for: result.room, on: server) } @@ -152,111 +171,20 @@ public final class OpenGroupAPIV2: NSObject { } } - public static func legacyCompactPoll(_ server: String) -> Promise { - let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage - let rooms: [String] = storage.getAllV2OpenGroups().values - .filter { $0.server == server } - .map { $0.room } - var getAuthTokenPromises: [String: Promise] = [:] - let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupPollerV2.maxInactivityPeriod) - - hasPerformedInitialPoll[server] = true - - if !hasUpdatedLastOpenDate { - UserDefaults.standard[.lastOpen] = Date() - hasUpdatedLastOpenDate = true - } - - for room in rooms { - getAuthTokenPromises[room] = getAuthToken(for: room, on: server) - } - - let requestBody: CompactPollBody = CompactPollBody( - requests: rooms - .map { roomId -> CompactPollBody.Room in - CompactPollBody.Room( - id: roomId, - fromMessageServerId: (useMessageLimit ? nil : - storage.getLastMessageServerID(for: roomId, on: server) - ), - fromDeletionServerId: (useMessageLimit ? nil : - storage.getLastDeletionServerID(for: roomId, on: server) - ), - legacyAuthToken: nil - ) - } - ) - - return when(fulfilled: [Promise](getAuthTokenPromises.values)) - .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise in - let requestBodyWithAuthTokens: CompactPollBody = CompactPollBody( - requests: requestBody.requests.compactMap { oldRoom -> CompactPollBody.Room? in - guard let authToken: String = getAuthTokenPromises[oldRoom.id]?.value else { return nil } - - return CompactPollBody.Room( - id: oldRoom.id, - fromMessageServerId: oldRoom.fromMessageServerId, - fromDeletionServerId: oldRoom.fromDeletionServerId, - legacyAuthToken: authToken - ) - } - ) - - guard let body: Data = try? JSONEncoder().encode(requestBodyWithAuthTokens) else { - return Promise(error: HTTP.Error.invalidJSON) - } - - let request = Request( - verb: .post, - room: nil, - server: server, - endpoint: .legacyCompactPoll(legacyAuth: true), - body: body, - isAuthRequired: false - ) - - return send(request) - .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in - let response: CompactPollResponse = try data.decoded(as: CompactPollResponse.self, customError: Error.parsingFailed) - - return when( - fulfilled: response.results - .compactMap { (result: CompactPollResponse.Result) -> Promise<[Deletion]>? in - // A 401 means that we didn't provide a (valid) auth token for a route that - // required one. We use this as an indication that the token we're using has - // expired. Note that a 403 has a different meaning; it means that we provided - // a valid token but it doesn't have a high enough permission level for the - // route in question. - guard result.statusCode != 401 else { - storage.writeSync { transaction in - storage.removeAuthToken(for: result.room, on: server, using: transaction) - } - - return nil - } - - return process(messages: result.messages, for: result.room, on: server) - .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise<[Deletion]> in - process(deletions: result.deletions, for: result.room, on: server) - } - } - ).then(on: OpenGroupAPIV2.workQueue) { _ in Promise.value(response) } - } - } - } - // MARK: - Authentication - // TODO: Turn 'Sodium' and 'NonceGenerator16Byte' into protocols for unit testing + // TODO: Turn 'Sodium' and 'NonceGenerator16Byte' into protocols for unit testing. static func sign( _ request: URLRequest, with publicKey: String, sodium: Sodium = Sodium(), nonceGenerator: NonceGenerator16Byte = NonceGenerator16Byte() ) -> URLRequest? { - guard let path: String = request.url?.path else { return nil } + guard let url: URL = request.url else { return nil } var updatedRequest: URLRequest = request + let path: String = url.path + .appending(url.query.map { value in "?\(value)" }) let method: String = (request.httpMethod ?? "GET") let timestamp: Int = Int(floor(Date().timeIntervalSince1970)) let nonce: Data = Data(nonceGenerator.nonce()) @@ -311,191 +239,248 @@ public final class OpenGroupAPIV2: NSObject { return updatedRequest } - private static func getAuthToken(for room: String, on server: String) -> Promise { - // TODO: Do we need to check the `/capabilities` of the SOGS to determine if it has new auth and if not then fall back to the old auth approach?????? - let storage = SNMessagingKitConfiguration.shared.storage - - if let authToken: String = storage.getAuthToken(for: room, on: server) { - return Promise.value(authToken) - } - - if let authTokenPromise: Promise = authTokenPromises.wrappedValue["\(server).\(room)"] { - return authTokenPromise - } - - let promise: Promise = requestNewAuthToken(for: room, on: server) - .then(on: OpenGroupAPIV2.workQueue) { claimAuthToken($0, for: room, on: server) } - .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in - let (promise, seal) = Promise.pending() - storage.write(with: { transaction in - storage.setAuthToken(for: room, on: server, to: authToken, using: transaction) - }, completion: { - seal.fulfill(authToken) - }) - return promise - } - - promise - .done(on: OpenGroupAPIV2.workQueue) { _ in - authTokenPromises.wrappedValue["\(server).\(room)"] = nil - } - .catch(on: OpenGroupAPIV2.workQueue) { _ in - authTokenPromises.wrappedValue["\(server).\(room)"] = nil - } - - authTokenPromises.wrappedValue["\(server).\(room)"] = promise - return promise - } - - public static func requestNewAuthToken(for room: String, on server: String) -> Promise { - SNLog("Requesting auth token for server: \(server).") - guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { - return Promise(error: Error.generic) - } - + // MARK: - Capabilities + + public static func capabilities(on server: String) -> Promise { let request: Request = Request( - verb: .get, - room: room, server: server, - endpoint: .legacyAuthTokenChallenge(legacyAuth: true), - queryParameters: [ - .publicKey: getUserHexEncodedPublicKey() - ], - isAuthRequired: false + endpoint: .capabilities, + queryParameters: [:] // TODO: Add any requirements '.required' ) - return send(request).map(on: OpenGroupAPIV2.workQueue) { data in - let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed) - let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) - - guard let tokenAsData = try? AESGCM.decrypt(response.challenge.ciphertext, with: symmetricKey) else { - throw Error.decryptionFailed - } - - return tokenAsData.toHexString() - } - } - - public static func claimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) - - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Promise(error: HTTP.Error.invalidJSON) - } - - let request: Request = Request( - verb: .post, - room: room, - server: server, - endpoint: .legacyAuthTokenClaim(legacyAuth: true), - body: body, - headers: [ - // Set explicitly here because is isn't in the database yet at this point - .authorization: authToken - ], - isAuthRequired: false - ) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in authToken } - } - - /// Should be called when leaving a group. - public static func deleteAuthToken(for room: String, on server: String) -> Promise { - let request: Request = Request( - verb: .delete, - room: room, - server: server, - endpoint: .legacyAuthToken(legacyAuth: true) - ) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in - let storage = SNMessagingKitConfiguration.shared.storage - - storage.write { transaction in - storage.removeAuthToken(for: room, on: server, using: transaction) - } - } + // TODO: Handle a `412` response (ie. a required capability isn't supported) + return send(request) + .decoded(as: Capabilities.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - // MARK: - File Storage + // MARK: - Room - public static func upload(_ file: Data, to room: String, on server: String) -> Promise { - let requestBody: FileUploadBody = FileUploadBody(file: file.base64EncodedString()) + public static func rooms(for server: String) -> Promise<[Room]> { + let request: Request = Request( + server: server, + endpoint: .rooms + ) + + return send(request) + .decoded(as: [Room].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + public static func room(for roomToken: String, on server: String) -> Promise { + let request: Request = Request( + server: server, + endpoint: .room(roomToken) + ) + + return send(request) + .decoded(as: Room.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + public static func roomPollInfo(lastUpdated: Int64, for roomToken: String, on server: String) -> Promise { + let request: Request = Request( + server: server, + endpoint: .roomPollInfo(roomToken, lastUpdated) + ) + + return send(request) + .decoded(as: RoomPollInfo.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + // MARK: - Messages + + public static func send( + _ plaintext: Data, + to roomToken: String, + on server: String, + whisperTo: String?, + whisperMods: Bool, + with serverPublicKey: String + ) -> Promise { + // TODO: Change this to use '.blinded' once it's working + guard let signedRequest: (data: Data, signature: Data) = SendMessageRequest.sign(message: plaintext, for: .standard, with: serverPublicKey) else { + return Promise(error: Error.signingFailed) + } + + let requestBody: SendMessageRequest = SendMessageRequest( + data: signedRequest.data, + signature: signedRequest.signature, + whisperTo: whisperTo, + whisperMods: whisperMods, + fileIds: nil // TODO: Add support for 'fileIds' + ) guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Promise(error: HTTP.Error.invalidJSON) - } - - let request = Request(verb: .post, room: room, server: server, endpoint: .files, body: body) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { data in - let response: FileUploadResponse = try data.decoded(as: FileUploadResponse.self, customError: Error.parsingFailed) - - return response.fileId - } - } - - public static func download(_ file: UInt64, from room: String, on server: String) -> Promise { - let request = Request(verb: .get, room: room, server: server, endpoint: .file(file)) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { data in - let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) - - return response.data - } - } - - // MARK: - Message Sending & Receiving - - public static func send(_ message: OpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { - // TODO: Test if we need a legacy version - guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) } - guard let body: Data = try? JSONEncoder().encode(signedMessage) else { return Promise(error: Error.parsingFailed) } - let request = Request(verb: .post, room: room, server: server, endpoint: .messages, body: body) - return send(request).map(on: OpenGroupAPIV2.workQueue) { data in - let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed) - Storage.shared.write { transaction in - Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction) - } - return message - } - } - - public static func getMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { - let storage = SNMessagingKitConfiguration.shared.storage - let request: Request = Request( - verb: .get, - room: room, + let request = Request( + method: .post, server: server, - endpoint: .messages, - queryParameters: [ - .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } - ].compactMapValues { $0 } + endpoint: .roomMessage(roomToken), + body: body ) - return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[OpenGroupMessageV2]> in - let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed) - - return process(messages: messages, for: room, on: server) - } + return send(request) + .decoded(as: Message.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) } - private static func process(messages: [OpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { - guard let messages: [OpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } + + public static func recentMessages(in roomToken: String, on server: String) -> Promise<[Message]> { + // TODO: Recent vs. Since? + let request: Request = Request( + server: server, + endpoint: .roomMessagesRecent(roomToken) + // TODO: Limit? +// queryParameters: [ +// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } +// ].compactMapValues { $0 } + ) + + return send(request) + .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + .then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in + process(messages: messages, for: roomToken, on: server) + } + } + + public static func messagesBefore(messageId: Int64, in roomToken: String, on server: String) -> Promise<[Message]> { + // TODO: Recent vs. Since? + let request: Request = Request( + server: server, + endpoint: .roomMessagesBefore(roomToken, id: messageId) + // TODO: Limit? +// queryParameters: [ +// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } +// ].compactMapValues { $0 } + ) + + return send(request) + .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + .then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in + process(messages: messages, for: roomToken, on: server) + } + } + + public static func messagesSince(seqNo: Int64, in roomToken: String, on server: String) -> Promise<[Message]> { + // TODO: Recent vs. Since? + let request: Request = Request( + server: server, + endpoint: .roomMessagesSince(roomToken, seqNo: seqNo) + // TODO: Limit? +// queryParameters: [ +// .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } +// ].compactMapValues { $0 } + ) + + return send(request) + .decoded(as: [Message].self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + .then(on: OpenGroupAPIV2.workQueue) { messages -> Promise<[Message]> in + process(messages: messages, for: roomToken, on: server) + } + } + + // MARK: - Files + + // TODO: Shift this logic to the `OpenGroupManager` (makes more sense since it's not API logic) + public static func roomImage(_ fileId: Int64, for roomToken: String, on server: String) -> Promise { + // Normally the image for a given group is stored with the group thread, so it's only + // fetched once. However, on the join open group screen we show images for groups the + // user * hasn't * joined yet. We don't want to re-fetch these images every time the + // user opens the app because that could slow the app down or be data-intensive. So + // instead we assume that these images don't change that often and just fetch them once + // a week. We also assume that they're all fetched at the same time as well, so that + // we only need to maintain one date in user defaults. On top of all of this we also + // don't double up on fetch requests by storing the existing request as a promise if + // there is one. + let lastOpenGroupImageUpdate: Date? = UserDefaults.standard[.lastOpenGroupImageUpdate] + let now: Date = Date() + let timeSinceLastUpdate: TimeInterval = (given(lastOpenGroupImageUpdate) { now.timeIntervalSince($0) } ?? .greatestFiniteMagnitude) + let updateInterval: TimeInterval = (7 * 24 * 60 * 60) + + if let data = Storage.shared.getOpenGroupImage(for: roomToken, on: server), server == defaultServer, timeSinceLastUpdate < updateInterval { + return Promise.value(data) + } + + if let promise = groupImagePromises["\(server).\(roomToken)"] { + return promise + } + + let promise: Promise = downloadFile(fileId, from: roomToken, on: server) + _ = promise.done(on: OpenGroupAPIV2.workQueue) { imageData in + if server == defaultServer { + Storage.shared.write { transaction in + Storage.shared.setOpenGroupImage(to: imageData, for: roomToken, on: server, using: transaction) + } + UserDefaults.standard[.lastOpenGroupImageUpdate] = now + } + } + groupImagePromises["\(server).\(roomToken)"] = promise + + return promise + } + + public static func uploadFile(_ bytes: [UInt8], fileName: String? = nil, to roomToken: String, on server: String) -> Promise { + let request: Request = Request( + method: .post, + server: server, + endpoint: .roomFile(roomToken), + queryParameters: [ .fileName: fileName ].compactMapValues { $0 }, + body: Data(bytes) + ) + + return send(request) + .decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + /// 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) -> Promise { + let request: Request = Request( + method: .post, + server: server, + endpoint: .roomFileJson(roomToken), + queryParameters: [ .fileName: fileName ].compactMapValues { $0 }, + body: Data(base64Encoded: base64EncodedString) + ) + + return send(request) + .decoded(as: FileUploadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + public static func downloadFile(_ fileId: Int64, from roomToken: String, on server: String) -> Promise { + let request: Request = Request( + server: server, + endpoint: .roomFileIndividual(roomToken, fileId) + ) + + return send(request) + } + + public static func downloadFileJson(_ fileId: Int64, from roomToken: String, on server: String) -> Promise { + let request: Request = Request( + server: server, + endpoint: .roomFileIndividualJson(roomToken, fileId) + ) + + return send(request) + .decoded(as: FileDownloadResponse.self, on: OpenGroupAPIV2.workQueue, error: Error.parsingFailed) + } + + // MARK: - Processing + // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) + + private static func process(messages: [Message]?, for room: String, on server: String) -> Promise<[Message]> { + guard let messages: [Message] = messages, !messages.isEmpty else { return Promise.value([]) } let storage = SNMessagingKitConfiguration.shared.storage - let serverID: Int64 = (messages.compactMap { $0.serverID }.max() ?? 0) - let lastMessageServerID: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) + let seqNo: Int64 = (messages.compactMap { $0.seqNo }.max() ?? 0) + let lastMessageSeqNo: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) - if serverID > lastMessageServerID { - let (promise, seal) = Promise<[OpenGroupMessageV2]>.pending() + if seqNo > lastMessageSeqNo { + let (promise, seal) = Promise<[Message]>.pending() storage.write( with: { transaction in - storage.setLastMessageServerID(for: room, on: server, to: serverID, using: transaction) + storage.setLastMessageServerID(for: room, on: server, to: seqNo, using: transaction) }, completion: { seal.fulfill(messages) @@ -508,39 +493,6 @@ public final class OpenGroupAPIV2: NSObject { return Promise.value(messages) } - // MARK: - Message Deletion - - public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise { - let request: Request = Request( - verb: .delete, - room: room, - server: server, - endpoint: .messagesForServer(serverID) - ) - // TODO: Legacy version? - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } - } - - public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> { - let storage = SNMessagingKitConfiguration.shared.storage - - let request: Request = Request( - verb: .get, - room: room, - server: server, - endpoint: .deletedMessages, - queryParameters: [ - .fromServerId: storage.getLastDeletionServerID(for: room, on: server).map { String($0) } - ].compactMapValues { $0 } - ) - // TODO: Legacy version? - return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[Deletion]> in - let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed) - - return process(deletions: response.deletions, for: room, on: server) - } - } - private static func process(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { guard let deletions: [Deletion] = deletions else { return Promise.value([]) } @@ -565,80 +517,6 @@ public final class OpenGroupAPIV2: NSObject { return Promise.value(deletions) } - - // MARK: - Moderation - - public static func getModerators(for room: String, on server: String) -> Promise<[String]> { - let request: Request = Request( - verb: .get, - room: room, - server: server, - endpoint: .moderators - ) - // TODO: Legacy version? - return send(request) - .map(on: OpenGroupAPIV2.workQueue) { data in - let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed) - - if var x = self.moderators[server] { - x[room] = Set(response.moderators) - self.moderators[server] = x - } - else { - self.moderators[server] = [room: Set(response.moderators)] - } - - return response.moderators - } - } - - public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) - - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Promise(error: HTTP.Error.invalidJSON) - } - // TODO: Legacy version? - let request: Request = Request( - verb: .post, - room: room, - server: server, - endpoint: .blockList, - body: body - ) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } - } - - public static func banAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise { - let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) - - guard let body: Data = try? JSONEncoder().encode(requestBody) else { - return Promise(error: HTTP.Error.invalidJSON) - } - - // TODO: Legacy version? - let request: Request = Request( - verb: .post, - room: room, - server: server, - endpoint: .banAndDeleteAll, - body: body - ) - - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } - } - - public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise { - let request: Request = Request( - verb: .delete, - room: room, - server: server, - endpoint: .blockListIndividual(publicKey) - ) - // TODO: Legacy version? - return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } - } public static func isUserModerator(_ publicKey: String, for room: String, on server: String) -> Bool { return moderators[server]?[room]?.contains(publicKey) ?? false @@ -653,10 +531,19 @@ public final class OpenGroupAPIV2: NSObject { }, completion: { let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { - OpenGroupAPIV2.getAllRooms(from: defaultServer) + OpenGroupAPIV2.rooms(for: defaultServer) } _ = promise.done(on: OpenGroupAPIV2.workQueue) { items in - items.forEach { getGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() } + items + .compactMap { room -> (Int64, String)? in + guard let imageId: Int64 = room.imageId else { return nil} + + return (imageId, room.token) + } + .forEach { imageId, roomToken in + roomImage(imageId, for: roomToken, on: defaultServer) + .retainUntilComplete() + } } promise.catch(on: OpenGroupAPIV2.workQueue) { _ in OpenGroupAPIV2.defaultRoomsPromise = nil @@ -666,62 +553,331 @@ public final class OpenGroupAPIV2: NSObject { ) } - public static func getInfo(for room: String, on server: String) -> Promise { + // MARK: - Convenience + + private static func send(_ request: Request) -> Promise { + 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() + urlRequest.httpBody = request.body + + if request.useOnionRouting { + guard let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: request.server) else { + return Promise(error: Error.noPublicKey) + } + + if request.isAuthRequired { + // Determine if we should be using legacy auth for this endpoint + // TODO: Might need to store this at an OpenGroup level (so all requests can use the appropriate method). + if request.endpoint.useLegacyAuth { + // Because legacy auth happens on a per-room basis, we need to have a room to + // make an authenticated request + guard let room = request.room else { + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + } + + return legacyGetAuthToken(for: room, on: request.server) + .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in + urlRequest.setValue(authToken, forHTTPHeaderField: Header.authorization.rawValue) + + let promise = OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + promise.catch(on: OpenGroupAPIV2.workQueue) { error in + // A 401 means that we didn't provide a (valid) auth token for a route + // that required one. We use this as an indication that the token we're + // using has expired. Note that a 403 has a different meaning; it means + // that we provided a valid token but it doesn't have a high enough + // permission level for the route in question. + if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _, _) = error, statusCode == 401 { + let storage = SNMessagingKitConfiguration.shared.storage + + storage.writeSync { transaction in + storage.removeAuthToken(for: room, on: request.server, using: transaction) + } + } + } + + return promise + } + } + + // Attempt to sign the request with the new auth + guard let signedRequest: URLRequest = sign(urlRequest, with: publicKey) else { + return Promise(error: Error.signingFailed) + } + + // TODO: 'removeAuthToken' as a migration??? (would previously do this when getting a `401`). + return OnionRequestAPI.sendOnionRequest(signedRequest, to: request.server, using: publicKey) + } + + return OnionRequestAPI.sendOnionRequest(urlRequest, to: request.server, using: publicKey) + } + + preconditionFailure("It's currently not allowed to send non onion routed requests.") + } + + // MARK: - + // MARK: - + // MARK: - Legacy Requests (To be removed) + // TODO: Remove the legacy requests (should be unused once we release - just here for testing) + + public static var legacyDefaultRoomsPromise: Promise<[LegacyRoomInfo]>? + + // MARK: -- Legacy Auth + + private static func legacyGetAuthToken(for room: String, on server: String) -> Promise { + let storage = SNMessagingKitConfiguration.shared.storage + + if let authToken: String = storage.getAuthToken(for: room, on: server) { + return Promise.value(authToken) + } + + if let authTokenPromise: Promise = authTokenPromises.wrappedValue["\(server).\(room)"] { + return authTokenPromise + } + + let promise: Promise = legacyRequestNewAuthToken(for: room, on: server) + .then(on: OpenGroupAPIV2.workQueue) { legacyClaimAuthToken($0, for: room, on: server) } + .then(on: OpenGroupAPIV2.workQueue) { authToken -> Promise in + let (promise, seal) = Promise.pending() + storage.write(with: { transaction in + storage.setAuthToken(for: room, on: server, to: authToken, using: transaction) + }, completion: { + seal.fulfill(authToken) + }) + return promise + } + + promise + .done(on: OpenGroupAPIV2.workQueue) { _ in + authTokenPromises.wrappedValue["\(server).\(room)"] = nil + } + .catch(on: OpenGroupAPIV2.workQueue) { _ in + authTokenPromises.wrappedValue["\(server).\(room)"] = nil + } + + authTokenPromises.wrappedValue["\(server).\(room)"] = promise + return promise + } + + public static func legacyRequestNewAuthToken(for room: String, on server: String) -> Promise { + SNLog("Requesting auth token for server: \(server).") + guard let userKeyPair: ECKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { + return Promise(error: Error.generic) + } + let request: Request = Request( - verb: .get, - room: room, server: server, - endpoint: .roomInfo(room), + room: room, + endpoint: .legacyAuthTokenChallenge(legacyAuth: true), + queryParameters: [ + .publicKey: getUserHexEncodedPublicKey() + ], isAuthRequired: false ) - return send(request) - .map(on: OpenGroupAPIV2.workQueue) { data in - let response: GetInfoResponse = try data.decoded(as: GetInfoResponse.self, customError: Error.parsingFailed) + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response = try data.decoded(as: AuthTokenResponse.self, customError: Error.parsingFailed) + let symmetricKey = try AESGCM.generateSymmetricKey(x25519PublicKey: response.challenge.ephemeralPublicKey, x25519PrivateKey: userKeyPair.privateKey) + + guard let tokenAsData = try? AESGCM.decrypt(response.challenge.ciphertext, with: symmetricKey) else { + throw Error.decryptionFailed + } + + return tokenAsData.toHexString() + } + } + + public static func legacyClaimAuthToken(_ authToken: String, for room: String, on server: String) -> Promise { + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request: Request = Request( + method: .post, + server: server, + room: room, + endpoint: .legacyAuthTokenClaim(legacyAuth: true), + body: body, + headers: [ + // Set explicitly here because is isn't in the database yet at this point + .authorization: authToken + ], + isAuthRequired: false + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in authToken } + } + + /// Should be called when leaving a group. + public static func legacyDeleteAuthToken(for room: String, on server: String) -> Promise { + let request: Request = Request( + method: .delete, + server: server, + room: room, + endpoint: .legacyAuthToken(legacyAuth: true) + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in + let storage = SNMessagingKitConfiguration.shared.storage + + storage.write { transaction in + storage.removeAuthToken(for: room, on: server, using: transaction) + } + } + } + + // MARK: -- Legacy Requests + + public static func legacyCompactPoll(_ server: String) -> Promise { + let storage: SessionMessagingKitStorageProtocol = SNMessagingKitConfiguration.shared.storage + let rooms: [String] = storage.getAllV2OpenGroups().values + .filter { $0.server == server } + .map { $0.room } + var getAuthTokenPromises: [String: Promise] = [:] + let useMessageLimit = (hasPerformedInitialPoll[server] != true && timeSinceLastOpen > OpenGroupPollerV2.maxInactivityPeriod) + + hasPerformedInitialPoll[server] = true + + if !hasUpdatedLastOpenDate { + UserDefaults.standard[.lastOpen] = Date() + hasUpdatedLastOpenDate = true + } + + for room in rooms { + getAuthTokenPromises[room] = legacyGetAuthToken(for: room, on: server) + } + + let requestBody: LegacyCompactPollBody = LegacyCompactPollBody( + requests: rooms + .map { roomId -> LegacyCompactPollBody.Room in + LegacyCompactPollBody.Room( + id: roomId, + fromMessageServerId: (useMessageLimit ? nil : + storage.getLastMessageServerID(for: roomId, on: server) + ), + fromDeletionServerId: (useMessageLimit ? nil : + storage.getLastDeletionServerID(for: roomId, on: server) + ), + legacyAuthToken: nil + ) + } + ) + + return when(fulfilled: [Promise](getAuthTokenPromises.values)) + .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise in + let requestBodyWithAuthTokens: LegacyCompactPollBody = LegacyCompactPollBody( + requests: requestBody.requests.compactMap { oldRoom -> LegacyCompactPollBody.Room? in + guard let authToken: String = getAuthTokenPromises[oldRoom.id]?.value else { return nil } + + return LegacyCompactPollBody.Room( + id: oldRoom.id, + fromMessageServerId: oldRoom.fromMessageServerId, + fromDeletionServerId: oldRoom.fromDeletionServerId, + legacyAuthToken: authToken + ) + } + ) - return response.room + guard let body: Data = try? JSONEncoder().encode(requestBodyWithAuthTokens) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request = Request( + method: .post, + server: server, + endpoint: .legacyCompactPoll(legacyAuth: true), + body: body, + isAuthRequired: false + ) + + return send(request) + .then(on: OpenGroupAPIV2.workQueue) { data -> Promise in + let response: LegacyCompactPollResponse = try data.decoded(as: LegacyCompactPollResponse.self, customError: Error.parsingFailed) + + return when( + fulfilled: response.results + .compactMap { (result: LegacyCompactPollResponse.Result) -> Promise<[Deletion]>? in + // A 401 means that we didn't provide a (valid) auth token for a route that + // required one. We use this as an indication that the token we're using has + // expired. Note that a 403 has a different meaning; it means that we provided + // a valid token but it doesn't have a high enough permission level for the + // route in question. + guard result.statusCode != 401 else { + storage.writeSync { transaction in + storage.removeAuthToken(for: result.room, on: server, using: transaction) + } + + return nil + } + + return legacyProcess(messages: result.messages, for: result.room, on: server) + .then(on: OpenGroupAPIV2.workQueue) { _ -> Promise<[Deletion]> in + legacyProcess(deletions: result.deletions, for: result.room, on: server) + } + } + ).then(on: OpenGroupAPIV2.workQueue) { _ in Promise.value(response) } + } } } - public static func getAllRooms(from server: String) -> Promise<[RoomInfo]> { + public static func legacyGetDefaultRoomsIfNeeded() { + Storage.shared.write( + with: { transaction in + Storage.shared.setOpenGroupPublicKey(for: defaultServer, to: defaultServerPublicKey, using: transaction) + }, + completion: { + let promise = attempt(maxRetryCount: 8, recoveringOn: DispatchQueue.main) { + OpenGroupAPIV2.legacyGetAllRooms(from: defaultServer) + } + _ = promise.done(on: OpenGroupAPIV2.workQueue) { items in + items.forEach { legacyGetGroupImage(for: $0.id, on: defaultServer).retainUntilComplete() } + } + promise.catch(on: OpenGroupAPIV2.workQueue) { _ in + OpenGroupAPIV2.defaultRoomsPromise = nil + } + legacyDefaultRoomsPromise = promise + } + ) + } + + public static func legacyGetAllRooms(from server: String) -> Promise<[LegacyRoomInfo]> { let request: Request = Request( - verb: .get, - room: nil, server: server, - endpoint: .rooms, + endpoint: .legacyRooms, isAuthRequired: false ) return send(request) .map(on: OpenGroupAPIV2.workQueue) { data in - let response: RoomsResponse = try data.decoded(as: RoomsResponse.self, customError: Error.parsingFailed) + let response: LegacyRoomsResponse = try data.decoded(as: LegacyRoomsResponse.self, customError: Error.parsingFailed) return response.rooms } } - public static func getMemberCount(for room: String, on server: String) -> Promise { + public static func legacyGetRoomInfo(for room: String, on server: String) -> Promise { let request: Request = Request( - verb: .get, - room: room, server: server, - endpoint: .legacyMemberCount(legacyAuth: true) + room: room, + endpoint: .legacyRoomInfo(room), + isAuthRequired: false ) - // TODO: Non-legacy version? + return send(request) .map(on: OpenGroupAPIV2.workQueue) { data in - let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed) + let response: LegacyGetInfoResponse = try data.decoded(as: LegacyGetInfoResponse.self, customError: Error.parsingFailed) - let storage = SNMessagingKitConfiguration.shared.storage - storage.write { transaction in - storage.setUserCount(to: response.memberCount, forV2OpenGroupWithID: "\(server).\(room)", using: transaction) - } - - return response.memberCount + return response.room } } - public static func getGroupImage(for room: String, on server: String) -> Promise { + public static func legacyGetGroupImage(for room: String, on server: String) -> Promise { // Normally the image for a given group is stored with the group thread, so it's only // fetched once. However, on the join open group screen we show images for groups the // user * hasn't * joined yet. We don't want to re-fetch these images every time the @@ -745,15 +901,14 @@ public final class OpenGroupAPIV2: NSObject { } let request: Request = Request( - verb: .get, - room: room, server: server, - endpoint: .roomImage(room), + room: room, + endpoint: .legacyRoomImage(room), isAuthRequired: false ) - // TODO: Legacy version (doesn't work on new SOGS) + let promise: Promise = send(request).map(on: OpenGroupAPIV2.workQueue) { data in - let response: FileDownloadResponse = try data.decoded(as: FileDownloadResponse.self, customError: Error.parsingFailed) + let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed) if server == defaultServer { Storage.shared.write { transaction in @@ -768,4 +923,245 @@ public final class OpenGroupAPIV2: NSObject { return promise } + + public static func legacyGetMemberCount(for room: String, on server: String) -> Promise { + let request: Request = Request( + server: server, + room: room, + endpoint: .legacyMemberCount(legacyAuth: true) + ) + + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: MemberCountResponse = try data.decoded(as: MemberCountResponse.self, customError: Error.parsingFailed) + + let storage = SNMessagingKitConfiguration.shared.storage + storage.write { transaction in + storage.setUserCount(to: response.memberCount, forV2OpenGroupWithID: "\(server).\(room)", using: transaction) + } + + return response.memberCount + } + } + + // MARK: - Legacy File Storage + + public static func upload(_ file: Data, to room: String, on server: String) -> 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, room: room, endpoint: .legacyFiles, body: body) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response: LegacyFileUploadResponse = try data.decoded(as: LegacyFileUploadResponse.self, customError: Error.parsingFailed) + + return response.fileId + } + } + + public static func download(_ file: UInt64, from room: String, on server: String) -> Promise { + let request = Request(server: server, room: room, endpoint: .legacyFile(file)) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let response: LegacyFileDownloadResponse = try data.decoded(as: LegacyFileDownloadResponse.self, customError: Error.parsingFailed) + + return response.data + } + } + + // MARK: - Legacy Message Sending & Receiving + + public static func legacySend(_ message: OpenGroupMessageV2, to room: String, on server: String, with publicKey: String) -> Promise { + guard let signedMessage = message.sign(with: publicKey) else { return Promise(error: Error.signingFailed) } + guard let body: Data = try? JSONEncoder().encode(signedMessage) else { + return Promise(error: Error.parsingFailed) + } + let request = Request(method: .post, server: server, room: room, endpoint: .legacyMessages, body: body) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { data in + let message: OpenGroupMessageV2 = try data.decoded(as: OpenGroupMessageV2.self, customError: Error.parsingFailed) + Storage.shared.write { transaction in + Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction) + } + return message + } + } + + public static func legacyGetMessages(for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { + let storage = SNMessagingKitConfiguration.shared.storage + let request: Request = Request( + server: server, + room: room, + endpoint: .legacyMessages, + queryParameters: [ + .fromServerId: storage.getLastMessageServerID(for: room, on: server).map { String($0) } + ].compactMapValues { $0 } + ) + + return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[OpenGroupMessageV2]> in + let messages: [OpenGroupMessageV2] = try data.decoded(as: [OpenGroupMessageV2].self, customError: Error.parsingFailed) + + return legacyProcess(messages: messages, for: room, on: server) + } + } + + // MARK: - Legacy Message Deletion + + public static func deleteMessage(with serverID: Int64, from room: String, on server: String) -> Promise { + let request: Request = Request( + method: .delete, + server: server, + room: room, + endpoint: .legacyMessagesForServer(serverID) + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } + } + + public static func getDeletedMessages(for room: String, on server: String) -> Promise<[Deletion]> { + let storage = SNMessagingKitConfiguration.shared.storage + + let request: Request = Request( + server: server, + room: room, + endpoint: .legacyDeletedMessages, + queryParameters: [ + .fromServerId: storage.getLastDeletionServerID(for: room, on: server).map { String($0) } + ].compactMapValues { $0 } + ) + + return send(request).then(on: OpenGroupAPIV2.workQueue) { data -> Promise<[Deletion]> in + let response: DeletedMessagesResponse = try data.decoded(as: DeletedMessagesResponse.self, customError: Error.parsingFailed) + + return process(deletions: response.deletions, for: room, on: server) + } + } + + // MARK: - Legacy Moderation + + public static func getModerators(for room: String, on server: String) -> Promise<[String]> { + let request: Request = Request( + server: server, + room: room, + endpoint: .legacyModerators + ) + + return send(request) + .map(on: OpenGroupAPIV2.workQueue) { data in + let response: ModeratorsResponse = try data.decoded(as: ModeratorsResponse.self, customError: Error.parsingFailed) + + if var x = self.moderators[server] { + x[room] = Set(response.moderators) + self.moderators[server] = x + } + else { + self.moderators[server] = [room: Set(response.moderators)] + } + + return response.moderators + } + } + + public static func ban(_ publicKey: String, from room: String, on server: String) -> Promise { + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request: Request = Request( + method: .post, + server: server, + room: room, + endpoint: .legacyBlockList, + body: body + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } + } + + public static func banAndDeleteAllMessages(_ publicKey: String, from room: String, on server: String) -> Promise { + let requestBody: PublicKeyBody = PublicKeyBody(publicKey: getUserHexEncodedPublicKey()) + + guard let body: Data = try? JSONEncoder().encode(requestBody) else { + return Promise(error: HTTP.Error.invalidJSON) + } + + let request: Request = Request( + method: .post, + server: server, + room: room, + endpoint: .legacyBanAndDeleteAll, + body: body + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } + } + + public static func unban(_ publicKey: String, from room: String, on server: String) -> Promise { + let request: Request = Request( + method: .delete, + server: server, + room: room, + endpoint: .legacyBlockListIndividual(publicKey) + ) + + return send(request).map(on: OpenGroupAPIV2.workQueue) { _ in } + } + + // MARK: - Processing + // TODO: Move these methods to the OpenGroupManager? (seems odd for them to be in the API) + + private static func legacyProcess(messages: [OpenGroupMessageV2]?, for room: String, on server: String) -> Promise<[OpenGroupMessageV2]> { + guard let messages: [OpenGroupMessageV2] = messages, !messages.isEmpty else { return Promise.value([]) } + + let storage = SNMessagingKitConfiguration.shared.storage + let serverID: Int64 = (messages.compactMap { $0.serverID }.max() ?? 0) + let lastMessageServerID: Int64 = (storage.getLastMessageServerID(for: room, on: server) ?? 0) + + if serverID > lastMessageServerID { + let (promise, seal) = Promise<[OpenGroupMessageV2]>.pending() + + storage.write( + with: { transaction in + storage.setLastMessageServerID(for: room, on: server, to: serverID, using: transaction) + }, + completion: { + seal.fulfill(messages) + } + ) + + return promise + } + + return Promise.value(messages) + } + + private static func legacyProcess(deletions: [Deletion]?, for room: String, on server: String) -> Promise<[Deletion]> { + guard let deletions: [Deletion] = deletions else { return Promise.value([]) } + + let storage = SNMessagingKitConfiguration.shared.storage + let serverID: Int64 = (deletions.compactMap { $0.id }.max() ?? 0) + let lastDeletionServerID: Int64 = (storage.getLastDeletionServerID(for: room, on: server) ?? 0) + + if serverID > lastDeletionServerID { + let (promise, seal) = Promise<[Deletion]>.pending() + + storage.write( + with: { transaction in + storage.setLastDeletionServerID(for: room, on: server, to: serverID, using: transaction) + }, + completion: { + seal.fulfill(deletions) + } + ) + + return promise + } + + return Promise.value(deletions) + } } diff --git a/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift b/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift index 804a1b730..6ce29d141 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupManagerV2.swift @@ -41,42 +41,116 @@ public final class OpenGroupManagerV2 : NSObject { let transaction = transaction as! YapDatabaseReadWriteTransaction transaction.addCompletionQueue(DispatchQueue.global(qos: .userInitiated)) { // Get the group info - OpenGroupAPIV2.getInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { info in - // Create the open group model and the thread - let openGroup = OpenGroupV2(server: server, room: room, name: info.name, publicKey: publicKey, imageID: info.imageID) - let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) - let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) - // Store everything - storage.write(with: { transaction in - let transaction = transaction as! YapDatabaseReadWriteTransaction - let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.shouldBeVisible = true - thread.save(with: transaction) - storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) - }, completion: { - // Start the poller if needed - if OpenGroupManagerV2.shared.pollers[server] == nil { - let poller = OpenGroupPollerV2(for: server) - poller.startIfNeeded() - OpenGroupManagerV2.shared.pollers[server] = poller - } - // Fetch the group image - OpenGroupAPIV2.getGroupImage(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in - storage.write { transaction in - // Update the thread + // TODO: Remove this legacy method +// OpenGroupAPIV2.legacyGetRoomInfo(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { info in +// // Create the open group model and the thread +// let openGroup = OpenGroupV2(server: server, room: room, name: info.name, publicKey: publicKey, imageID: info.imageID) +// let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) +// let model = TSGroupModel(title: openGroup.name, memberIds: [ getUserHexEncodedPublicKey() ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: []) +// // Store everything +// storage.write(with: { transaction in +// let transaction = transaction as! YapDatabaseReadWriteTransaction +// let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) +// thread.shouldBeVisible = true +// thread.save(with: transaction) +// storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) +// }, completion: { +// // Start the poller if needed +// if OpenGroupManagerV2.shared.pollers[server] == nil { +// let poller = OpenGroupPollerV2(for: server) +// poller.startIfNeeded() +// OpenGroupManagerV2.shared.pollers[server] = poller +// } +// // Fetch the group image +// OpenGroupAPIV2.legacyGetGroupImage(for: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { data in +// storage.write { transaction in +// // Update the thread +// let transaction = transaction as! YapDatabaseReadWriteTransaction +// let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) +// thread.groupModel.groupImage = UIImage(data: data) +// thread.save(with: transaction) +// } +// }.retainUntilComplete() +// // Finish +// seal.fulfill(()) +// }) +// }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in +// seal.reject(error) +// } + + OpenGroupAPIV2.room(for: room, on: server) + .done(on: DispatchQueue.global(qos: .userInitiated)) { room in + // Create the open group model and the thread + let openGroup: OpenGroupV2 = OpenGroupV2( + server: server, + room: room.token, + name: room.name, + publicKey: publicKey, + imageID: room.imageId.map { "\($0)" } // TODO: Update this? + ) + + let groupID: Data = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id) + let model: TSGroupModel = TSGroupModel( + title: openGroup.name, + memberIds: [ getUserHexEncodedPublicKey() ], + image: nil, + groupId: groupID, + groupType: .openGroup, + adminIds: [] // TODO: This is part of the 'room' object + ) + + // Store everything + storage.write( + with: { transaction in let transaction = transaction as! YapDatabaseReadWriteTransaction let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) - thread.groupModel.groupImage = UIImage(data: data) + thread.shouldBeVisible = true thread.save(with: transaction) + storage.setV2OpenGroup(openGroup, for: thread.uniqueId!, using: transaction) + }, + completion: { + // Start the poller if needed + if OpenGroupManagerV2.shared.pollers[server] == nil { + let poller = OpenGroupPollerV2(for: server) + poller.startIfNeeded() + OpenGroupManagerV2.shared.pollers[server] = poller + } + + // Fetch the group image (if there is one) + // TODO: Need to test this + // TODO: Clean this up (can we avoid the if/else with fancy promise wrangling?) + if let imageId: Int64 = room.imageId { + OpenGroupAPIV2.roomImage(imageId, for: room.token, on: server) + .done(on: DispatchQueue.global(qos: .userInitiated)) { data in + storage.write { transaction in + // Update the thread + let transaction = transaction as! YapDatabaseReadWriteTransaction + let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) + thread.groupModel.groupImage = UIImage(data: data) + thread.save(with: transaction) + } + } + .retainUntilComplete() + } + else { + storage.write { transaction in + // Update the thread + let transaction = transaction as! YapDatabaseReadWriteTransaction + let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction) + thread.save(with: transaction) + } + } + + // Finish + seal.fulfill(()) } - }.retainUntilComplete() - // Finish - seal.fulfill(()) - }) - }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in - seal.reject(error) - } + ) + } + .catch(on: DispatchQueue.global(qos: .userInitiated)) { error in + seal.reject(error) + } } + return promise } @@ -102,7 +176,7 @@ public final class OpenGroupManagerV2 : NSObject { Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction) Storage.shared.removeLastMessageServerID(for: openGroup.room, on: openGroup.server, using: transaction) Storage.shared.removeLastDeletionServerID(for: openGroup.room, on: openGroup.server, using: transaction) - let _ = OpenGroupAPIV2.deleteAuthToken(for: openGroup.room, on: openGroup.server) + let _ = OpenGroupAPIV2.legacyDeleteAuthToken(for: openGroup.room, on: openGroup.server) thread.removeAllThreadInteractions(with: transaction) thread.remove(with: transaction) Storage.shared.removeV2OpenGroup(for: thread.uniqueId!, using: transaction) diff --git a/SessionMessagingKit/Open Groups/Types/Endpoint.swift b/SessionMessagingKit/Open Groups/Types/Endpoint.swift index ac6b4526b..aa4a55ca1 100644 --- a/SessionMessagingKit/Open Groups/Types/Endpoint.swift +++ b/SessionMessagingKit/Open Groups/Types/Endpoint.swift @@ -3,51 +3,154 @@ import Foundation enum Endpoint { - case files - case file(UInt64) + // Utility - case messages - case messagesForServer(Int64) - case deletedMessages + case onion + case batch + case sequence + case capabilities - case moderators - - case blockList - case blockListIndividual(String) - case banAndDeleteAll + // Rooms case rooms - case roomInfo(String) - case roomImage(String) + case room(String) + case roomPollInfo(String, Int64) + + // Messages + + case roomMessage(String) + case roomMessageIndividual(String, String) + case roomMessagesRecent(String) + case roomMessagesBefore(String, id: Int64) + case roomMessagesSince(String, seqNo: Int64) + + // Pinning + + case roomPinMessage(String, id: Int64) + case roomUnpinMessage(String, id: Int64) + case roomUnpinAll(String) + + // Files + + case roomFile(String) + case roomFileJson(String) + case roomFileIndividual(String, Int64) + case roomFileIndividualJson(String, Int64) + + // Users + + case userBan(String) + case userUnban(String) + case userPermission(String) + case userModerator(String) + case userDeleteMessages(String) // Legacy endpoints (to be deprecated and removed) + + case legacyFiles + case legacyFile(UInt64) + + case legacyMessages + case legacyMessagesForServer(Int64) + case legacyDeletedMessages + + case legacyModerators + + case legacyBlockList + case legacyBlockListIndividual(String) + case legacyBanAndDeleteAll + case legacyCompactPoll(legacyAuth: Bool) case legacyAuthToken(legacyAuth: Bool) case legacyAuthTokenChallenge(legacyAuth: Bool) case legacyAuthTokenClaim(legacyAuth: Bool) + + case legacyRooms + case legacyRoomInfo(String) + case legacyRoomImage(String) case legacyMemberCount(legacyAuth: Bool) var path: String { switch self { - case .files: return "files" - case .file(let fileId): return "files/\(fileId)" + // Utility - case .messages: return "messages" - case .messagesForServer(let serverId): return "messages/\(serverId)" - case .deletedMessages: return "deleted_messages" + case .onion: return "oxen/v4/lsrpc" + case .batch: return "batch" + case .sequence: return "sequence" + case .capabilities: return "capabilities" - case .moderators: return "moderators" - - case .blockList: return "block_list" - case .blockListIndividual(let publicKey): return "block_list/\(publicKey)" - case .banAndDeleteAll: return "ban_and_delete_all" + // Rooms case .rooms: return "rooms" - case .roomInfo(let roomName): return "rooms/\(roomName)" - case .roomImage(let roomName): return "rooms/\(roomName)/image" + case .room(let roomToken): return "room/\(roomToken)" + case .roomPollInfo(let roomToken, let infoUpdated): return "room/\(roomToken)/pollInfo/\(infoUpdated)" + + // Messages + + case .roomMessage(let roomToken): + return "room/\(roomToken)/message" + + case .roomMessageIndividual(let roomToken, let messageId): + return "room/\(roomToken)/message/\(messageId)" + + case .roomMessagesRecent(let roomToken): + return "room/\(roomToken)/messages/recent" + + case .roomMessagesBefore(let roomToken, let messageId): + return "room/\(roomToken)/messages/before/\(messageId)" + + case .roomMessagesSince(let roomToken, let seqNo): + return "room/\(roomToken)/messages/since/\(seqNo)" + + // Pinning + + case .roomPinMessage(let roomToken, let messageId): + return "room/\(roomToken)/pin/\(messageId)" + + case .roomUnpinMessage(let roomToken, let messageId): + return "room/\(roomToken)/unpin/\(messageId)" + + case .roomUnpinAll(let roomToken): + return "room/\(roomToken)/unpin/all" + + // 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 + let fileName: String = "" + return "room/\(roomToken)/file/\(fileId)/\(fileName)" + + case .roomFileIndividualJson(let roomToken, let fileId): + return "room/\(roomToken)/file/\(fileId)" + + // Users + + case .userBan(let sessionId): return "user/\(sessionId)/ban" + case .userUnban(let sessionId): return "user/\(sessionId)/unban" + case .userPermission(let sessionId): return "user/\(sessionId)/permission" + case .userModerator(let sessionId): return "user/\(sessionId)/moderator" + case .userDeleteMessages(let sessionId): return "user/\(sessionId)/deleteMessages" // Legacy endpoints (to be deprecated and removed) - // TODO: Look for a nicer way to prepend 'legacy'? (OnionRequestAPI messes with this but the new auth needs it to be correct...) + // TODO: Look for a nicer way to prepend 'legacy'? (OnionRequestAPI messes with this but the new auth needs it to be correct... ) + + + case .legacyFiles: return "legacy/files" + case .legacyFile(let fileId): return "legacy/files/\(fileId)" + + case .legacyMessages: return "legacy/messages" + case .legacyMessagesForServer(let serverId): return "legacy/messages/\(serverId)" + case .legacyDeletedMessages: return "legacy/deleted_messages" + + case .legacyModerators: return "legacy/moderators" + + case .legacyBlockList: return "legacy/block_list" + case .legacyBlockListIndividual(let publicKey): return "legacy/block_list/\(publicKey)" + case .legacyBanAndDeleteAll: return "legacy/ban_and_delete_all" + case .legacyCompactPoll(let useLegacyAuth): return "\(useLegacyAuth ? "" : "legacy/")compact_poll" @@ -60,6 +163,10 @@ enum Endpoint { case .legacyAuthTokenClaim(let useLegacyAuth): return "\(useLegacyAuth ? "" : "legacy/")claim_auth_token" + case .legacyRooms: return "legacy/rooms" + case .legacyRoomInfo(let roomName): return "legacy/rooms/\(roomName)" + case .legacyRoomImage(let roomName): return "legacy/rooms/\(roomName)/image" + case .legacyMemberCount(let useLegacyAuth): return "\(useLegacyAuth ? "" : "legacy/")member_count" } @@ -68,7 +175,11 @@ enum Endpoint { var useLegacyAuth: Bool { switch self { // File upload/download should use legacy auth - case .files, .file: return true + case .legacyFiles, .legacyFile, .legacyMessages, + .legacyMessagesForServer, .legacyDeletedMessages, + .legacyModerators, .legacyBlockList, + .legacyBlockListIndividual, .legacyBanAndDeleteAll: + return true case .legacyCompactPoll(let useLegacyAuth), .legacyAuthToken(let useLegacyAuth), @@ -76,6 +187,9 @@ enum Endpoint { .legacyAuthTokenClaim(let useLegacyAuth), .legacyMemberCount(let useLegacyAuth): return useLegacyAuth + + case .legacyRooms, .legacyRoomInfo, .legacyRoomImage: + return true default: return false } diff --git a/SessionMessagingKit/Open Groups/Types/Request.swift b/SessionMessagingKit/Open Groups/Types/Request.swift index cb66217c3..2e34adebc 100644 --- a/SessionMessagingKit/Open Groups/Types/Request.swift +++ b/SessionMessagingKit/Open Groups/Types/Request.swift @@ -1,12 +1,13 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import SessionUtilitiesKit extension OpenGroupAPIV2 { struct Request { - let verb: HTTP.Verb - let room: String? + let method: HTTP.Verb let server: String + let room: String? // TODO: Remove this? let endpoint: Endpoint let queryParameters: [QueryParam: String] let body: Data? @@ -17,9 +18,9 @@ extension OpenGroupAPIV2 { let useOnionRouting: Bool init( - verb: HTTP.Verb, - room: String?, + method: HTTP.Verb = .get, server: String, + room: String? = nil, endpoint: Endpoint, queryParameters: [QueryParam: String] = [:], body: Data? = nil, @@ -27,9 +28,9 @@ extension OpenGroupAPIV2 { isAuthRequired: Bool = true, useOnionRouting: Bool = true ) { - self.verb = verb - self.room = room + self.method = method self.server = server + self.room = room self.endpoint = endpoint self.queryParameters = queryParameters self.body = body @@ -39,19 +40,21 @@ extension OpenGroupAPIV2 { } var url: URL? { - guard verb == .get else { return URL(string: "\(server)/\(endpoint.path)") } + return URL(string: "\(server)\(urlPathAndParamsString)") + } + + var urlPathAndParamsString: String { + guard method == .get else { return "/\(endpoint.path)" } - return URL( - string: [ - "\(server)/\(endpoint.path)", - queryParameters - .map { key, value in "\(key.rawValue)=\(value)" } - .joined(separator: "&") - ] - .compactMap { $0 } - .filter { !$0.isEmpty } - .joined(separator: "?") - ) + return [ + "/\(endpoint.path)", + queryParameters + .map { key, value in "\(key.rawValue)=\(value)" } + .joined(separator: "&") + ] + .compactMap { $0 } + .filter { !$0.isEmpty } + .joined(separator: "?") } } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 8542ea923..7289420aa 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -97,8 +97,11 @@ public final class MessageSender : NSObject { // MARK: Convenience public static func send(_ message: Message, to destination: Message.Destination, using transaction: Any) -> Promise { switch destination { - case .contact(_), .closedGroup(_): return sendToSnodeDestination(destination, message: message, using: transaction) - case .openGroup(_, _), .openGroupV2(_, _): return sendToOpenGroupDestination(destination, message: message, using: transaction) + case .contact, .closedGroup: + return sendToSnodeDestination(destination, message: message, using: transaction) + + case .legacyOpenGroup, .openGroup: + return sendToOpenGroupDestination(destination, message: message, using: transaction) } } @@ -117,11 +120,13 @@ public final class MessageSender : NSObject { message.sentTimestamp = NSDate.millisecondTimestamp() } message.sender = userPublicKey + switch destination { - case .contact(let publicKey): message.recipient = publicKey - case .closedGroup(let groupPublicKey): message.recipient = groupPublicKey - case .openGroup(_, _), .openGroupV2(_, _): preconditionFailure() + case .contact(let publicKey): message.recipient = publicKey + case .closedGroup(let groupPublicKey): message.recipient = groupPublicKey + case .legacyOpenGroup, .openGroup: preconditionFailure() } + let isSelfSend = (message.recipient == userPublicKey) // Set the failure handler (need it here already for precondition failure handling) func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) { @@ -167,29 +172,41 @@ public final class MessageSender : NSObject { let ciphertext: Data do { switch destination { - case .contact(let publicKey): ciphertext = try encryptWithSessionProtocol(plaintext, for: publicKey) - case .closedGroup(let groupPublicKey): - guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { throw Error.noKeyPair } - ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey) - case .openGroup(_, _), .openGroupV2(_, _): preconditionFailure() + case .contact(let publicKey): + ciphertext = try encryptWithSessionProtocol(plaintext, for: publicKey) + + case .closedGroup(let groupPublicKey): + guard let encryptionKeyPair = Storage.shared.getLatestClosedGroupEncryptionKeyPair(for: groupPublicKey) else { + throw Error.noKeyPair + } + + ciphertext = try encryptWithSessionProtocol(plaintext, for: encryptionKeyPair.hexEncodedPublicKey) + + case .legacyOpenGroup, .openGroup: preconditionFailure() } - } catch { + } + catch { SNLog("Couldn't encrypt message for destination: \(destination) due to error: \(error).") handleFailure(with: error, using: transaction) return promise } + // Wrap the result let kind: SNProtoEnvelope.SNProtoEnvelopeType let senderPublicKey: String + switch destination { - case .contact(_): - kind = .sessionMessage - senderPublicKey = "" - case .closedGroup(let groupPublicKey): - kind = .closedGroupMessage - senderPublicKey = groupPublicKey - case .openGroup(_, _), .openGroupV2(_, _): preconditionFailure() + case .contact: + kind = .sessionMessage + senderPublicKey = "" + + case .closedGroup(let groupPublicKey): + kind = .closedGroupMessage + senderPublicKey = groupPublicKey + + case .legacyOpenGroup, .openGroup: preconditionFailure() } + let wrappedMessage: Data do { wrappedMessage = try MessageWrapper.wrap(type: kind, timestamp: message.sentTimestamp!, @@ -277,16 +294,27 @@ public final class MessageSender : NSObject { } switch destination { - case .contact(_): preconditionFailure() - case .closedGroup(_): preconditionFailure() - case .openGroup(let channel, let server): message.recipient = "\(server).\(channel)" - case .openGroupV2(let room, let server): message.recipient = "\(server).\(room)" + case .contact(_): preconditionFailure() + case .closedGroup(_): preconditionFailure() + case .legacyOpenGroup(let channel, let server): message.recipient = "\(server).\(channel)" + + case .openGroup(let room, let server, let whisperTo, let whisperMods, _): + message.recipient = [ + server, + room, + whisperTo, + (whisperTo == nil && whisperMods ? "mods" : nil) + ] + .compactMap { $0 } + .joined(separator: ".") } + // Set the failure handler (need it here already for precondition failure handling) func handleFailure(with error: Swift.Error, using transaction: YapDatabaseReadWriteTransaction) { MessageSender.handleFailedMessageSend(message, with: error, using: transaction) seal.reject(error) } + // Validate the message guard let message = message as? VisibleMessage else { #if DEBUG @@ -296,45 +324,84 @@ public final class MessageSender : NSObject { return promise #endif } - guard message.isValid else { handleFailure(with: Error.invalidMessage, using: transaction); return promise } + guard message.isValid else { + handleFailure(with: Error.invalidMessage, using: transaction) + return promise + } + // Attach the user's profile - guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise } + guard let name = storage.getUser()?.name else { + handleFailure(with: Error.noUsername, using: transaction) + return promise + } + if let profileKey = storage.getUser()?.profileEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL { message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL) - } else { + } + else { message.profile = VisibleMessage.Profile(displayName: name) } + // Convert it to protobuf - guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise } + guard let proto = message.toProto(using: transaction) else { + handleFailure(with: Error.protoConversionFailed, using: transaction) + return promise + } + // Serialize the protobuf let plaintext: Data + do { plaintext = (try proto.serializedData() as NSData).paddedMessageBody() - } catch { + } + catch { SNLog("Couldn't serialize proto due to error: \(error).") handleFailure(with: error, using: transaction) return promise } - // Send the result - guard case .openGroupV2(let room, let server) = destination else { preconditionFailure() } - // TODO: Determine if the 'getV2OpenGroup' call will cause issues - guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { preconditionFailure() } - let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!, - base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil) - OpenGroupAPIV2.send(openGroupMessage, to: room, on: server, with: openGroupV2.publicKey).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in - message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) } - storage.write(with: { transaction in - MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: openGroupMessage.sentTimestamp, using: transaction) - seal.fulfill(()) - }, completion: { }) - }.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in - storage.write(with: { transaction in - handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction) - }, completion: { }) + // Send the result + + guard case .openGroup(let room, let server, let whisperTo, let whisperMods, _) = destination else { + preconditionFailure() } - // Return - return promise + + // TODO: Determine if the 'getV2OpenGroup' call will cause issues. + guard let threadId: String = message.threadID, let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadId) else { + preconditionFailure() + } + +// let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!, +// base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil) + + //OpenGroupAPIV2.send(openGroupMessage, to: room, on: server, with: openGroupV2.publicKey) + return promise // TODO: Remove!!! +// OpenGroupAPIV2 +// .send( +// plaintext, +// to: room, +// on: server, +// whisperTo: whisperTo, +// whisperMods: whisperMods, +// with: openGroupV2.publicKey +// ) +// .done(on: DispatchQueue.global(qos: .userInitiated)) { response in +// print("RAWR") +//// message.openGroupServerMessageID = given(response.serverID) { UInt64($0) } +//// storage.write(with: { transaction in +//// Storage.shared.addReceivedMessageTimestamp(message.sentTimestamp, using: transaction) +//// +//// MessageSender.handleSuccessfulMessageSend(message, to: destination, serverTimestamp: response.sentTimestamp, using: transaction) +//// seal.fulfill(()) +//// }, completion: { }) +// } +// .catch(on: DispatchQueue.global(qos: .userInitiated)) { error in +// storage.write(with: { transaction in +// handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction) +// }, completion: { }) +// } +// // Return +// return promise } // MARK: Success & Failure Handling diff --git a/SessionMessagingKit/Utilities/Promise+Utilities.swift b/SessionMessagingKit/Utilities/Promise+Utilities.swift new file mode 100644 index 000000000..c75808c7a --- /dev/null +++ b/SessionMessagingKit/Utilities/Promise+Utilities.swift @@ -0,0 +1,12 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import PromiseKit + +extension Promise where T == Data { + func decoded(as type: R.Type, on queue: DispatchQueue? = nil, error: Error? = nil) -> Promise { + self.map(on: queue) { data -> R in + try data.decoded(as: type, customError: error) + } + } +} diff --git a/SessionMessagingKit/Utilities/String+Utlities.swift b/SessionMessagingKit/Utilities/String+Utlities.swift new file mode 100644 index 000000000..f97695f5e --- /dev/null +++ b/SessionMessagingKit/Utilities/String+Utlities.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +internal extension String { + func appending(_ other: String?) -> String { + guard let value: String = other else { return self } + + return self.appending(value) + } +} diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index e6f89cb85..57192182a 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -328,7 +328,7 @@ public enum OnionRequestAPI { // endpoint (in which case we need it to ensure the request signing works correctly // TODO: Confirm the 'removingPrefix' isn't going to break the request signing on non-legacy endpoints let endpoint: String = url.path - .removingPrefix("/", if: !url.path.starts(with: "/legacy")) +// .removingPrefix("/", if: !url.path.starts(with: "/legacy")) .appending(url.query.map { value in "?\(value)" }) let scheme: String? = url.scheme let port: UInt16? = url.port.map { UInt16($0) } @@ -382,90 +382,148 @@ public enum OnionRequestAPI { return seal.reject(error) } let destinationSymmetricKey = intermediate.destinationSymmetricKey - HTTP.execute(.post, url, body: body).done2 { json in - guard let base64EncodedIVAndCiphertext = json["result"] as? String, - let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidJSON) } - do { - let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey) - guard let json = try JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON, - let statusCode = json["status_code"] as? Int ?? json["status"] as? Int else { return seal.reject(HTTP.Error.invalidJSON) } - if statusCode == 406 { // Clock out of sync - SNLog("The user's clock is out of sync with the service node network.") - seal.reject(SnodeAPI.Error.clockOutOfSync) - } else if let bodyAsString = json["body"] as? String { - guard let bodyAsData = bodyAsString.data(using: .utf8), - let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { return seal.reject(HTTP.Error.invalidJSON) } - if let timestamp = body["t"] as? Int64 { - let offset = timestamp - Int64(NSDate.millisecondTimestamp()) - SnodeAPI.clockOffset = offset + + HTTP.execute(.post, url, body: body) + .done2 { json in + guard let base64EncodedIVAndCiphertext = json["result"] as? String, let ivAndCiphertext = Data(base64Encoded: base64EncodedIVAndCiphertext), ivAndCiphertext.count >= AESGCM.ivSize else { + return seal.reject(HTTP.Error.invalidJSON) + } + + do { + let data = try AESGCM.decrypt(ivAndCiphertext, with: destinationSymmetricKey) + + // The JSON data can be either an array or an object so can't cast to 'JSON' here + // TODO: Would be nice to ditch this 'JSONSerialization' behaviour if we can + guard let jsonObject = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) else { + return seal.reject(HTTP.Error.invalidJSON) } - guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: body, destination: destination)) + + // TODO: How do we now handle this case when the `status_code` is out of sync now that the value isn't provided? + // TODO: Upgrade to V4? + var customStatusCode: Int = 200 + + if let json: JSON = jsonObject as? JSON, let bodyStatusCode: Int = (((json["status_code"] as? Int) ?? json["status"] as? Int) ?? json["code"] as? Int) { + guard bodyStatusCode != 406 else { + SNLog("The user's clock is out of sync with the service node network.") + return seal.reject(SnodeAPI.Error.clockOutOfSync) + } + + customStatusCode = bodyStatusCode } - seal.fulfill(data) - } else { - guard 200...299 ~= statusCode else { - return seal.reject(Error.httpRequestFailedAtDestination(statusCode: UInt(statusCode), json: json, destination: destination)) + + if let json: JSON = jsonObject as? JSON, let bodyAsString: String = json["body"] as? String { + guard let bodyAsData = bodyAsString.data(using: .utf8), let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { + return seal.reject(HTTP.Error.invalidJSON) + } + + if let timestamp = body["t"] as? Int64 { + let offset = timestamp - Int64(NSDate.millisecondTimestamp()) + SnodeAPI.clockOffset = offset + } + + guard 200...299 ~= customStatusCode else { + return seal.reject( + Error.httpRequestFailedAtDestination( + statusCode: UInt(customStatusCode), + json: body, + destination: destination + ) + ) + } + + return seal.fulfill(data) } + + guard 200...299 ~= customStatusCode else { + return seal.reject( + Error.httpRequestFailedAtDestination( + statusCode: UInt(customStatusCode), + json: json, + destination: destination + ) + ) + } + seal.fulfill(data) } - } catch { + catch { + seal.reject(error) + } + } + .catch2 { error in seal.reject(error) } - }.catch2 { error in - seal.reject(error) - } }.catch2 { error in seal.reject(error) } } + promise.catch2 { error in // Must be invoked on Threading.workQueue - guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error, let guardSnode = guardSnode else { return } + guard case HTTP.Error.httpRequestFailed(let statusCode, let json) = error, let guardSnode = guardSnode else { + return + } + let path = paths.first { $0.contains(guardSnode) } + func handleUnspecificError() { guard let path = path else { return } + var pathFailureCount = OnionRequestAPI.pathFailureCount[path] ?? 0 pathFailureCount += 1 + if pathFailureCount >= pathFailureThreshold { dropGuardSnode(guardSnode) path.forEach { snode in SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw } + drop(path) - } else { + } + else { OnionRequestAPI.pathFailureCount[path] = pathFailureCount } } + let prefix = "Next node not found: " + if let message = json?["result"] as? String, message.hasPrefix(prefix) { let ed25519PublicKey = message[message.index(message.startIndex, offsetBy: prefix.count)..= snodeFailureThreshold { SnodeAPI.handleError(withStatusCode: statusCode, json: json, forSnode: snode) // Intentionally don't throw do { try drop(snode) - } catch { + } + catch { handleUnspecificError() } - } else { + } + else { OnionRequestAPI.snodeFailureCount[snode] = snodeFailureCount } } else { // Do nothing } - } else if let message = json?["result"] as? String, message == "Loki Server error" { + } + else if let message = json?["result"] as? String, message == "Loki Server error" { // Do nothing - } else if case .server(let host, _, _, _, _) = destination, host == "116.203.70.33" && statusCode == 0 { + } + else if case .server(let host, _, _, _, _) = destination, host == "116.203.70.33" && statusCode == 0 { // FIXME: Temporary thing to kick out nodes that can't talk to the V2 OGS yet handleUnspecificError() - } else if statusCode == 0 { // Timeout + } + else if statusCode == 0 { // Timeout // Do nothing - } else { + } + else { handleUnspecificError() } } + return promise } } diff --git a/SessionUtilitiesKit/Networking/HTTP.swift b/SessionUtilitiesKit/Networking/HTTP.swift index 5ce5e12ac..20ba08101 100644 --- a/SessionUtilitiesKit/Networking/HTTP.swift +++ b/SessionUtilitiesKit/Networking/HTTP.swift @@ -68,7 +68,7 @@ public enum HTTP { } // MARK: Verb - public enum Verb : String { + public enum Verb: String, Codable { case get = "GET" case put = "PUT" case post = "POST"