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:
parent
8b37002d89
commit
6970ff22cc
|
@ -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 */,
|
||||
);
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import Foundation
|
||||
import GRDB
|
||||
import PromiseKit
|
||||
import SignalCoreKit
|
||||
import SessionUtilitiesKit
|
||||
import SessionSnodeKit
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
// )
|
||||
// )
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,7 +6,6 @@ import Foundation
|
|||
import AVFoundation
|
||||
import MediaPlayer
|
||||
import CoreServices
|
||||
import PromiseKit
|
||||
import SessionUIKit
|
||||
import SessionMessagingKit
|
||||
import SignalCoreKit
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
// Copyright (c) 2019 Open Whisper Systems. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import PromiseKit
|
||||
import SessionMessagingKit
|
||||
import SignalCoreKit
|
||||
|
||||
|
|
Loading…
Reference in New Issue