Refactored the remaining references to PromiseKit

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Media Viewing & Editing/PhotoCapture.swift
#	SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift
#	SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift
#	SessionMessagingKit/Utilities/Promise+Utilities.swift
#	SessionShareExtension/ShareVC.swift
#	SessionShareExtension/ThreadPickerVC.swift
#	SessionSnodeKit/OnionRequestAPI+Encryption.swift
#	SessionSnodeKit/OnionRequestAPI.swift
#	SessionUtilitiesKit/Database/Storage.swift
#	SessionUtilitiesKit/Networking/HTTP.swift
This commit is contained in:
Morgan Pretty 2022-12-05 13:52:39 +11:00
parent 8b37002d89
commit 6970ff22cc
36 changed files with 763 additions and 1525 deletions

View File

@ -107,7 +107,6 @@
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; };
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; };
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */; };
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */; };
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.swift */; };
@ -344,7 +343,6 @@
C33FDDC5255A582000E217F9 /* OWSError.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDC0B255A581D00E217F9 /* OWSError.m */; };
C33FDDCC255A582000E217F9 /* TSConstants.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC12255A581E00E217F9 /* TSConstants.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDDD0255A582000E217F9 /* FunctionalUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDC16255A581E00E217F9 /* FunctionalUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */; };
C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */; };
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D72553860B00C340D1 /* AESGCM.swift */; };
@ -433,11 +431,8 @@
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; };
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; };
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; };
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; };
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; };
C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; };
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; };
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.swift */; };
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */; };
@ -452,8 +447,6 @@
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */; };
C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */; };
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */; };
C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */; };
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D02553860800C340D1 /* Promise+Threading.swift */; };
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; };
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.swift */; };
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */; };
@ -604,6 +597,28 @@
FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.swift */; };
FD26FA512919F9CE005801D8 /* GroupDeleteMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA502919F9CE005801D8 /* GroupDeleteMessage.swift */; };
FD26FA53291CACA9005801D8 /* BatchResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4386A27B4E88F00C60D73 /* BatchResponse.swift */; };
FD26FA54291CAD31005801D8 /* Request.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD83B9CD27D17A04005E1583 /* Request.swift */; };
FD26FA55291CAD44005801D8 /* HTTPQueryParam.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385027B4807400C60D73 /* HTTPQueryParam.swift */; };
FD26FA57291CADAE005801D8 /* HTTPQueryParam+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA56291CADAE005801D8 /* HTTPQueryParam+OpenGroup.swift */; };
FD26FA58291CAE38005801D8 /* HTTPHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4384E27B4804F00C60D73 /* HTTPHeader.swift */; };
FD26FA5A291CAE9B005801D8 /* HTTPHeader+OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA59291CAE9B005801D8 /* HTTPHeader+OpenGroup.swift */; };
FD26FA5E291CAFF9005801D8 /* Data+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA5D291CAFF9005801D8 /* Data+Utilities.swift */; };
FD26FA60291CB098005801D8 /* ResponseInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA5F291CB098005801D8 /* ResponseInfo.swift */; };
FD26FA62291CB46D005801D8 /* RequestInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC438B027BB159600C60D73 /* RequestInfo.swift */; };
FD26FA66291CC981005801D8 /* HTTPError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA65291CC981005801D8 /* HTTPError.swift */; };
FD26FA68291CC99E005801D8 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA67291CC99E005801D8 /* HTTPMethod.swift */; };
FD26FA6B291DA6BC005801D8 /* SSKDependencies.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6A291DA6BC005801D8 /* SSKDependencies.swift */; };
FD26FA6D291DADAE005801D8 /* SnodeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6C291DADAE005801D8 /* SnodeRequest.swift */; };
FD26FA6F291DB171005801D8 /* ONSResolveRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA6E291DB171005801D8 /* ONSResolveRequest.swift */; };
FD26FA71291DB253005801D8 /* OxenDaemonRPCRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA70291DB253005801D8 /* OxenDaemonRPCRequest.swift */; };
FD26FA73291DB5F3005801D8 /* SnodeBatchRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA72291DB5F3005801D8 /* SnodeBatchRequest.swift */; };
FD26FA75291DBC8B005801D8 /* ONSResolveResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA74291DBC8B005801D8 /* ONSResolveResponse.swift */; };
FD26FA77291DE2C7005801D8 /* SnodeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA76291DE2C7005801D8 /* SnodeResponse.swift */; };
FD26FA79291DEDD7005801D8 /* GetMessagesRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA78291DEDD7005801D8 /* GetMessagesRequest.swift */; };
FD26FA7B291DF8F3005801D8 /* SnodeAPINamespace.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA7A291DF8F3005801D8 /* SnodeAPINamespace.swift */; };
FD26FA7D291E0B10005801D8 /* GetMessagesResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD26FA7C291E0B10005801D8 /* GetMessagesResponse.swift */; };
FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAEE28ED3E1100A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAF028ED57B500A49611 /* SynchronousStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD2AAAEF28ED57B500A49611 /* SynchronousStorage.swift */; };
@ -788,6 +803,7 @@
FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */; };
FD8ECF922938552800C0D1BB /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF912938552800C0D1BB /* Threading.swift */; };
FD8ECF94293856AF00C0D1BB /* Randomness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF93293856AF00C0D1BB /* Randomness.swift */; };
FD8ECFA1293D8FDD00C0D1BB /* URLResponse+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */; };
FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; };
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.swift */; };
FD9004142818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */; };
@ -1208,7 +1224,6 @@
7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiPickerCollectionView.swift; sourceTree = "<group>"; };
7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EmojiSkinTonePicker.swift; sourceTree = "<group>"; };
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSENotificationPresenter.swift; sourceTree = "<group>"; };
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Timeout.swift"; sourceTree = "<group>"; };
7B1D74AF27C365960030B423 /* Timer+MainThread.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Timer+MainThread.swift"; sourceTree = "<group>"; };
7B2DB2AD26F1B0FF0035B509 /* si */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = si; path = si.lproj/Localizable.strings; sourceTree = "<group>"; };
7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AllMediaViewController.swift; sourceTree = "<group>"; };
@ -1577,8 +1592,6 @@
C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebSocketResources.pb.swift; sourceTree = "<group>"; };
C3A71D662558A0170043A11F /* DiffieHellman.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DiffieHellman.swift; sourceTree = "<group>"; };
C3A71F882558BA9F0043A11F /* Mnemonic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Mnemonic.swift; sourceTree = "<group>"; };
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; };
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Retaining.swift"; sourceTree = "<group>"; };
C3A8AF752665B03900A467FE /* hi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = hi; path = hi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3A8AF762665F97A00A467FE /* fi */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fi; path = fi.lproj/Localizable.strings; sourceTree = "<group>"; };
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
@ -1597,14 +1610,10 @@
C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Notification+OnionRequestAPI.swift"; sourceTree = "<group>"; };
C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SnodeAPI.swift; sourceTree = "<group>"; };
C3C2A5CE2553860700C340D1 /* Logging.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Logging.swift; sourceTree = "<group>"; };
C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Hashing.swift"; sourceTree = "<group>"; };
C3C2A5D02553860800C340D1 /* Promise+Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Threading.swift"; sourceTree = "<group>"; };
C3C2A5D12553860800C340D1 /* Array+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Array+Utilities.swift"; sourceTree = "<group>"; };
C3C2A5D22553860900C340D1 /* String+Trimming.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String+Trimming.swift"; sourceTree = "<group>"; };
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Delaying.swift"; sourceTree = "<group>"; };
C3C2A5D42553860A00C340D1 /* Threading.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
C3C2A5D52553860A00C340D1 /* Dictionary+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Dictionary+Utilities.swift"; sourceTree = "<group>"; };
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Promise+Retrying.swift"; sourceTree = "<group>"; };
C3C2A5D72553860B00C340D1 /* AESGCM.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AESGCM.swift; sourceTree = "<group>"; };
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Data+Utilities.swift"; sourceTree = "<group>"; };
C3C2A5D92553860B00C340D1 /* JSON.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = JSON.swift; sourceTree = "<group>"; };
@ -1907,6 +1916,7 @@
FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SessionUtil+UserProfile.swift"; sourceTree = "<group>"; };
FD8ECF912938552800C0D1BB /* Threading.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Threading.swift; sourceTree = "<group>"; };
FD8ECF93293856AF00C0D1BB /* Randomness.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Randomness.swift; sourceTree = "<group>"; };
FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "URLResponse+Utilities.swift"; sourceTree = "<group>"; };
FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GetSnodePoolJob.swift; sourceTree = "<group>"; };
FD9004132818AD0B00ABAAF6 /* _002_SetupStandardJobs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_SetupStandardJobs.swift; sourceTree = "<group>"; };
FDA8EAFD280E8B78002B68E5 /* FailedMessageSendsJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedMessageSendsJob.swift; sourceTree = "<group>"; };
@ -1948,7 +1958,6 @@
FDC4386827B4E6B700C60D73 /* String+Utlities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Utlities.swift"; sourceTree = "<group>"; };
FDC4386A27B4E88F00C60D73 /* BatchRequestInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchRequestInfo.swift; sourceTree = "<group>"; };
FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileUploadResponse.swift; sourceTree = "<group>"; };
FDC4387327B5BB9B00C60D73 /* Promise+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Utilities.swift"; sourceTree = "<group>"; };
FDC4387727B5C35400C60D73 /* SendMessageRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequest.swift; sourceTree = "<group>"; };
FDC4388E27B9FFC700C60D73 /* SessionMessagingKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = SessionMessagingKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPISpec.swift; sourceTree = "<group>"; };
@ -2522,18 +2531,6 @@
path = Crypto;
sourceTree = "<group>";
};
B8A582AD258C655E00AFD84C /* PromiseKit */ = {
isa = PBXGroup;
children = (
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */,
C3C2A5D32553860900C340D1 /* Promise+Delaying.swift */,
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */,
C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */,
7B1D74AB27BDE7510030B423 /* Promise+Timeout.swift */,
);
path = PromiseKit;
sourceTree = "<group>";
};
B8A582AE258C65D000AFD84C /* Networking */ = {
isa = PBXGroup;
children = (
@ -2542,6 +2539,8 @@
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */,
C3C2A5D92553860B00C340D1 /* JSON.swift */,
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */,
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */,
);
path = Networking;
sourceTree = "<group>";
@ -3252,8 +3251,6 @@
isa = PBXGroup;
children = (
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */,
C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */,
C3C2A5D02553860800C340D1 /* Promise+Threading.swift */,
C3C2A5D22553860900C340D1 /* String+Trimming.swift */,
C3C2A5D42553860A00C340D1 /* Threading.swift */,
);
@ -3271,7 +3268,6 @@
FD9004102818ABB000ABAAF6 /* JobRunner */,
B8A582AF258C665E00AFD84C /* Media */,
B8A582AE258C65D000AFD84C /* Networking */,
B8A582AD258C655E00AFD84C /* PromiseKit */,
FD09796527F6B0A800936362 /* Utilities */,
FD37E9FE28A5F2CD003AE748 /* Configuration.swift */,
);
@ -5370,11 +5366,19 @@
C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */,
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */,
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */,
FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */,
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */,
C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */,
FD26FA7B291DF8F3005801D8 /* SnodeAPINamespace.swift in Sources */,
FD8ECF6E292C9EA100C0D1BB /* GetServiceNodesRequest.swift in Sources */,
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */,
FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */,
C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */,
FD8ECF56292C327700C0D1BB /* LegacyGetMessagesRequest.swift in Sources */,
FD8ECF54292C2DB000C0D1BB /* SnodeAuthenticatedRequestBody.swift in Sources */,
FD26FA6D291DADAE005801D8 /* SnodeRequest.swift in Sources */,
FD8ECF50292C2B2B00C0D1BB /* DeleteAllBeforeRequest.swift in Sources */,
FD8ECF5A292C431B00C0D1BB /* GetSwarmRequest.swift in Sources */,
C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */,
FD17D7A727F41AF000122BE0 /* SSKLegacy.swift in Sources */,
FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */,
@ -5395,7 +5399,6 @@
buildActionMask = 2147483647;
files = (
FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */,
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */,
C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */,
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */,
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
@ -5422,7 +5425,7 @@
FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */,
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */,
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */,
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */,
FD26FA53291CACA9005801D8 /* BatchResponse.swift in Sources */,
FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
@ -5440,6 +5443,7 @@
FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */,
FD09796B27F6C67500936362 /* Failable.swift in Sources */,
FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */,
FD8ECFA1293D8FDD00C0D1BB /* URLResponse+Utilities.swift in Sources */,
FD705A92278D051200F16121 /* ReusableView.swift in Sources */,
FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */,
FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */,
@ -5458,14 +5462,12 @@
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */,
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */,
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */,
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
FD8ECF922938552800C0D1BB /* Threading.swift in Sources */,
B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */,
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */,
@ -5492,8 +5494,7 @@
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */,
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */,
FDFD645B27F26D4600808CA1 /* Data+Utilities.swift in Sources */,
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
FD26FA5E291CAFF9005801D8 /* Data+Utilities.swift in Sources */,
FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */,
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
);

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import CallKit
import GRDB
import WebRTC
@ -223,6 +224,7 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
.inserted(db)
self.callInteractionId = interaction?.id
try? self.webRTCSession
.sendPreOffer(
db,
@ -230,14 +232,19 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
interactionId: interaction?.id,
in: thread
)
.done { [weak self] _ in
Storage.shared.writeAsync { db in
self?.webRTCSession.sendOffer(db, to: sessionId)
.sinkUntilComplete(
receiveCompletion: { [weak self] result in
switch result {
case .failure: break
case .finished:
Storage.shared.writeAsync { db in
self?.webRTCSession.sendOffer(db, to: sessionId)
}
self?.setupTimeoutTimer()
}
}
self?.setupTimeoutTimer()
}
.retainUntilComplete()
)
}
func answerSessionCall() {
@ -406,8 +413,8 @@ public final class SessionCall: CurrentCallProtocol, WebRTCSessionDelegate {
let webRTCSession: WebRTCSession = self.webRTCSession
Storage.shared
.read { db in webRTCSession.sendOffer(db, to: sessionId, isRestartingICEConnection: true) }
.retainUntilComplete()
.readPublisherFlatMap { db in webRTCSession.sendOffer(db, to: sessionId, isRestartingICEConnection: true) }
.sinkUntilComplete()
}
// MARK: - Timeout

View File

@ -345,23 +345,24 @@ extension ConversationVC:
dataSource: dataSource,
dataUTI: kUTTypeMPEG4 as String
)
.attachmentPromise
.done { attachment in
guard
!modalActivityIndicator.wasCancelled,
let attachment = attachment as? SignalAttachment
else { return }
modalActivityIndicator.dismiss {
guard !attachment.hasError else {
self?.showErrorAlert(for: attachment, onDismiss: nil)
return
}
.attachmentPublisher
.sinkUntilComplete(
receiveValue: { [weak self] attachment in
guard
!modalActivityIndicator.wasCancelled,
let attachment = attachment as? SignalAttachment
else { return }
self?.showAttachmentApprovalDialog(for: [ attachment ])
modalActivityIndicator.dismiss {
guard !attachment.hasError else {
self?.showErrorAlert(for: attachment, onDismiss: nil)
return
}
self?.showAttachmentApprovalDialog(for: [ attachment ])
}
}
}
.retainUntilComplete()
)
}
}
@ -422,8 +423,8 @@ extension ConversationVC:
)
// Send the message
Storage.shared.writeAsync(
updates: { [weak self] db in
Storage.shared
.writePublisher { [weak self] db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
return
}
@ -485,11 +486,12 @@ extension ConversationVC:
interaction: interaction,
in: thread
)
},
completion: { [weak self] _, _ in
self?.handleMessageSent()
}
)
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent()
}
)
}
func sendAttachments(_ attachments: [SignalAttachment], with text: String, hasPermissionToSendSeed: Bool = false, onComplete: (() -> ())? = nil) {
@ -545,8 +547,8 @@ extension ConversationVC:
)
// Send the message
Storage.shared.writeAsync(
updates: { [weak self] db in
Storage.shared
.writePublisher { [weak self] db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
return
}
@ -574,23 +576,36 @@ extension ConversationVC:
.asRequest(of: TimeInterval.self)
.fetchOne(db)
).inserted(db)
try MessageSender.send(
db,
interaction: interaction,
with: attachments,
in: thread
)
},
completion: { [weak self] _, _ in
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
DispatchQueue.main.async {
onComplete?()
guard let interactionId: Int64 = interaction.id else {
return
}
// Prepare any attachments
try Attachment.prepare(
db,
attachments: attachments,
for: interactionId
)
// Prepare the message send data
try MessageSender
.send(
db,
interaction: interaction,
in: thread
)
}
)
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen
DispatchQueue.main.async {
onComplete?()
}
}
)
}
func handleMessageSent() {
@ -1419,7 +1434,7 @@ extension ConversationVC:
.eraseToAnyPublisher()
}
return MessageSender.sendImmediate(data: sendData)
return MessageSender.sendImmediate(preparedSendData: sendData)
}
.sinkUntilComplete()
}

View File

@ -449,11 +449,11 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
// Initially position offscreen, we'll animate it in.
collectionPickerView.frame = collectionPickerView.frame.offsetBy(dx: 0, dy: collectionPickerView.frame.height)
UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) {
UIView.animate(withDuration: 0.25) {
collectionPickerView.superview?.layoutIfNeeded()
self.titleView.rotateIcon(.up)
}.retainUntilComplete()
}
}
func hideCollectionPicker() {
@ -461,14 +461,18 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
assert(isShowingCollectionPickerController)
isShowingCollectionPickerController = false
UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) {
self.collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height)
self.titleView.rotateIcon(.down)
}.done { _ in
self.collectionPickerController.view.removeFromSuperview()
self.collectionPickerController.removeFromParent()
}.retainUntilComplete()
UIView.animate(
withDuration: 0.25,
animations: {
self.collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height)
self.titleView.rotateIcon(.down)
},
completion: { [weak self] _ in
self?.collectionPickerController.view.removeFromSuperview()
self?.collectionPickerController.removeFromParent()
}
)
}
// MARK: - UICollectionView

View File

@ -4,7 +4,7 @@ import Foundation
import Combine
import AVFoundation
import CoreServices
import SignalCoreKit
import SessionUtilitiesKit
protocol PhotoCaptureDelegate: AnyObject {
func photoCapture(_ photoCapture: PhotoCapture, didFinishProcessingAttachment attachment: SignalAttachment)
@ -368,14 +368,23 @@ extension PhotoCapture: CaptureButtonDelegate {
AssertIsOnMainThread()
Logger.verbose("")
sessionQueue.async(.promise) {
try self.startAudioCapture()
self.captureOutput.beginVideo(delegate: self)
}.done {
self.delegate?.photoCaptureDidBeginVideo(self)
}.catch { error in
self.delegate?.photoCapture(self, processingDidError: error)
}.retainUntilComplete()
Just(())
.subscribe(on: sessionQueue)
.sinkUntilComplete(
receiveCompletion: { [weak self] _ in
guard let strongSelf = self else { return }
do {
try strongSelf.startAudioCapture()
strongSelf.captureOutput.beginVideo(delegate: strongSelf)
strongSelf.delegate?.photoCaptureDidBeginVideo(strongSelf)
}
catch {
strongSelf.delegate?.photoCapture(strongSelf, processingDidError: error)
}
}
)
}
func didCompleteLongPressCaptureButton(_ captureButton: CaptureButton) {

View File

@ -1,16 +1,13 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import UIKit
import Combine
import UserNotifications
import GRDB
import PromiseKit
import WebRTC
import SessionUIKit
import UIKit
import SessionMessagingKit
import SessionUtilitiesKit
import SessionUIKit
import UserNotifications
import UIKit
import SignalUtilitiesKit
import SignalCoreKit

View File

@ -559,15 +559,14 @@ class NotificationActionHandler {
includingOlder: true,
trySendReadReceipt: true
)
// TODO: Will need to split the attachment upload from the message preparation logic
return try MessageSender.preparedSendData(
db,
interaction: interaction,
in: thread
)
}
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.handleEvents(
receiveCompletion: { result in
switch result {

View File

@ -1,6 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import WebRTC
import SessionUtilitiesKit
@ -21,7 +22,7 @@ extension WebRTCSession {
else {
guard sdp.type == .offer else { return }
self?.sendAnswer(to: sessionId).retainUntilComplete()
self?.sendAnswer(to: sessionId).sinkUntilComplete()
}
})
}

View File

@ -1,8 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import Combine
import GRDB
import PromiseKit
import WebRTC
import SessionUtilitiesKit
@ -80,7 +80,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
// MARK: - Error
public enum Error : LocalizedError {
public enum WebRTCSessionError: LocalizedError {
case noThread
public var errorDescription: String? {
@ -124,94 +124,48 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
message: CallMessage,
interactionId: Int64?,
in thread: SessionThread
) throws -> Promise<Void> {
) throws -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending pre-offer message.")
return try MessageSender
.sendNonDurably(
db,
message: message,
interactionId: interactionId,
in: thread
return MessageSender
.sendImmediate(
preparedSendData: try MessageSender
.preparedSendData(
db,
message: message,
to: try Message.Destination.from(db, thread: thread),
interactionId: interactionId
)
)
.done2 {
SNLog("[Calls] Pre-offer message has been sent.")
}
.handleEvents(
receiveCompletion: { result in
switch result {
case .failure: break
case .finished: SNLog("[Calls] Pre-offer message has been sent.")
}
}
)
.eraseToAnyPublisher()
}
public func sendOffer(
_ db: Database,
to sessionId: String,
isRestartingICEConnection: Bool = false
) -> Promise<Void> {
) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending offer message.")
let (promise, seal) = Promise<Void>.pending()
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection)
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
return Promise(error: Error.noThread)
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
}
self.peerConnection?.offer(for: mediaConstraints) { [weak self] sdp, error in
if let error = error {
seal.reject(error)
return
}
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
}
self?.peerConnection?.setLocalDescription(sdp) { error in
return Future<Void, Error> { [weak self] resolver in
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
if let error = error {
print("Couldn't initiate call due to error: \(error).")
return seal.reject(error)
}
}
Storage.shared
.writeAsync { db in
try MessageSender
.sendNonDurably(
db,
message: CallMessage(
uuid: uuid,
kind: .offer,
sdps: [ sdp.sdp ],
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000))
),
interactionId: nil,
in: thread
)
}
.done2 {
seal.fulfill(())
}
.catch2 { error in
seal.reject(error)
}
.retainUntilComplete()
}
return promise
}
public func sendAnswer(to sessionId: String) -> Promise<Void> {
SNLog("[Calls] Sending answer message.")
let (promise, seal) = Promise<Void>.pending()
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
Storage.shared.writeAsync { [weak self] db in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
seal.reject(Error.noThread)
return
}
self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in
if let error = error {
seal.reject(error)
resolver(Result.failure(error))
return
}
@ -221,33 +175,103 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't accept call due to error: \(error).")
return seal.reject(error)
print("Couldn't initiate call due to error: \(error).")
resolver(Result.failure(error))
return
}
}
try? MessageSender
.sendNonDurably(
db,
message: CallMessage(
uuid: uuid,
kind: .answer,
sdps: [ sdp.sdp ]
),
interactionId: nil,
in: thread
Storage.shared
.writePublisher { db in
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .offer,
sdps: [ sdp.sdp ],
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000))
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
}
}
)
.done2 {
seal.fulfill(())
}
.catch2 { error in
seal.reject(error)
}
.retainUntilComplete()
}
}
.eraseToAnyPublisher()
}
public func sendAnswer(to sessionId: String) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending answer message.")
let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
return promise
return Storage.shared
.readPublisherFlatMap { db -> AnyPublisher<SessionThread, Error> in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
}
return Just(thread)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.flatMap { [weak self] thread in
Future<Void, Error> { resolver in
self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in
if let error = error {
resolver(Result.failure(error))
return
}
guard let sdp: RTCSessionDescription = self?.correctSessionDescription(sdp: sdp) else {
preconditionFailure()
}
self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error {
print("Couldn't accept call due to error: \(error).")
return resolver(Result.failure(error))
}
}
Storage.shared
.writePublisher { db in
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .answer,
sdps: [ sdp.sdp ]
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil
)
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
}
}
)
}
}
}
.eraseToAnyPublisher()
}
private func queueICECandidateForSending(_ candidate: RTCIceCandidate) {
@ -268,26 +292,36 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
// Empty the queue
self.queuedICECandidates.removeAll()
Storage.shared.writeAsync { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else { return }
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
try MessageSender.sendNonDurably(
db,
message: CallMessage(
uuid: uuid,
kind: .iceCandidates(
sdpMLineIndexes: candidates.map { UInt32($0.sdpMLineIndex) },
sdpMids: candidates.map { $0.sdpMid! }
),
sdps: candidates.map { $0.sdp }
),
interactionId: nil,
in: thread
)
.retainUntilComplete()
}
Storage.shared
.writePublisherFlatMap { db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else {
return Fail(error: WebRTCSessionError.noThread)
.eraseToAnyPublisher()
}
SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
return Just(
try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: uuid,
kind: .iceCandidates(
sdpMLineIndexes: candidates.map { UInt32($0.sdpMLineIndex) },
sdpMids: candidates.map { $0.sdpMid! }
),
sdps: candidates.map { $0.sdp }
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil
)
)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete()
}
public func endCall(_ db: Database, with sessionId: String) throws {
@ -295,17 +329,22 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
SNLog("[Calls] Sending end call message.")
try MessageSender.sendNonDurably(
db,
message: CallMessage(
uuid: self.uuid,
kind: .endCall,
sdps: []
),
interactionId: nil,
in: thread
)
.retainUntilComplete()
let preparedSendData: MessageSender.PreparedSendData = try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: self.uuid,
kind: .endCall,
sdps: []
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil
)
MessageSender
.sendImmediate(preparedSendData: preparedSendData)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
}
public func dropConnection() {

View File

@ -972,6 +972,34 @@ extension Attachment {
// MARK: - Upload
extension Attachment {
public static func prepare(_ db: Database, attachments: [SignalAttachment], for interactionId: Int64) throws {
// Prepare any attachments
try attachments.enumerated()
.forEach { index, signalAttachment in
let maybeAttachment: Attachment? = Attachment(
variant: (signalAttachment.isVoiceMessage ?
.voiceMessage :
.standard
),
contentType: signalAttachment.mimeType,
dataSource: signalAttachment.dataSource,
sourceFilename: signalAttachment.sourceFilename,
caption: signalAttachment.captionText
)
guard let attachment: Attachment = maybeAttachment else { return }
let interactionAttachment: InteractionAttachment = InteractionAttachment(
albumIndex: index,
interactionId: interactionId,
attachmentId: attachment.id
)
try attachment.insert(db)
try interactionAttachment.insert(db)
}
}
internal func upload(
_ db: Database? = nil,
queue: DispatchQueue,

View File

@ -446,7 +446,7 @@ public extension LinkPreview {
}
private static func parse(linkData: Data, response: URLResponse) throws -> Contents {
guard let linkText = String(data: linkData, urlResponse: response) else {
guard let linkText = String(bytes: linkData, encoding: response.stringEncoding ?? .utf8) else {
print("Could not parse link text.")
throw LinkPreviewError.invalidInput
}

View File

@ -2,7 +2,7 @@
import Foundation
import GRDB
import PromiseKit
import SignalCoreKit
import SessionUtilitiesKit
import SessionSnodeKit

View File

@ -160,22 +160,22 @@ public enum MessageSendJob: JobExecutor {
// Add the threadId to the message if there isn't one set
details.message.threadId = (details.message.threadId ?? job.threadId)
// Perform the actual message sending
/// Perform the actual message sending
///
/// **Note:** No need to upload attachments as part of this process as the above logic splits that out into it's own job
/// so we shouldn't get here until attachments have already been uploaded
Storage.shared
.writePublisher { db in
// TODO: Will need to split the attachment upload from the message preparation logic
try MessageSender.preparedSendData(
db,
message: details.message,
to: details.destination
.with(fileIds: messageFileIds), // TODO: This???
.with(fileIds: messageFileIds),
interactionId: job.interactionId
)
}
.subscribe(on: queue)
// TODO: Is this needed? (should be caught before this??)
// .flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete(
receiveCompletion: { result in
switch result {

View File

@ -47,8 +47,8 @@ public enum SendReadReceiptsJob: JobExecutor {
)
}
.subscribe(on: queue)
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.receive(on: queue)
.flatMap { MessageSender.sendImmediate(data: $0) }
.sinkUntilComplete(
receiveCompletion: { result in
switch result {

View File

@ -3,9 +3,8 @@
//
import Foundation
import Combine
import MobileCoreServices
import PromiseKit
import AVFoundation
import SessionUtilitiesKit
@ -887,11 +886,16 @@ public class SignalAttachment: Equatable, Hashable {
return videoDir
}
public class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> (Promise<SignalAttachment>, AVAssetExportSession?) {
public class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> (AnyPublisher<SignalAttachment, Error>, AVAssetExportSession?) {
guard let url = dataSource.dataUrl() else {
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .missingData
return (Promise.value(attachment), nil)
return (
Just(attachment)
.setFailureType(to: Error.self)
.eraseToAnyPublisher(),
nil
)
}
let asset = AVAsset(url: url)
@ -899,7 +903,12 @@ public class SignalAttachment: Equatable, Hashable {
guard let exportSession = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetMediumQuality) else {
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4
return (Promise.value(attachment), nil)
return (
Just(attachment)
.setFailureType(to: Error.self)
.eraseToAnyPublisher(),
nil
)
}
exportSession.shouldOptimizeForNetworkUse = true
@ -909,48 +918,43 @@ public class SignalAttachment: Equatable, Hashable {
let exportURL = videoTempPath.appendingPathComponent(UUID().uuidString).appendingPathExtension("mp4")
exportSession.outputURL = exportURL
let (promise, resolver) = Promise<SignalAttachment>.pending()
let publisher = Future<SignalAttachment, Error> { resolver in
exportSession.exportAsynchronously {
let baseFilename = dataSource.sourceFilename
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
exportSession.exportAsynchronously {
let baseFilename = dataSource.sourceFilename
let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4")
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
shouldDeleteOnDeallocation: true) else {
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4
resolver(Result.success(attachment))
return
}
guard let dataSource = DataSourcePath.dataSource(with: exportURL,
shouldDeleteOnDeallocation: true) else {
let attachment = SignalAttachment(dataSource: DataSourceValue.emptyDataSource(), dataUTI: dataUTI)
attachment.error = .couldNotConvertToMpeg4
resolver.fulfill(attachment)
return
dataSource.sourceFilename = mp4Filename
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
resolver(Result.success(attachment))
}
dataSource.sourceFilename = mp4Filename
let attachment = SignalAttachment(dataSource: dataSource, dataUTI: kUTTypeMPEG4 as String)
resolver.fulfill(attachment)
}
.eraseToAnyPublisher()
return (promise, exportSession)
return (publisher, exportSession)
}
@objc
public class VideoCompressionResult: NSObject {
@objc
public let attachmentPromise: AnyPromise
@objc
public struct VideoCompressionResult {
public let attachmentPublisher: AnyPublisher<SignalAttachment, Error>
public let exportSession: AVAssetExportSession?
fileprivate init(attachmentPromise: Promise<SignalAttachment>, exportSession: AVAssetExportSession?) {
self.attachmentPromise = AnyPromise(attachmentPromise)
fileprivate init(attachmentPublisher: AnyPublisher<SignalAttachment, Error>, exportSession: AVAssetExportSession?) {
self.attachmentPublisher = attachmentPublisher
self.exportSession = exportSession
super.init()
}
}
@objc
public class func compressVideoAsMp4(dataSource: DataSource, dataUTI: String) -> VideoCompressionResult {
let (attachmentPromise, exportSession) = compressVideoAsMp4(dataSource: dataSource, dataUTI: dataUTI)
return VideoCompressionResult(attachmentPromise: attachmentPromise, exportSession: exportSession)
let (attachmentPublisher, exportSession) = compressVideoAsMp4(dataSource: dataSource, dataUTI: dataUTI)
return VideoCompressionResult(attachmentPublisher: attachmentPublisher, exportSession: exportSession)
}
@objc

View File

@ -194,19 +194,21 @@ extension MessageReceiver {
)
.inserted(db)
try MessageSender
.sendNonDurably(
db,
message: CallMessage(
uuid: message.uuid,
kind: .endCall,
sdps: [],
sentTimestampMs: nil // Explicitly nil as it's a separate message from above
),
interactionId: nil, // Explicitly nil as it's a separate message from above
in: thread
)
.retainUntilComplete()
MessageSender.sendImmediate(
preparedSendData: try MessageSender
.preparedSendData(
db,
message: CallMessage(
uuid: message.uuid,
kind: .endCall,
sdps: [],
sentTimestampMs: nil // Explicitly nil as it's a separate message from above
),
to: try Message.Destination.from(db, thread: thread),
interactionId: nil // Explicitly nil as it's a separate message from above
)
)
.sinkUntilComplete()
}
@discardableResult public static func insertCallInfoMessage(

View File

@ -113,7 +113,7 @@ extension MessageSender {
.MergeMany(
// Send a closed group update message to all members individually
memberSendData
.map { MessageSender.sendImmediate(data: $0) }
.map { MessageSender.sendImmediate(preparedSendData: $0) }
.appending(
// Notify the PN server
PushNotificationAPI.performOperation(
@ -209,7 +209,7 @@ extension MessageSender {
.eraseToAnyPublisher()
}
return MessageSender.sendImmediate(data: sendData)
return MessageSender.sendImmediate(preparedSendData: sendData)
.map { _ in newKeyPair }
.eraseToAnyPublisher()
.handleEvents(
@ -490,7 +490,7 @@ extension MessageSender {
// Send the update to the group and generate + distribute a new encryption key pair
return MessageSender
.sendImmediate(
data: try MessageSender
preparedSendData: try MessageSender
.preparedSendData(
db,
message: LegacyClosedGroupControlMessage(
@ -593,7 +593,7 @@ extension MessageSender {
}
return MessageSender
.sendImmediate(data: sendData)
.sendImmediate(preparedSendData: sendData)
.handleEvents(
receiveCompletion: { result in
switch result {

View File

@ -3,26 +3,12 @@
import Foundation
import Combine
import GRDB
import PromiseKit
import SessionUtilitiesKit
extension MessageSender {
// MARK: - Durable
public static func send(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws {
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
try prep(db, signalAttachments: attachments, for: interactionId)
send(
db,
message: VisibleMessage.from(db, interaction: interaction),
threadId: thread.id,
interactionId: interactionId,
to: try Message.Destination.from(db, thread: thread)
)
}
public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread) throws {
// Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
@ -64,33 +50,6 @@ extension MessageSender {
// MARK: - Non-Durable
public static func sendNonDurably(_ db: Database, interaction: Interaction, with attachments: [SignalAttachment], in thread: SessionThread) throws -> Promise<Void> {
guard let interactionId: Int64 = interaction.id else { return Promise(error: StorageError.objectNotSaved) }
try prep(db, signalAttachments: attachments, for: interactionId)
return sendNonDurably(
db,
message: VisibleMessage.from(db, interaction: interaction),
interactionId: interactionId,
to: try Message.Destination.from(db, thread: thread)
)
}
public static func sendNonDurably(_ db: Database, interaction: Interaction, in thread: SessionThread) throws -> Promise<Void> {
// Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
guard let interactionId: Int64 = interaction.id else { throw StorageError.objectNotSaved }
return sendNonDurably(
db,
message: VisibleMessage.from(db, interaction: interaction),
interactionId: interactionId,
to: try Message.Destination.from(db, thread: thread)
)
}
public static func preparedSendData(
_ db: Database,
interaction: Interaction,
@ -108,105 +67,6 @@ extension MessageSender {
)
}
public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, in thread: SessionThread) throws -> Promise<Void> {
return sendNonDurably(
db,
message: message,
interactionId: interactionId,
to: try Message.Destination.from(db, thread: thread)
)
}
public static func sendNonDurably(_ db: Database, message: Message, interactionId: Int64?, to destination: Message.Destination) -> Promise<Void> {
var attachmentUploadPromises: [Promise<String?>] = [Promise.value(nil)]
// If we have an interactionId then check if it has any attachments and process them first
if let interactionId: Int64 = interactionId {
let threadId: String = {
switch destination {
case .contact(let publicKey, _): return publicKey
case .closedGroup(let groupPublicKey, _): return groupPublicKey
case .openGroup(let roomToken, let server, _, _, _):
return OpenGroup.idFor(roomToken: roomToken, server: server)
case .openGroupInbox(_, _, let blindedPublicKey): return blindedPublicKey
}
}()
let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId)
let attachmentStateInfo: [Attachment.StateInfo] = (try? Attachment
.stateInfo(interactionId: interactionId, state: .uploading)
.fetchAll(db))
.defaulting(to: [])
attachmentUploadPromises = (try? Attachment
.filter(ids: attachmentStateInfo.map { $0.attachmentId })
.fetchAll(db))
.defaulting(to: [])
.map { attachment -> Promise<String?> in
let (promise, seal) = Promise<String?>.pending()
attachment.upload(
db,
queue: DispatchQueue.global(qos: .userInitiated),
using: { db, data in
if let openGroup: OpenGroup = openGroup {
return OpenGroupAPI
.uploadFile(
db,
bytes: data.bytes,
to: openGroup.roomToken,
on: openGroup.server
)
.map { _, response -> String in response.id }
.eraseToAnyPublisher()
}
return FileServerAPI.upload(data)
.map { response -> String in response.id }
.eraseToAnyPublisher()
},
encrypt: (openGroup == nil),
success: { fileId in seal.fulfill(fileId) },
failure: { seal.reject($0) }
)
return promise
}
}
// Once the attachments are processed then send the message
// TODO: Need to update all usages of this method
preconditionFailure()
// return when(resolved: attachmentUploadPromises)
// .then { results -> Promise<Void> in
// let errors: [Error] = results
// .compactMap { result -> Error? in
// if case .rejected(let error) = result { return error }
//
// return nil
// }
//
// if let error: Error = errors.first { return Promise(error: error) }
//
// return Storage.shared.writeAsync { db in
// let fileIds: [String] = results
// .compactMap { result -> String? in
// if case .fulfilled(let value) = result { return value }
//
// return nil
// }
//
// return try MessageSender.sendImmediate(
// db,
// message: message,
// to: destination
// .with(fileIds: fileIds),
// interactionId: interactionId
// )
// }
// }
}
public static func performUploadsIfNeeded(
preparedSendData: PreparedSendData
) -> AnyPublisher<PreparedSendData, Error> {
@ -242,7 +102,7 @@ extension MessageSender {
// If there is no attachment data then just return early
guard !attachmentStateInfo.isEmpty else { return nil }
// TODO: Just run an AttachmentUploadJob directly???
// Otherwise we need to generate the upload requests
let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId)
@ -385,23 +245,14 @@ extension MessageSender {
.retainUntilComplete()
// TODO: Test this (does it break anything? want to stop the db write asap)
/// We don't want to block the db write thread so we trigger the actual message sending after the query has
/// finished
return Future<Void, Error> { resolver in
db.afterNextTransaction { _ in
// TODO: Remove the 'Swift.'
resolver(Swift.Result.success(()))
resolver(Result.success(()))
}
}
.flatMap { _ in MessageSender.sendImmediate(data: sendData) }
.flatMap { _ in MessageSender.sendImmediate(preparedSendData: sendData) }
.eraseToAnyPublisher()
// return MessageSender
// .sendImmediate(
// data: try MessageSender.preparedSendData(
// db,
// message: configurationMessage,
// to: destination,
// interactionId: nil
// )
// )
}
}

View File

@ -8,52 +8,6 @@ import SessionUtilitiesKit
import Sodium
public final class MessageSender {
// MARK: - Preparation
public static func prep(
_ db: Database,
signalAttachments: [SignalAttachment],
for interactionId: Int64
) throws {
try signalAttachments.enumerated().forEach { index, signalAttachment in
let maybeAttachment: Attachment? = Attachment(
variant: (signalAttachment.isVoiceMessage ?
.voiceMessage :
.standard
),
contentType: signalAttachment.mimeType,
dataSource: signalAttachment.dataSource,
sourceFilename: signalAttachment.sourceFilename,
caption: signalAttachment.captionText
)
guard let attachment: Attachment = maybeAttachment else { return }
let interactionAttachment: InteractionAttachment = InteractionAttachment(
albumIndex: index,
interactionId: interactionId,
attachmentId: attachment.id
)
try attachment.insert(db)
try interactionAttachment.insert(db)
}
}
// MARK: - Convenience
// public static func sendImmediate(_ db: Database, message: Message, to destination: Message.Destination, interactionId: Int64?) throws -> Promise<Void> {
// switch destination {
// case .contact, .closedGroup:
// return try sendToSnodeDestination(db, message: message, to: destination, interactionId: interactionId)
//
// case .openGroup:
// return sendToOpenGroupDestination(db, message: message, to: destination, interactionId: interactionId)
//
// case .openGroupInbox:
// return sendToOpenGroupInboxDestination(db, message: message, to: destination, interactionId: interactionId)
// }
// }
// MARK: - Message Preparation
public struct PreparedSendData {
@ -644,19 +598,19 @@ public final class MessageSender {
// MARK: - Sending
public static func sendImmediate(
data: PreparedSendData,
preparedSendData: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies()
) -> AnyPublisher<Void, Error> {
guard data.shouldSend else {
guard preparedSendData.shouldSend else {
return Just(())
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
switch data.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: data, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: data, using: dependencies)
switch preparedSendData.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: preparedSendData, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: preparedSendData, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: preparedSendData, using: dependencies)
case .none:
return Fail(error: MessageSenderError.invalidMessage)
.eraseToAnyPublisher()

View File

@ -1,29 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import SessionSnodeKit
import SessionUtilitiesKit
extension Promise where T == Data {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<R> {
self.map(on: queue) { data -> R in
try data.decoded(as: type, using: dependencies)
}
}
}
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> {
self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in
guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
do {
return (responseInfo, try data.decoded(as: type, using: dependencies))
}
catch {
throw HTTP.Error.parsingFailed
}
}
}
}

View File

@ -1,15 +1,15 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import CoreServices
import PromiseKit
import SignalUtilitiesKit
import SessionUIKit
import SignalCoreKit
final class ShareVC: UINavigationController, ShareViewDelegate {
private var areVersionMigrationsComplete = false
public static var attachmentPrepPromise: Promise<[SignalAttachment]>?
public static var attachmentPrepPublisher: AnyPublisher<[SignalAttachment], Error>?
// MARK: - Error
@ -187,20 +187,19 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
setViewControllers([ threadPickerVC ], animated: false)
let promise = buildAttachments()
ModalActivityIndicatorViewController.present(
fromViewController: self,
canCancel: false,
message: "vc_share_loading_message".localized()) { activityIndicator in
promise
.done { _ in
activityIndicator.dismiss { }
}
.catch { _ in
activityIndicator.dismiss { }
}
}
ShareVC.attachmentPrepPromise = promise
let publisher = buildAttachments()
ModalActivityIndicatorViewController
.present(
fromViewController: self,
canCancel: false,
message: "vc_share_loading_message".localized()
) { activityIndicator in
publisher
.sinkUntilComplete(
receiveCompletion: { _ in activityIndicator.dismiss { } }
)
}
ShareNavController.attachmentPrepPublisher = publisher
}
func shareViewWasUnlocked() {
@ -365,10 +364,11 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
return []
}
private func selectItemProviders() -> Promise<[NSItemProvider]> {
private func selectItemProviders() -> AnyPublisher<[NSItemProvider], Error> {
guard let inputItems = self.extensionContext?.inputItems else {
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error)
return Fail(error: error)
.eraseToAnyPublisher()
}
for inputItemRaw in inputItems {
@ -377,12 +377,15 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
continue
}
if let itemProviders = ShareVC.preferredItemProviders(inputItem: inputItem) {
return Promise.value(itemProviders)
if let itemProviders = ShareNavController.preferredItemProviders(inputItem: inputItem) {
return Just(itemProviders)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
}
let error = ShareViewControllerError.assertionError(description: "no input item")
return Promise(error: error)
return Fail(error: error)
.eraseToAnyPublisher()
}
// MARK: - LoadedItem
@ -412,7 +415,7 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
}
}
private func loadItemProvider(itemProvider: NSItemProvider) -> Promise<LoadedItem> {
private func loadItemProvider(itemProvider: NSItemProvider) -> AnyPublisher<LoadedItem, Error> {
Logger.info("attachment: \(itemProvider)")
// We need to be very careful about which UTI type we use.
@ -426,115 +429,173 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
// using the file extension.
guard let srcUtiType = ShareVC.utiType(itemProvider: itemProvider) else {
let error = ShareViewControllerError.unsupportedMedia
return Promise(error: error)
return Fail(error: error)
.eraseToAnyPublisher()
}
Logger.debug("matched utiType: \(srcUtiType)")
let (promise, resolver) = Promise<LoadedItem>.pending()
let loadCompletion: NSItemProvider.CompletionHandler = { [weak self]
(value, error) in
guard let _ = self else { return }
guard error == nil else {
resolver.reject(error!)
return
}
guard let value = value else {
let missingProviderError = ShareViewControllerError.assertionError(description: "missing item provider")
resolver.reject(missingProviderError)
return
}
Logger.info("value type: \(type(of: value))")
if let data = value as? Data {
let customFileName = "Contact.vcf"
let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType)
guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: customFileExtension) else {
let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
resolver.reject(writeError)
return
}
let fileUrl = URL(fileURLWithPath: tempFilePath)
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
customFileName: customFileName,
isConvertibleToContactShare: false))
} else if let string = value as? String {
Logger.debug("string provider: \(string)")
guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else {
let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
resolver.reject(writeError)
return
}
guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: "txt") else {
let writeError = ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))")
resolver.reject(writeError)
return Future<LoadedItem, Error> { resolver in
let loadCompletion: NSItemProvider.CompletionHandler = { [weak self] value, error in
guard self != nil else { return }
if let error: Error = error {
resolver(Result.failure(error))
return
}
let fileUrl = URL(fileURLWithPath: tempFilePath)
guard let value = value else {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "missing item provider"))
)
return
}
let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)
Logger.info("value type: \(type(of: value))")
switch value {
case let data as Data:
let customFileName = "Contact.vcf"
if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) {
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} else {
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: kUTTypeText as String,
isConvertibleToTextMessage: isConvertibleToTextMessage))
let customFileExtension = MIMETypeUtil.fileExtension(forUTIType: srcUtiType)
guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: customFileExtension) else {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))"))
)
return
}
let fileUrl = URL(fileURLWithPath: tempFilePath)
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
customFileName: customFileName,
isConvertibleToContactShare: false
)
)
)
case let string as String:
Logger.debug("string provider: \(string)")
guard let data = string.filterStringForDisplay().data(using: String.Encoding.utf8) else {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))"))
)
return
}
guard let tempFilePath = OWSFileSystem.writeData(toTemporaryFile: data, fileExtension: "txt") else {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "Error writing item data: \(String(describing: error))"))
)
return
}
let fileUrl = URL(fileURLWithPath: tempFilePath)
let isConvertibleToTextMessage = !itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)
if UTTypeConformsTo(srcUtiType as CFString, kUTTypeText) {
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage
)
)
)
}
else {
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: fileUrl,
utiType: kUTTypeText as String,
isConvertibleToTextMessage: isConvertibleToTextMessage
)
)
)
}
case let url as URL:
// If the share itself is a URL (e.g. a link from Safari), try to send this as a text message.
let isConvertibleToTextMessage = (
itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) &&
!itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String)
)
if isConvertibleToTextMessage {
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: url,
utiType: kUTTypeURL as String,
isConvertibleToTextMessage: isConvertibleToTextMessage
)
)
)
}
else {
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: url,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage
)
)
)
}
case let image as UIImage:
if let data = image.pngData() {
let tempFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png")
do {
let url = NSURL.fileURL(withPath: tempFilePath)
try data.write(to: url)
resolver(
Result.success(
LoadedItem(
itemProvider: itemProvider,
itemUrl: url,
utiType: srcUtiType
)
)
)
}
catch {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))"))
)
}
}
else {
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "couldn't convert UIImage to PNG: \(String(describing: error))"))
)
}
default:
// It's unavoidable that we may sometimes receives data types that we
// don't know how to handle.
resolver(
Result.failure(ShareViewControllerError.assertionError(description: "unexpected value: \(String(describing: value))"))
)
}
} else if let url = value as? URL {
// If the share itself is a URL (e.g. a link from Safari), try to send this as a text message.
let isConvertibleToTextMessage = (itemProvider.registeredTypeIdentifiers.contains(kUTTypeURL as String) &&
!itemProvider.registeredTypeIdentifiers.contains(kUTTypeFileURL as String))
if isConvertibleToTextMessage {
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: url,
utiType: kUTTypeURL as String,
isConvertibleToTextMessage: isConvertibleToTextMessage))
} else {
resolver.fulfill(LoadedItem(itemProvider: itemProvider,
itemUrl: url,
utiType: srcUtiType,
isConvertibleToTextMessage: isConvertibleToTextMessage))
}
} else if let image = value as? UIImage {
if let data = image.pngData() {
let tempFilePath = OWSFileSystem.temporaryFilePath(withFileExtension: "png")
do {
let url = NSURL.fileURL(withPath: tempFilePath)
try data.write(to: url)
resolver.fulfill(LoadedItem(itemProvider: itemProvider, itemUrl: url,
utiType: srcUtiType))
} catch {
resolver.reject(ShareViewControllerError.assertionError(description: "couldn't write UIImage: \(String(describing: error))"))
}
} else {
resolver.reject(ShareViewControllerError.assertionError(description: "couldn't convert UIImage to PNG: \(String(describing: error))"))
}
} else {
// It's unavoidable that we may sometimes receives data types that we
// don't know how to handle.
let unexpectedTypeError = ShareViewControllerError.assertionError(description: "unexpected value: \(String(describing: value))")
resolver.reject(unexpectedTypeError)
}
itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion)
}
itemProvider.loadItem(forTypeIdentifier: srcUtiType, options: nil, completionHandler: loadCompletion)
return promise
.eraseToAnyPublisher()
}
private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> Promise<SignalAttachment> {
private func buildAttachment(forLoadedItem loadedItem: LoadedItem) -> AnyPublisher<SignalAttachment, Error> {
let itemProvider = loadedItem.itemProvider
let itemUrl = loadedItem.itemUrl
let utiType = loadedItem.utiType
@ -546,14 +607,16 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
}
} catch {
let error = ShareViewControllerError.assertionError(description: "Could not copy video")
return Promise(error: error)
return Fail(error: error)
.eraseToAnyPublisher()
}
Logger.debug("building DataSource with url: \(url), utiType: \(utiType)")
guard let dataSource = ShareVC.createDataSource(utiType: utiType, url: url, customFileName: loadedItem.customFileName) else {
let error = ShareViewControllerError.assertionError(description: "Unable to read attachment data")
return Promise(error: error)
return Fail(error: error)
.eraseToAnyPublisher()
}
// start with base utiType, but it might be something generic like "image"
@ -572,8 +635,8 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
guard !SignalAttachment.isInvalidVideo(dataSource: dataSource, dataUTI: specificUTIType) else {
// This can happen, e.g. when sharing a quicktime-video from iCloud drive.
let (promise, _) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType)
return promise
let (publisher, _) = SignalAttachment.compressVideoAsMp4(dataSource: dataSource, dataUTI: specificUTIType)
return publisher
}
let attachment = SignalAttachment.attachment(dataSource: dataSource, dataUTI: specificUTIType, imageQuality: .medium)
@ -584,34 +647,49 @@ final class ShareVC: UINavigationController, ShareViewDelegate {
Logger.info("isConvertibleToTextMessage")
attachment.isConvertibleToTextMessage = true
}
return Promise.value(attachment)
return Just(attachment)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
private func buildAttachments() -> Promise<[SignalAttachment]> {
return selectItemProviders().then { [weak self] (itemProviders) -> Promise<[SignalAttachment]> in
guard let strongSelf = self else {
let error = ShareViewControllerError.assertionError(description: "expired")
return Promise(error: error)
}
private func buildAttachments() -> AnyPublisher<[SignalAttachment], Error> {
return selectItemProviders()
.flatMap { [weak self] itemProviders -> AnyPublisher<[SignalAttachment], Error> in
guard let strongSelf = self else {
let error = ShareViewControllerError.assertionError(description: "expired")
return Fail(error: error)
.eraseToAnyPublisher()
}
var loadPromises = [Promise<SignalAttachment>]()
var loadPublishers = [AnyPublisher<SignalAttachment, Error>]()
for itemProvider in itemProviders.prefix(SignalAttachment.maxAttachmentsAllowed) {
let loadPromise = strongSelf.loadItemProvider(itemProvider: itemProvider)
.then({ (loadedItem) -> Promise<SignalAttachment> in
return strongSelf.buildAttachment(forLoadedItem: loadedItem)
})
for itemProvider in itemProviders.prefix(SignalAttachment.maxAttachmentsAllowed) {
let loadPublisher = strongSelf.loadItemProvider(itemProvider: itemProvider)
.flatMap { loadedItem -> AnyPublisher<SignalAttachment, Error> in
return strongSelf.buildAttachment(forLoadedItem: loadedItem)
}
.eraseToAnyPublisher()
loadPromises.append(loadPromise)
loadPublishers.append(loadPublisher)
}
return Publishers
.MergeMany(loadPublishers)
.collect()
.eraseToAnyPublisher()
}
return when(fulfilled: loadPromises)
}.map { (signalAttachments) -> [SignalAttachment] in
guard signalAttachments.count > 0 else {
let error = ShareViewControllerError.assertionError(description: "no valid attachments")
throw error
.flatMap { signalAttachments -> AnyPublisher<[SignalAttachment], Error> in
guard signalAttachments.count > 0 else {
return Fail(error: ShareViewControllerError.assertionError(description: "no valid attachments"))
.eraseToAnyPublisher()
}
return Just(signalAttachments)
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
return signalAttachments
}
.shareReplay(1)
.eraseToAnyPublisher()
}
// Some host apps (e.g. iOS Photos.app) sometimes auto-converts some video formats (e.g. com.apple.quicktime-movie)

View File

@ -1,8 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import UIKit
import Combine
import GRDB
import PromiseKit
import DifferenceKit
import SessionUIKit
import SignalUtilitiesKit
@ -149,14 +149,21 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
guard let attachments: [SignalAttachment] = ShareVC.attachmentPrepPromise?.value else { return }
let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController(
threadId: self.viewModel.viewData[indexPath.row].threadId,
attachments: attachments,
approvalDelegate: self
)
self.navigationController?.present(approvalVC, animated: true, completion: nil)
ShareNavController.attachmentPrepPublisher?
.receiveOnMain(immediately: true)
.sinkUntilComplete(
receiveValue: { [weak self] attachments in
guard let strongSelf = self else { return }
// TODO: Test this
let approvalVC: UINavigationController = AttachmentApprovalViewController.wrappedInNavController(
threadId: strongSelf.viewModel.viewData[indexPath.row].threadId,
attachments: attachments,
approvalDelegate: strongSelf
)
strongSelf.navigationController?.present(approvalVC, animated: true, completion: nil)
}
)
}
func attachmentApproval(_ attachmentApproval: AttachmentApprovalViewController, didApproveAttachments attachments: [SignalAttachment], forThreadId threadId: String, messageText: String?) {
@ -181,12 +188,11 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
ModalActivityIndicatorViewController.present(fromViewController: shareVC!, canCancel: false, message: "vc_share_sending_message".localized()) { activityIndicator in
// Resume database
NotificationCenter.default.post(name: Database.resumeNotification, object: self)
Storage.shared
.writeAsync { [weak self] db -> Promise<Void> in
.writePublisher { [weak self] db -> MessageSender.PreparedSendData in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
activityIndicator.dismiss { }
self?.shareVC?.shareViewFailed(error: MessageSenderError.noThread)
return Promise(error: MessageSenderError.noThread)
throw MessageSenderError.noThread
}
// Create the interaction
@ -205,7 +211,11 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
.fetchOne(db),
linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil)
).inserted(db)
guard let interactionId: Int64 = interaction.id else {
throw StorageError.failedToSave
}
// If the user is sharing a Url, there is a LinkPreview and it doesn't match an existing
// one then add it now
if
@ -223,26 +233,36 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
)
).insert(db)
}
return try MessageSender.sendNonDurably(
// Prepare any attachments
try Attachment.prepare(
db,
interaction: interaction,
with: finalAttachments,
in: thread
attachments: finalAttachments,
for: interactionId
)
// Prepare the message send data
return try MessageSender
.preparedSendData(
db,
interaction: interaction,
in: thread
)
}
.done { [weak self] _ in
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { }
self?.shareVC?.shareViewWasCompleted()
}
.catch { [weak self] error in
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { }
self?.shareVC?.shareViewFailed(error: error)
}
.flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete(
receiveCompletion: { [weak self] result in
// Suspend the database
NotificationCenter.default.post(name: Database.suspendNotification, object: self)
activityIndicator.dismiss { }
switch result {
case .finished: self?.shareNavController?.shareViewWasCompleted()
case .failure(let error): self?.shareNavController?.shareViewFailed(error: error)
}
}
)
}
}

View File

@ -3,20 +3,9 @@
import Foundation
import Combine
import CryptoSwift
import PromiseKit
import SessionUtilitiesKit
internal extension OnionRequestAPI {
static func encodeLegacy(ciphertext: Data, json: JSON) throws -> Data {
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
guard JSONSerialization.isValidJSONObject(json) else { throw HTTP.Error.invalidJSON }
let jsonAsData = try JSONSerialization.data(withJSONObject: json, options: [ .fragmentsAllowed ])
let ciphertextSize = Int32(ciphertext.count).littleEndian
let ciphertextSizeAsData = withUnsafePointer(to: ciphertextSize) { Data(bytes: $0, count: MemoryLayout<Int32>.size) }
return ciphertextSizeAsData + ciphertext + jsonAsData
}
static func encode(ciphertext: Data, json: JSON) -> AnyPublisher<Data, Error> {
// The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
guard
@ -34,102 +23,41 @@ internal extension OnionRequestAPI {
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
static func encrypt(_ payload: Data, for destination: OnionRequestAPIDestination) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
do {
switch destination {
case .snode(let snode):
// Need to wrap the payload for snode requests
let data: Data = try encodeLegacy(ciphertext: payload, json: [ "headers" : "" ])
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(data, for: snode.x25519PublicKey)
seal.fulfill(result)
case .server(_, _, let serverX25519PublicKey, _, _):
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(payload, for: serverX25519PublicKey)
seal.fulfill(result)
}
}
catch (let error) {
seal.reject(error)
}
}
return promise
}
/// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
static func encrypt(
_ payload: Data,
for destination: OnionRequestAPIDestination
) -> AnyPublisher<AESGCM.EncryptionResult, Error> {
return Future { resolver in
DispatchQueue.global(qos: .userInitiated).async {
do {
switch destination {
case .snode(let snode):
// Need to wrap the payload for snode requests
let data: Data = try encodeLegacy(ciphertext: payload, json: [ "headers" : "" ])
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(data, for: snode.x25519PublicKey)
resolver(Swift.Result.success(result))
case .server(_, _, let serverX25519PublicKey, _, _):
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(payload, for: serverX25519PublicKey)
resolver(Swift.Result.success(result))
// TODO: Test performance
switch destination {
case .snode(let snode):
// Need to wrap the payload for snode requests
return encode(ciphertext: payload, json: [ "headers" : "" ])
.flatMap { data -> AnyPublisher<AESGCM.EncryptionResult, Error> in
do {
return Just(try AESGCM.encrypt(data, for: snode.x25519PublicKey))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
}
.eraseToAnyPublisher()
case .server(_, _, let serverX25519PublicKey, _, _):
do {
return Just(try AESGCM.encrypt(payload, for: serverX25519PublicKey))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
catch (let error) {
resolver(Swift.Result.failure(error))
catch {
return Fail(error: error)
.eraseToAnyPublisher()
}
}
}
.eraseToAnyPublisher()
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
static func encryptHop(from lhs: OnionRequestAPIDestination, to rhs: OnionRequestAPIDestination, using previousEncryptionResult: AESGCM.EncryptionResult) -> Promise<AESGCM.EncryptionResult> {
let (promise, seal) = Promise<AESGCM.EncryptionResult>.pending()
DispatchQueue.global(qos: .userInitiated).async {
var parameters: JSON
switch rhs {
case .snode(let snode):
let snodeED25519PublicKey = snode.ed25519PublicKey
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, let target, _, let scheme, let port):
let scheme = scheme ?? "https"
let port = port ?? (scheme == "https" ? 443 : 80)
parameters = [ "host" : host, "target" : target, "method" : "POST", "protocol" : scheme, "port" : port ]
}
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
let snodeX25519PublicKey = snode.x25519PublicKey
x25519PublicKey = snodeX25519PublicKey
case .server(_, _, let serverX25519PublicKey, _, _):
x25519PublicKey = serverX25519PublicKey
}
do {
let plaintext = try encodeLegacy(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
let result = try AESGCM.encrypt(plaintext, for: x25519PublicKey)
seal.fulfill(result)
}
catch (let error) {
seal.reject(error)
}
}
return promise
}
/// Encrypts the previous encryption result (i.e. that of the hop after this one) for this hop. Use this to build the layers of an onion request.
@ -138,44 +66,42 @@ internal extension OnionRequestAPI {
to rhs: OnionRequestAPIDestination,
using previousEncryptionResult: AESGCM.EncryptionResult
) -> AnyPublisher<AESGCM.EncryptionResult, Error> {
return Future { resolver in
DispatchQueue.global(qos: .userInitiated).async {
var parameters: JSON
switch rhs {
case .snode(let snode):
let snodeED25519PublicKey = snode.ed25519PublicKey
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, let target, _, let scheme, let port):
let scheme = scheme ?? "https"
let port = port ?? (scheme == "https" ? 443 : 80)
parameters = [ "host" : host, "target" : target, "method" : "POST", "protocol" : scheme, "port" : port ]
}
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String
switch lhs {
case .snode(let snode):
let snodeX25519PublicKey = snode.x25519PublicKey
x25519PublicKey = snodeX25519PublicKey
case .server(_, _, let serverX25519PublicKey, _, _):
x25519PublicKey = serverX25519PublicKey
}
// TODO: Test performance
var parameters: JSON
switch rhs {
case .snode(let snode):
let snodeED25519PublicKey = snode.ed25519PublicKey
parameters = [ "destination" : snodeED25519PublicKey ]
case .server(let host, let target, _, let scheme, let port):
let scheme = scheme ?? "https"
let port = port ?? (scheme == "https" ? 443 : 80)
parameters = [ "host" : host, "target" : target, "method" : "POST", "protocol" : scheme, "port" : port ]
}
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String = {
switch lhs {
case .snode(let snode): return snode.x25519PublicKey
case .server(_, _, let serverX25519PublicKey, _, _):
return serverX25519PublicKey
}
}()
return encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
.flatMap { data -> AnyPublisher<AESGCM.EncryptionResult, Error> in
do {
let plaintext = try encodeLegacy(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
let result = try AESGCM.encrypt(plaintext, for: x25519PublicKey)
resolver(Swift.Result.success(result))
return Just(try AESGCM.encrypt(data, for: x25519PublicKey))
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
}
catch (let error) {
resolver(Swift.Result.failure(error))
return Fail(error: error)
.eraseToAnyPublisher()
}
}
}
.eraseToAnyPublisher()
.eraseToAnyPublisher()
}
}

View File

@ -4,7 +4,6 @@ import Foundation
import Combine
import CryptoSwift
import GRDB
import PromiseKit
import SessionUtilitiesKit
public protocol OnionRequestAPIType {
@ -14,7 +13,6 @@ public protocol OnionRequestAPIType {
/// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI: OnionRequestAPIType {
private static var buildPathsPromise: Promise<[[Snode]]>? = nil
private static var buildPathsPublisher: Atomic<AnyPublisher<[[Snode]], Error>?> = Atomic(nil)
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
@ -63,40 +61,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
// MARK: - Private API
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
private static func testSnode(_ snode: Snode) -> Promise<Void> {
let (promise, seal) = Promise<Void>.pending()
DispatchQueue.global(qos: .userInitiated).async {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
let timeout: TimeInterval = 3 // Use a shorter timeout for testing
HTTP.executeLegacy(.get, url, timeout: timeout)
.done2 { responseData in
// TODO: Remove JSON usage
guard let responseJson: JSON = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON else {
throw HTTP.Error.invalidJSON
}
guard let version = responseJson["version"] as? String else {
return seal.reject(OnionRequestAPIError.missingSnodeVersion)
}
if version >= "2.0.7" {
seal.fulfill(())
}
else {
SNLog("Unsupported snode version: \(version).")
seal.reject(OnionRequestAPIError.unsupportedSnodeVersion(version))
}
}
.catch2 { error in
seal.reject(error)
}
}
return promise
}
/// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise.
private static func testSnode(_ snode: Snode) -> AnyPublisher<Void, Error> {
let url = "\(snode.address):\(snode.port)/get_stats/v1"
@ -126,51 +90,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
.eraseToAnyPublisher()
}
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with
/// `Error.insufficientSnodes` if not enough (reliable) snodes are available.
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise<Set<Snode>> {
if guardSnodes.count >= targetGuardSnodeCount {
return Promise<Set<Snode>> { $0.fulfill(guardSnodes) }
}
else {
SNLog("Populating guard snode cache.")
// Sync on LokiAPI.workQueue
var unusedSnodes = SnodeAPI.snodePool.wrappedValue.subtracting(reusableGuardSnodes)
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else {
return Promise(error: OnionRequestAPIError.insufficientSnodes)
}
func getGuardSnode() -> Promise<Snode> {
// randomElement() uses the system's default random generator, which
// is cryptographically secure
guard let candidate = unusedSnodes.randomElement() else {
return Promise<Snode> { $0.reject(OnionRequestAPIError.insufficientSnodes) }
}
unusedSnodes.remove(candidate) // All used snodes should be unique
SNLog("Testing guard snode: \(candidate).")
// Loop until a reliable guard snode is found
return testSnode(candidate).map2 { candidate }.recover(on: DispatchQueue.main) { _ in
withDelay(0.1, completionQueue: Threading.workQueue) { getGuardSnode() }
}
}
let promises = (0..<(targetGuardSnodeCount - reusableGuardSnodeCount)).map { _ in
getGuardSnode()
}
return when(fulfilled: promises).map2 { guardSnodes in
let guardSnodesAsSet = Set(guardSnodes + reusableGuardSnodes)
OnionRequestAPI.guardSnodes = guardSnodesAsSet
return guardSnodesAsSet
}
}
}
/// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with
/// `Error.insufficientSnodes` if not enough (reliable) snodes are available.
@ -227,59 +146,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
)
.eraseToAnyPublisher()
}
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
@discardableResult
private static func buildPaths(reusing reusablePaths: [[Snode]]) -> Promise<[[Snode]]> {
if let existingBuildPathsPromise = buildPathsPromise { return existingBuildPathsPromise }
SNLog("Building onion request paths.")
DispatchQueue.main.async {
NotificationCenter.default.post(name: .buildingPaths, object: nil)
}
let reusableGuardSnodes = reusablePaths.map { $0[0] }
let promise: Promise<[[Snode]]> = getGuardSnodes(reusing: reusableGuardSnodes)
.map2 { guardSnodes -> [[Snode]] in
var unusedSnodes = SnodeAPI.snodePool.wrappedValue
.subtracting(guardSnodes)
.subtracting(reusablePaths.flatMap { $0 })
let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count)
let pathSnodeCount = (targetGuardSnodeCount - reusableGuardSnodeCount) * pathSize - (targetGuardSnodeCount - reusableGuardSnodeCount)
guard unusedSnodes.count >= pathSnodeCount else { throw OnionRequestAPIError.insufficientSnodes }
// Don't test path snodes as this would reveal the user's IP to them
return guardSnodes.subtracting(reusableGuardSnodes).map { guardSnode in
let result = [ guardSnode ] + (0..<(pathSize - 1)).map { _ in
// randomElement() uses the system's default random generator, which is cryptographically secure
let pathSnode = unusedSnodes.randomElement()! // Safe because of the pathSnodeCount check above
unusedSnodes.remove(pathSnode) // All used snodes should be unique
return pathSnode
}
SNLog("Built new onion request path: \(result.prettifiedDescription).")
return result
}
}
.map2 { paths in
OnionRequestAPI.paths = paths + reusablePaths
Storage.shared.write { db in
SNLog("Persisting onion request paths to database.")
try? paths.save(db)
}
DispatchQueue.main.async {
NotificationCenter.default.post(name: .pathsBuilt, object: nil)
}
return paths
}
promise.done2 { _ in buildPathsPromise = nil }
promise.catch2 { _ in buildPathsPromise = nil }
buildPathsPromise = promise
return promise
}
/// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available.
@ -347,74 +213,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return publisher
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
private static func getPath(excluding snode: Snode?) -> Promise<[Snode]> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
let paths: [[Snode]] = OnionRequestAPI.paths
if !paths.isEmpty {
guardSnodes.formUnion([ paths[0][0] ])
if paths.count >= 2 {
guardSnodes.formUnion([ paths[1][0] ])
}
}
// randomElement() uses the system's default random generator, which is cryptographically secure
if
paths.count >= targetPathCount,
let targetPath: [Snode] = paths
.filter({ snode == nil || !$0.contains(snode!) })
.randomElement()
{
return Promise { $0.fulfill(targetPath) }
}
else if !paths.isEmpty {
if let snode = snode {
if let path = paths.first(where: { !$0.contains(snode) }) {
let tmp: Promise<[[Snode]]> = buildPaths(reusing: paths) // Re-build paths in the background
return Promise { $0.fulfill(path) }
}
else {
return buildPaths(reusing: paths).map2 { paths in
guard let path: [Snode] = paths.filter({ !$0.contains(snode) }).randomElement() else {
throw OnionRequestAPIError.insufficientSnodes
}
return path
}
}
}
else {
let tmp: Promise<[[Snode]]> = buildPaths(reusing: paths) // Re-build paths in the background
guard let path: [Snode] = paths.randomElement() else {
return Promise(error: OnionRequestAPIError.insufficientSnodes)
}
return Promise { $0.fulfill(path) }
}
}
else {
return buildPaths(reusing: []).map2 { paths in
if let snode = snode {
if let path = paths.filter({ !$0.contains(snode) }).randomElement() {
return path
}
throw OnionRequestAPIError.insufficientSnodes
}
guard let path: [Snode] = paths.randomElement() else {
throw OnionRequestAPIError.insufficientSnodes
}
return path
}
}
}
/// Returns a `Path` to be used for building an onion request. Builds new paths as needed.
private static func getPath(excluding snode: Snode?) -> AnyPublisher<[Snode], Error> {
@ -566,50 +364,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
try? paths.save(db)
}
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(around payload: Data, targetedAt destination: OnionRequestAPIDestination) -> Promise<OnionBuildingResult> {
var guardSnode: Snode!
var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination
var encryptionResult: AESGCM.EncryptionResult!
var snodeToExclude: Snode?
if case .snode(let snode) = destination { snodeToExclude = snode }
return getPath(excluding: snodeToExclude)
.then2 { path -> Promise<AESGCM.EncryptionResult> in
guardSnode = path.first!
// Encrypt in reverse order, i.e. the destination first
return encrypt(payload, for: destination)
.then2 { r -> Promise<AESGCM.EncryptionResult> in
targetSnodeSymmetricKey = r.symmetricKey
// Recursively encrypt the layers of the onion (again in reverse order)
encryptionResult = r
var path = path
var rhs = destination
func addLayer() -> Promise<AESGCM.EncryptionResult> {
guard !path.isEmpty else {
return Promise<AESGCM.EncryptionResult> { $0.fulfill(encryptionResult) }
}
let lhs = OnionRequestAPIDestination.snode(path.removeLast())
return OnionRequestAPI
.encryptHop(from: lhs, to: rhs, using: encryptionResult)
.then2 { r -> Promise<AESGCM.EncryptionResult> in
encryptionResult = r
rhs = lhs
return addLayer()
}
}
return addLayer()
}
}
.map2 { _ in (guardSnode, encryptionResult, targetSnodeSymmetricKey) }
}
/// Builds an onion around `payload` and returns the result.
private static func buildOnion(
@ -926,123 +680,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
}
}
private static func handleResponse(
responseData: Data,
destinationSymmetricKey: Data,
version: OnionRequestAPIVersion,
destination: OnionRequestAPIDestination,
seal: Resolver<(OnionRequestResponseInfoType, Data?)>
) {
switch version {
// V2 and V3 Onion Requests have the same structure for responses
case .v2, .v3:
let json: JSON
if let processedJson = try? JSONSerialization.jsonObject(with: responseData, options: [ .fragmentsAllowed ]) as? JSON {
json = processedJson
}
else if let result: String = String(data: responseData, encoding: .utf8) {
json = [ "result": result ]
}
else {
return seal.reject(HTTP.Error.invalidJSON)
}
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.")
return seal.reject(SnodeAPIError.clockOutOfSync)
}
if statusCode == 401 { // Signature verification failed
SNLog("Failed to verify the signature.")
return seal.reject(SnodeAPIError.signatureVerificationFailed)
}
if let bodyAsString = json["body"] as? String {
guard let bodyAsData = bodyAsString.data(using: .utf8) else {
return seal.reject(HTTP.Error.invalidResponse)
}
guard let body = try? JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else {
return seal.reject(OnionRequestAPIError.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: bodyAsData, destination: destination))
}
if let timestamp = body["t"] as? Int64 {
let offset = timestamp - Int64(floor(Date().timeIntervalSince1970 * 1000))
SnodeAPI.clockOffset.mutate { $0 = offset }
}
guard 200...299 ~= statusCode else {
return seal.reject(OnionRequestAPIError.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: bodyAsData, destination: destination))
}
return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), bodyAsData))
}
guard 200...299 ~= statusCode else {
return seal.reject(OnionRequestAPIError.httpRequestFailedAtDestination(statusCode: UInt(statusCode), data: data, destination: destination))
}
return seal.fulfill((OnionRequestAPI.ResponseInfo(code: statusCode, headers: [:]), data))
}
catch {
return seal.reject(error)
}
// V4 Onion Requests have a very different structure for responses
case .v4:
guard responseData.count >= AESGCM.ivSize else { return seal.reject(HTTP.Error.invalidResponse) }
do {
let data: Data = try AESGCM.decrypt(responseData, with: destinationSymmetricKey)
// Process the bencoded response
guard let processedResponse: (info: ResponseInfo, body: Data?) = process(bencodedData: data) else {
return seal.reject(HTTP.Error.invalidResponse)
}
// Custom handle a clock out of sync error (v4 returns '425' but included the '406'
// just in case)
guard processedResponse.info.code != 406 && processedResponse.info.code != 425 else {
SNLog("The user's clock is out of sync with the service node network.")
return seal.reject(SnodeAPIError.clockOutOfSync)
}
guard processedResponse.info.code != 401 else { // Signature verification failed
SNLog("Failed to verify the signature.")
return seal.reject(SnodeAPIError.signatureVerificationFailed)
}
// Handle error status codes
guard 200...299 ~= processedResponse.info.code else {
return seal.reject(
OnionRequestAPIError.httpRequestFailedAtDestination(
statusCode: UInt(processedResponse.info.code),
data: data,
destination: destination
)
)
}
return seal.fulfill(processedResponse)
}
catch {
return seal.reject(error)
}
}
}
private static func handleResponse(
responseData: Data,
destinationSymmetricKey: Data,

View File

@ -1,13 +0,0 @@
import PromiseKit
extension Promise : Hashable {
public func hash(into hasher: inout Hasher) {
let reference = ObjectIdentifier(self)
hasher.combine(reference.hashValue)
}
public static func == (lhs: Promise, rhs: Promise) -> Bool {
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
}
}

View File

@ -1,91 +0,0 @@
import PromiseKit
public extension Thenable {
@discardableResult
func then2<U>(_ body: @escaping (T) throws -> U) -> Promise<U.T> where U : Thenable {
return then(on: Threading.workQueue, body)
}
@discardableResult
func map2<U>(_ transform: @escaping (T) throws -> U) -> Promise<U> {
return map(on: Threading.workQueue, transform)
}
@discardableResult
func done2(_ body: @escaping (T) throws -> Void) -> Promise<Void> {
return done(on: Threading.workQueue, body)
}
@discardableResult
func get2(_ body: @escaping (T) throws -> Void) -> Promise<T> {
return get(on: Threading.workQueue, body)
}
}
public extension Thenable where T: Sequence {
@discardableResult
func mapValues2<U>(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> {
return mapValues(on: Threading.workQueue, transform)
}
}
public extension Guarantee {
@discardableResult
func then2<U>(_ body: @escaping (T) -> Guarantee<U>) -> Guarantee<U> {
return then(on: Threading.workQueue, body)
}
@discardableResult
func map2<U>(_ body: @escaping (T) -> U) -> Guarantee<U> {
return map(on: Threading.workQueue, body)
}
@discardableResult
func done2(_ body: @escaping (T) -> Void) -> Guarantee<Void> {
return done(on: Threading.workQueue, body)
}
@discardableResult
func get2(_ body: @escaping (T) -> Void) -> Guarantee<T> {
return get(on: Threading.workQueue, body)
}
}
public extension CatchMixin {
@discardableResult
func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer {
return self.catch(on: Threading.workQueue, body)
}
@discardableResult
func recover2<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T {
return recover(on: Threading.workQueue, body)
}
@discardableResult
func recover2(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T> {
return recover(on: Threading.workQueue, body)
}
@discardableResult
func ensure2(_ body: @escaping () -> Void) -> Promise<T> {
return ensure(on: Threading.workQueue, body)
}
}
public extension CatchMixin where T == Void {
@discardableResult
func recover2(_ body: @escaping(Error) -> Void) -> Guarantee<Void> {
return recover(on: Threading.workQueue, body)
}
@discardableResult
func recover2(_ body: @escaping(Error) throws -> Void) -> Promise<Void> {
return recover(on: Threading.workQueue, body)
}
}

View File

@ -3,7 +3,7 @@
import Foundation
import Combine
import GRDB
import PromiseKit
import SignalCoreKit
open class Storage {
private static let dbFileName: String = "Session.sqlite"
@ -414,50 +414,6 @@ open class Storage {
}
}
// MARK: - Promise Extensions
public extension Storage {
// FIXME: Would be good to replace these with Swift Combine
@discardableResult func read<T>(_ value: (Database) throws -> Promise<T>) -> Promise<T> {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
return Promise(error: StorageError.databaseInvalid)
}
do {
return try dbWriter.read(value)
}
catch {
return Promise(error: error)
}
}
// FIXME: Can't overrwrite this in `SynchronousStorage` since it's in an extension
@discardableResult func writeAsync<T>(updates: @escaping (Database) throws -> Promise<T>) -> Promise<T> {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
return Promise(error: StorageError.databaseInvalid)
}
let (promise, seal) = Promise<T>.pending()
dbWriter.asyncWrite(
{ db in
try updates(db)
.done { result in seal.fulfill(result) }
.catch { error in seal.reject(error) }
.retainUntilComplete()
},
completion: { _, result in
switch result {
case .failure(let error): seal.reject(error)
default: break
}
}
)
return promise
}
}
// MARK: - Combine Extensions
public extension Storage {

View File

@ -1,5 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import PromiseKit
import Combine
public enum HTTP {
private static let seedNodeURLSession = URLSession(configuration: .ephemeral, delegate: seedNodeURLSessionDelegate, delegateQueue: nil)
@ -67,75 +69,6 @@ public enum HTTP {
}
}
// MARK: - Main
public static func executeLegacy(
_ method: HTTPMethod,
_ url: String,
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> Promise<Data> {
return executeLegacy(method, url, body: nil, timeout: timeout, useSeedNodeURLSession: useSeedNodeURLSession)
}
public static func executeLegacy(
_ method: HTTPMethod,
_ url: String,
body: Data?,
timeout: TimeInterval = HTTP.defaultTimeout,
useSeedNodeURLSession: Bool = false
) -> Promise<Data> {
var request = URLRequest(url: URL(string: url)!)
request.httpMethod = verb.rawValue
request.httpBody = body
request.timeoutInterval = timeout
request.allHTTPHeaderFields?.removeValue(forKey: "User-Agent")
request.setValue("WhatsApp", forHTTPHeaderField: "User-Agent") // Set a fake value
request.setValue("en-us", forHTTPHeaderField: "Accept-Language") // Set a fake value
let (promise, seal) = Promise<Data>.pending()
let urlSession = useSeedNodeURLSession ? seedNodeURLSession : snodeURLSession
let task = urlSession.dataTask(with: request) { data, response, error in
guard let data = data, let response = response as? HTTPURLResponse else {
if let error = error {
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
} else {
SNLog("\(verb.rawValue) request to \(url) failed.")
}
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
switch (error as? NSError)?.code {
case NSURLErrorTimedOut: return seal.reject(Error.timeout)
default: return seal.reject(Error.httpRequestFailed(statusCode: 0, data: nil))
}
}
if let error = error {
SNLog("\(verb.rawValue) request to \(url) failed due to error: \(error).")
// Override the actual error so that we can correctly catch failed requests in sendOnionRequest(invoking:on:with:)
return seal.reject(Error.httpRequestFailed(statusCode: 0, data: data))
}
let statusCode = UInt(response.statusCode)
guard 200...299 ~= statusCode else {
var json: JSON? = nil
if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {
json = processedJson
}
else if let result: String = String(data: data, encoding: .utf8) {
json = [ "result": result ]
}
let jsonDescription: String = (json?.prettifiedDescription ?? "no debugging info provided")
SNLog("\(verb.rawValue) request to \(url) failed with status code: \(statusCode) (\(jsonDescription)).")
return seal.reject(Error.httpRequestFailed(statusCode: statusCode, data: data))
}
seal.fulfill(data)
}
task.resume()
return promise
}
// MARK: - Execution
public static func execute(
@ -193,7 +126,7 @@ public enum HTTP {
.eraseToAnyPublisher()
}
let statusCode = UInt(response.statusCode)
// TODO: Remove all the JSON handling?
// TODO: Remove all the JSON handling?
guard 200...299 ~= statusCode else {
var json: JSON? = nil
if let processedJson: JSON = try? JSONSerialization.jsonObject(with: data, options: [ .fragmentsAllowed ]) as? JSON {

View File

@ -0,0 +1,14 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation
public extension URLResponse {
var stringEncoding: String.Encoding? {
guard let encodingName = textEncodingName else { return nil }
let encoding = CFStringConvertIANACharSetNameToEncoding(encodingName as CFString)
guard encoding != kCFStringEncodingInvalidId else { return nil }
return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(encoding))
}
}

View File

@ -1,10 +0,0 @@
import PromiseKit
public extension AnyPromise {
static func from<T : Any>(_ promise: Promise<T>) -> AnyPromise {
let result = AnyPromise(promise)
result.retainUntilComplete()
return result
}
}

View File

@ -1,14 +0,0 @@
import PromiseKit
/// Delay the execution of the promise constructed in `body` by `delay` seconds.
public func withDelay<T>(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
let (promise, seal) = Promise<T>.pending()
Timer.scheduledTimerOnMainThread(withTimeInterval: delay, repeats: false) { _ in
body().done(on: completionQueue) {
seal.fulfill($0)
}.catch(on: completionQueue) {
seal.reject($0)
}
}
return promise
}

View File

@ -1,45 +0,0 @@
import PromiseKit
public extension AnyPromise {
@objc func retainUntilComplete() {
var retainCycle: AnyPromise? = self
_ = self.ensure {
assert(retainCycle != nil)
retainCycle = nil
}
}
}
public extension PMKFinalizer {
func retainUntilComplete() {
var retainCycle: PMKFinalizer? = self
self.finally {
assert(retainCycle != nil)
retainCycle = nil
}
}
}
public extension Promise {
func retainUntilComplete() {
var retainCycle: Promise<T>? = self
_ = self.ensure {
assert(retainCycle != nil)
retainCycle = nil
}
}
}
public extension Guarantee {
func retainUntilComplete() {
var retainCycle: Guarantee<T>? = self
_ = self.done { _ in
assert(retainCycle != nil)
retainCycle = nil
}
}
}

View File

@ -1,14 +0,0 @@
import PromiseKit
/// Retry the promise constructed in `body` up to `maxRetryCount` times.
public func attempt<T>(maxRetryCount: UInt, recoveringOn queue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
var retryCount = 0
func attempt() -> Promise<T> {
return body().recover(on: queue) { error -> Promise<T> in
guard retryCount < maxRetryCount else { throw error }
retryCount += 1
return attempt()
}
}
return attempt()
}

View File

@ -1,19 +0,0 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import PromiseKit
public extension Promise {
func timeout(seconds: TimeInterval, timeoutError: Error) -> Promise<T> {
return Promise<T> { seal in
after(seconds: seconds).done {
seal.reject(timeoutError)
}
self.done { result in
seal.fulfill(result)
}.catch { err in
seal.reject(err)
}
}
}
}

View File

@ -6,7 +6,6 @@ import Foundation
import AVFoundation
import MediaPlayer
import CoreServices
import PromiseKit
import SessionUIKit
import SessionMessagingKit
import SignalCoreKit

View File

@ -1,7 +1,6 @@
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
import Foundation
import PromiseKit
import SessionMessagingKit
import SignalCoreKit