Refactored the remaining references to PromiseKit

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

View File

@ -107,7 +107,6 @@
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; }; 7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; };
7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; }; 7B1B52E028580D51006069F2 /* EmojiSkinTonePicker.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DC28580D50006069F2 /* EmojiSkinTonePicker.swift */; };
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.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 */; }; 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1D74AF27C365960030B423 /* Timer+MainThread.swift */; };
7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; }; 7B46AAAF28766DF4001AF2DC /* AllMediaViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B46AAAE28766DF4001AF2DC /* AllMediaViewController.swift */; };
7B4C75CB26B37E0F0000AC89 /* UnsendRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B4C75CA26B37E0F0000AC89 /* UnsendRequest.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 */; }; 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, ); }; }; 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, ); }; }; 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 */; }; C3402FE52559036600EA6424 /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */; }; C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3471ECA2555356A00297E91 /* MessageSender+Encryption.swift */; };
C3471ED42555386B00297E91 /* AESGCM.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D72553860B00C340D1 /* AESGCM.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 */; }; C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; };
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; }; C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.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 */; }; C3AAFFF225AE99710089E6DD /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFF125AE99710089E6DD /* AppDelegate.swift */; };
C3ADC66126426688005F1414 /* ShareVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareVC.swift */; }; C3ADC66126426688005F1414 /* ShareNavController.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3ADC66026426688005F1414 /* ShareNavController.swift */; };
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D62553860B00C340D1 /* Promise+Retrying.swift */; };
C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; }; C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D92553860B00C340D1 /* JSON.swift */; };
C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BC255385EE00C340D1 /* HTTP.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 */; }; 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 */; }; 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 */; }; C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */; };
C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5BE255385EE00C340D1 /* SnodeAPI.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 */; }; C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D22553860900C340D1 /* String+Trimming.swift */; };
C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D42553860A00C340D1 /* Threading.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 */; }; 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 */; }; FD245C6B2850667400B966DD /* VisibleMessage+Profile.swift in Sources */ = {isa = PBXBuildFile; fileRef = C300A5B12554AF9800555489 /* VisibleMessage+Profile.swift */; };
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; }; FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A31225574F5200338F3E /* MessageReceiveJob.swift */; };
FD245C6D285066A400B966DD /* NotifyPushServerJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = C352A32E2557549C00338F3E /* NotifyPushServerJob.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 */; }; FD2AAAED28ED3E1000A49611 /* MockGeneralCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDFD645C27F273F300808CA1 /* MockGeneralCache.swift */; };
FD2AAAEE28ED3E1100A49611 /* 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 */; }; 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 */; }; FD8ECF9029381FC200C0D1BB /* SessionUtil+UserProfile.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF8F29381FC200C0D1BB /* SessionUtil+UserProfile.swift */; };
FD8ECF922938552800C0D1BB /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF912938552800C0D1BB /* Threading.swift */; }; FD8ECF922938552800C0D1BB /* Threading.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF912938552800C0D1BB /* Threading.swift */; };
FD8ECF94293856AF00C0D1BB /* Randomness.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD8ECF93293856AF00C0D1BB /* Randomness.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 */; }; FD90040F2818AB6D00ABAAF6 /* GetSnodePoolJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD90040E2818AB6D00ABAAF6 /* GetSnodePoolJob.swift */; };
FD9004122818ABDC00ABAAF6 /* Job.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDF0B73F280402C4004C14C5 /* Job.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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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; }; 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>"; }; FDC4389927BA002500C60D73 /* OpenGroupAPISpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupAPISpec.swift; sourceTree = "<group>"; };
@ -2522,18 +2531,6 @@
path = Crypto; path = Crypto;
sourceTree = "<group>"; 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 */ = { B8A582AE258C65D000AFD84C /* Networking */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -2542,6 +2539,8 @@
B8FF8EA525C11FEF004D1F22 /* IPv4.swift */, B8FF8EA525C11FEF004D1F22 /* IPv4.swift */,
C3C2A5D92553860B00C340D1 /* JSON.swift */, C3C2A5D92553860B00C340D1 /* JSON.swift */,
C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */, C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */,
C3C2A5BC255385EE00C340D1 /* HTTP.swift */,
FD8ECFA0293D8FDD00C0D1BB /* URLResponse+Utilities.swift */,
); );
path = Networking; path = Networking;
sourceTree = "<group>"; sourceTree = "<group>";
@ -3252,8 +3251,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */, C3C2A5D82553860B00C340D1 /* Data+Utilities.swift */,
C3C2A5CF2553860700C340D1 /* Promise+Hashing.swift */,
C3C2A5D02553860800C340D1 /* Promise+Threading.swift */,
C3C2A5D22553860900C340D1 /* String+Trimming.swift */, C3C2A5D22553860900C340D1 /* String+Trimming.swift */,
C3C2A5D42553860A00C340D1 /* Threading.swift */, C3C2A5D42553860A00C340D1 /* Threading.swift */,
); );
@ -3271,7 +3268,6 @@
FD9004102818ABB000ABAAF6 /* JobRunner */, FD9004102818ABB000ABAAF6 /* JobRunner */,
B8A582AF258C665E00AFD84C /* Media */, B8A582AF258C665E00AFD84C /* Media */,
B8A582AE258C65D000AFD84C /* Networking */, B8A582AE258C65D000AFD84C /* Networking */,
B8A582AD258C655E00AFD84C /* PromiseKit */,
FD09796527F6B0A800936362 /* Utilities */, FD09796527F6B0A800936362 /* Utilities */,
FD37E9FE28A5F2CD003AE748 /* Configuration.swift */, FD37E9FE28A5F2CD003AE748 /* Configuration.swift */,
); );
@ -5370,11 +5366,19 @@
C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */, C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */,
FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */, FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */,
FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FD17D7A427F40F8100122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */,
C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */,
FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */, FD17D7D227F5797A00122BE0 /* SnodeAPIEndpoint.swift in Sources */,
C3C2A5DE2553860B00C340D1 /* String+Trimming.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 */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */,
FD17D7A727F41AF000122BE0 /* SSKLegacy.swift in Sources */, FD17D7A727F41AF000122BE0 /* SSKLegacy.swift in Sources */,
FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */, FDC438B327BB15B400C60D73 /* ResponseInfo.swift in Sources */,
@ -5395,7 +5399,6 @@
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */, FDFD645927F26C6800808CA1 /* Array+Utilities.swift in Sources */,
7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */,
C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */, C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */,
7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */, 7B1D74B027C365960030B423 /* Timer+MainThread.swift in Sources */,
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */, C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */,
@ -5422,7 +5425,7 @@
FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */, FD71160228C8255900B47552 /* UIControl+Combine.swift in Sources */,
FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */, FD9004152818B46300ABAAF6 /* JobRunner.swift in Sources */,
C3A7211A2558BCA10043A11F /* DiffieHellman.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 */, FD17D7CA27F546D900122BE0 /* _001_InitialSetupMigration.swift in Sources */,
C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */,
C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Utilities.swift in Sources */,
@ -5440,6 +5443,7 @@
FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */, FD848B9328420164000E298B /* UnicodeScalar+Utilities.swift in Sources */,
FD09796B27F6C67500936362 /* Failable.swift in Sources */, FD09796B27F6C67500936362 /* Failable.swift in Sources */,
FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */, FD7115FA28C8153400B47552 /* UIBarButtonItem+Combine.swift in Sources */,
FD8ECFA1293D8FDD00C0D1BB /* URLResponse+Utilities.swift in Sources */,
FD705A92278D051200F16121 /* ReusableView.swift in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */,
FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */,
FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */, FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */,
@ -5458,14 +5462,12 @@
C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */,
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */,
B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Utilities.swift in Sources */,
C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */,
7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */, 7BD477A827EC39F5004E2822 /* Atomic.swift in Sources */,
B8BC00C0257D90E30032E807 /* General.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */,
FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */, FD17D7A127F40D2500122BE0 /* Storage.swift in Sources */,
FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */, FD1A94FB2900D1C2000D73D3 /* PersistableRecord+Utilities.swift in Sources */,
FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */, FD5D201E27B0D87C00FEA984 /* SessionId.swift in Sources */,
C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */,
C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */,
FD8ECF922938552800C0D1BB /* Threading.swift in Sources */, FD8ECF922938552800C0D1BB /* Threading.swift in Sources */,
B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */, B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */,
FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */, FDF22211281B5E0B000A4995 /* TableRecord+Utilities.swift in Sources */,
@ -5492,8 +5494,7 @@
FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */, FD9004162818B46700ABAAF6 /* JobRunnerError.swift in Sources */,
FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */, FD09797227FAA2F500936362 /* Optional+Utilities.swift in Sources */,
FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */, FD7162DB281B6C440060647B /* TypedTableAlias.swift in Sources */,
FDFD645B27F26D4600808CA1 /* Data+Utilities.swift in Sources */, FD26FA5E291CAFF9005801D8 /* Data+Utilities.swift in Sources */,
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */,
FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */, FD7115F828C8151C00B47552 /* DisposableBarButtonItem.swift in Sources */,
FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */, FD17D7E727F6A16700122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
); );

View File

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

View File

@ -345,8 +345,9 @@ extension ConversationVC:
dataSource: dataSource, dataSource: dataSource,
dataUTI: kUTTypeMPEG4 as String dataUTI: kUTTypeMPEG4 as String
) )
.attachmentPromise .attachmentPublisher
.done { attachment in .sinkUntilComplete(
receiveValue: { [weak self] attachment in
guard guard
!modalActivityIndicator.wasCancelled, !modalActivityIndicator.wasCancelled,
let attachment = attachment as? SignalAttachment let attachment = attachment as? SignalAttachment
@ -361,7 +362,7 @@ extension ConversationVC:
self?.showAttachmentApprovalDialog(for: [ attachment ]) self?.showAttachmentApprovalDialog(for: [ attachment ])
} }
} }
.retainUntilComplete() )
} }
} }
@ -422,8 +423,8 @@ extension ConversationVC:
) )
// Send the message // Send the message
Storage.shared.writeAsync( Storage.shared
updates: { [weak self] db in .writePublisher { [weak self] db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
return return
} }
@ -485,8 +486,9 @@ extension ConversationVC:
interaction: interaction, interaction: interaction,
in: thread in: thread
) )
}, }
completion: { [weak self] _, _ in .sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent() self?.handleMessageSent()
} }
) )
@ -545,8 +547,8 @@ extension ConversationVC:
) )
// Send the message // Send the message
Storage.shared.writeAsync( Storage.shared
updates: { [weak self] db in .writePublisher { [weak self] db in
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else { guard let thread: SessionThread = try SessionThread.fetchOne(db, id: threadId) else {
return return
} }
@ -575,14 +577,27 @@ extension ConversationVC:
.fetchOne(db) .fetchOne(db)
).inserted(db) ).inserted(db)
try MessageSender.send( 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, db,
interaction: interaction, interaction: interaction,
with: attachments,
in: thread in: thread
) )
}, }
completion: { [weak self] _, _ in .sinkUntilComplete(
receiveCompletion: { [weak self] _ in
self?.handleMessageSent() self?.handleMessageSent()
// Attachment successfully sent - dismiss the screen // Attachment successfully sent - dismiss the screen
@ -1419,7 +1434,7 @@ extension ConversationVC:
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
return MessageSender.sendImmediate(data: sendData) return MessageSender.sendImmediate(preparedSendData: sendData)
} }
.sinkUntilComplete() .sinkUntilComplete()
} }

View File

@ -450,10 +450,10 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
// Initially position offscreen, we'll animate it in. // Initially position offscreen, we'll animate it in.
collectionPickerView.frame = collectionPickerView.frame.offsetBy(dx: 0, dy: collectionPickerView.frame.height) 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() collectionPickerView.superview?.layoutIfNeeded()
self.titleView.rotateIcon(.up) self.titleView.rotateIcon(.up)
}.retainUntilComplete() }
} }
func hideCollectionPicker() { func hideCollectionPicker() {
@ -462,13 +462,17 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
assert(isShowingCollectionPickerController) assert(isShowingCollectionPickerController)
isShowingCollectionPickerController = false isShowingCollectionPickerController = false
UIView.animate(.promise, duration: 0.25, delay: 0, options: .curveEaseInOut) { UIView.animate(
withDuration: 0.25,
animations: {
self.collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height) self.collectionPickerController.view.frame = self.view.frame.offsetBy(dx: 0, dy: self.view.frame.height)
self.titleView.rotateIcon(.down) self.titleView.rotateIcon(.down)
}.done { _ in },
self.collectionPickerController.view.removeFromSuperview() completion: { [weak self] _ in
self.collectionPickerController.removeFromParent() self?.collectionPickerController.view.removeFromSuperview()
}.retainUntilComplete() self?.collectionPickerController.removeFromParent()
}
)
} }
// MARK: - UICollectionView // MARK: - UICollectionView

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,8 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import Combine
import GRDB import GRDB
import PromiseKit
import WebRTC import WebRTC
import SessionUtilitiesKit import SessionUtilitiesKit
@ -80,7 +80,7 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
// MARK: - Error // MARK: - Error
public enum Error : LocalizedError { public enum WebRTCSessionError: LocalizedError {
case noThread case noThread
public var errorDescription: String? { public var errorDescription: String? {
@ -124,38 +124,48 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
message: CallMessage, message: CallMessage,
interactionId: Int64?, interactionId: Int64?,
in thread: SessionThread in thread: SessionThread
) throws -> Promise<Void> { ) throws -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending pre-offer message.") SNLog("[Calls] Sending pre-offer message.")
return try MessageSender return MessageSender
.sendNonDurably( .sendImmediate(
preparedSendData: try MessageSender
.preparedSendData(
db, db,
message: message, message: message,
interactionId: interactionId, to: try Message.Destination.from(db, thread: thread),
in: 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( public func sendOffer(
_ db: Database, _ db: Database,
to sessionId: String, to sessionId: String,
isRestartingICEConnection: Bool = false isRestartingICEConnection: Bool = false
) -> Promise<Void> { ) -> AnyPublisher<Void, Error> {
SNLog("[Calls] Sending offer message.") SNLog("[Calls] Sending offer message.")
let (promise, seal) = Promise<Void>.pending()
let uuid: String = self.uuid let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection) let mediaConstraints: RTCMediaConstraints = mediaConstraints(isRestartingICEConnection)
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else { 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 return Future<Void, Error> { [weak self] resolver in
self?.peerConnection?.offer(for: mediaConstraints) { sdp, error in
if let error = error { if let error = error {
seal.reject(error) resolver(Result.failure(error))
return return
} }
@ -166,14 +176,15 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
self?.peerConnection?.setLocalDescription(sdp) { error in self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error { if let error = error {
print("Couldn't initiate call due to error: \(error).") print("Couldn't initiate call due to error: \(error).")
return seal.reject(error) resolver(Result.failure(error))
return
} }
} }
Storage.shared Storage.shared
.writeAsync { db in .writePublisher { db in
try MessageSender try MessageSender
.sendNonDurably( .preparedSendData(
db, db,
message: CallMessage( message: CallMessage(
uuid: uuid, uuid: uuid,
@ -181,37 +192,45 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
sdps: [ sdp.sdp ], sdps: [ sdp.sdp ],
sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000)) sentTimestampMs: UInt64(floor(Date().timeIntervalSince1970 * 1000))
), ),
interactionId: nil, to: try Message.Destination.from(db, thread: thread),
in: thread interactionId: nil
) )
} }
.done2 { .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
seal.fulfill(()) .sinkUntilComplete(
receiveCompletion: { result in
switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
} }
.catch2 { error in
seal.reject(error)
} }
.retainUntilComplete() )
}
}
.eraseToAnyPublisher()
} }
return promise public func sendAnswer(to sessionId: String) -> AnyPublisher<Void, Error> {
}
public func sendAnswer(to sessionId: String) -> Promise<Void> {
SNLog("[Calls] Sending answer message.") SNLog("[Calls] Sending answer message.")
let (promise, seal) = Promise<Void>.pending()
let uuid: String = self.uuid let uuid: String = self.uuid
let mediaConstraints: RTCMediaConstraints = mediaConstraints(false) let mediaConstraints: RTCMediaConstraints = mediaConstraints(false)
Storage.shared.writeAsync { [weak self] db in return Storage.shared
.readPublisherFlatMap { db -> AnyPublisher<SessionThread, Error> in
guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else { guard let thread: SessionThread = try? SessionThread.fetchOne(db, id: sessionId) else {
seal.reject(Error.noThread) return Fail(error: WebRTCSessionError.noThread)
return .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 self?.peerConnection?.answer(for: mediaConstraints) { [weak self] sdp, error in
if let error = error { if let error = error {
seal.reject(error) resolver(Result.failure(error))
return return
} }
@ -222,32 +241,37 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
self?.peerConnection?.setLocalDescription(sdp) { error in self?.peerConnection?.setLocalDescription(sdp) { error in
if let error = error { if let error = error {
print("Couldn't accept call due to error: \(error).") print("Couldn't accept call due to error: \(error).")
return seal.reject(error) return resolver(Result.failure(error))
} }
} }
try? MessageSender Storage.shared
.sendNonDurably( .writePublisher { db in
try MessageSender
.preparedSendData(
db, db,
message: CallMessage( message: CallMessage(
uuid: uuid, uuid: uuid,
kind: .answer, kind: .answer,
sdps: [ sdp.sdp ] sdps: [ sdp.sdp ]
), ),
interactionId: nil, to: try Message.Destination.from(db, thread: thread),
in: thread interactionId: nil
) )
.done2 {
seal.fulfill(())
} }
.catch2 { error in .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
seal.reject(error) .sinkUntilComplete(
} receiveCompletion: { result in
.retainUntilComplete() switch result {
case .finished: resolver(Result.success(()))
case .failure(let error): resolver(Result.failure(error))
} }
} }
)
return promise }
}
}
.eraseToAnyPublisher()
} }
private func queueICECandidateForSending(_ candidate: RTCIceCandidate) { private func queueICECandidateForSending(_ candidate: RTCIceCandidate) {
@ -268,12 +292,18 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
// Empty the queue // Empty the queue
self.queuedICECandidates.removeAll() self.queuedICECandidates.removeAll()
Storage.shared.writeAsync { db in Storage.shared
guard let thread: SessionThread = try SessionThread.fetchOne(db, id: contactSessionId) else { return } .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.") SNLog("[Calls] Batch sending \(candidates.count) ICE candidates.")
try MessageSender.sendNonDurably( return Just(
try MessageSender
.preparedSendData(
db, db,
message: CallMessage( message: CallMessage(
uuid: uuid, uuid: uuid,
@ -283,11 +313,15 @@ public final class WebRTCSession : NSObject, RTCPeerConnectionDelegate {
), ),
sdps: candidates.map { $0.sdp } sdps: candidates.map { $0.sdp }
), ),
interactionId: nil, to: try Message.Destination.from(db, thread: thread),
in: thread interactionId: nil
) )
.retainUntilComplete() )
.setFailureType(to: Error.self)
.eraseToAnyPublisher()
} }
.flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
.sinkUntilComplete()
} }
public func endCall(_ db: Database, with sessionId: String) throws { 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.") SNLog("[Calls] Sending end call message.")
try MessageSender.sendNonDurably( let preparedSendData: MessageSender.PreparedSendData = try MessageSender
.preparedSendData(
db, db,
message: CallMessage( message: CallMessage(
uuid: self.uuid, uuid: self.uuid,
kind: .endCall, kind: .endCall,
sdps: [] sdps: []
), ),
interactionId: nil, to: try Message.Destination.from(db, thread: thread),
in: thread interactionId: nil
) )
.retainUntilComplete()
MessageSender
.sendImmediate(preparedSendData: preparedSendData)
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
.sinkUntilComplete()
} }
public func dropConnection() { public func dropConnection() {

View File

@ -972,6 +972,34 @@ extension Attachment {
// MARK: - Upload // MARK: - Upload
extension Attachment { 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( internal func upload(
_ db: Database? = nil, _ db: Database? = nil,
queue: DispatchQueue, queue: DispatchQueue,

View File

@ -446,7 +446,7 @@ public extension LinkPreview {
} }
private static func parse(linkData: Data, response: URLResponse) throws -> Contents { 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.") print("Could not parse link text.")
throw LinkPreviewError.invalidInput throw LinkPreviewError.invalidInput
} }

View File

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

View File

@ -160,22 +160,22 @@ public enum MessageSendJob: JobExecutor {
// Add the threadId to the message if there isn't one set // Add the threadId to the message if there isn't one set
details.message.threadId = (details.message.threadId ?? job.threadId) 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 Storage.shared
.writePublisher { db in .writePublisher { db in
// TODO: Will need to split the attachment upload from the message preparation logic
try MessageSender.preparedSendData( try MessageSender.preparedSendData(
db, db,
message: details.message, message: details.message,
to: details.destination to: details.destination
.with(fileIds: messageFileIds), // TODO: This??? .with(fileIds: messageFileIds),
interactionId: job.interactionId interactionId: job.interactionId
) )
} }
.subscribe(on: queue) .subscribe(on: queue)
// TODO: Is this needed? (should be caught before this??) .flatMap { MessageSender.sendImmediate(preparedSendData: $0) }
// .flatMap { MessageSender.performUploadsIfNeeded(preparedSendData: $0) }
.flatMap { MessageSender.sendImmediate(data: $0) }
.sinkUntilComplete( .sinkUntilComplete(
receiveCompletion: { result in receiveCompletion: { result in
switch result { switch result {

View File

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

View File

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

View File

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

View File

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

View File

@ -3,26 +3,12 @@
import Foundation import Foundation
import Combine import Combine
import GRDB import GRDB
import PromiseKit
import SessionUtilitiesKit import SessionUtilitiesKit
extension MessageSender { extension MessageSender {
// MARK: - Durable // 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 { public static func send(_ db: Database, interaction: Interaction, in thread: SessionThread) throws {
// Only 'VisibleMessage' types can be sent via this method // Only 'VisibleMessage' types can be sent via this method
guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage } guard interaction.variant == .standardOutgoing else { throw MessageSenderError.invalidMessage }
@ -64,33 +50,6 @@ extension MessageSender {
// MARK: - Non-Durable // 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( public static func preparedSendData(
_ db: Database, _ db: Database,
interaction: Interaction, 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( public static func performUploadsIfNeeded(
preparedSendData: PreparedSendData preparedSendData: PreparedSendData
) -> AnyPublisher<PreparedSendData, Error> { ) -> AnyPublisher<PreparedSendData, Error> {
@ -242,7 +102,7 @@ extension MessageSender {
// If there is no attachment data then just return early // If there is no attachment data then just return early
guard !attachmentStateInfo.isEmpty else { return nil } guard !attachmentStateInfo.isEmpty else { return nil }
// TODO: Just run an AttachmentUploadJob directly???
// Otherwise we need to generate the upload requests // Otherwise we need to generate the upload requests
let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId) let openGroup: OpenGroup? = try? OpenGroup.fetchOne(db, id: threadId)
@ -385,23 +245,14 @@ extension MessageSender {
.retainUntilComplete() .retainUntilComplete()
// TODO: Test this (does it break anything? want to stop the db write asap) // 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 return Future<Void, Error> { resolver in
db.afterNextTransaction { _ in db.afterNextTransaction { _ in
// TODO: Remove the 'Swift.' resolver(Result.success(()))
resolver(Swift.Result.success(()))
} }
} }
.flatMap { _ in MessageSender.sendImmediate(data: sendData) } .flatMap { _ in MessageSender.sendImmediate(preparedSendData: sendData) }
.eraseToAnyPublisher() .eraseToAnyPublisher()
// return MessageSender
// .sendImmediate(
// data: try MessageSender.preparedSendData(
// db,
// message: configurationMessage,
// to: destination,
// interactionId: nil
// )
// )
} }
} }

View File

@ -8,52 +8,6 @@ import SessionUtilitiesKit
import Sodium import Sodium
public final class MessageSender { 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 // MARK: - Message Preparation
public struct PreparedSendData { public struct PreparedSendData {
@ -644,19 +598,19 @@ public final class MessageSender {
// MARK: - Sending // MARK: - Sending
public static func sendImmediate( public static func sendImmediate(
data: PreparedSendData, preparedSendData: PreparedSendData,
using dependencies: SMKDependencies = SMKDependencies() using dependencies: SMKDependencies = SMKDependencies()
) -> AnyPublisher<Void, Error> { ) -> AnyPublisher<Void, Error> {
guard data.shouldSend else { guard preparedSendData.shouldSend else {
return Just(()) return Just(())
.setFailureType(to: Error.self) .setFailureType(to: Error.self)
.eraseToAnyPublisher() .eraseToAnyPublisher()
} }
switch data.destination { switch preparedSendData.destination {
case .contact, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies) case .contact, .closedGroup: return sendToSnodeDestination(data: preparedSendData, using: dependencies)
case .openGroup: return sendToOpenGroupDestination(data: data, using: dependencies) case .openGroup: return sendToOpenGroupDestination(data: preparedSendData, using: dependencies)
case .openGroupInbox: return sendToOpenGroupInbox(data: data, using: dependencies) case .openGroupInbox: return sendToOpenGroupInbox(data: preparedSendData, using: dependencies)
case .none: case .none:
return Fail(error: MessageSenderError.invalidMessage) return Fail(error: MessageSenderError.invalidMessage)
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

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

View File

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

View File

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

View File

@ -3,20 +3,9 @@
import Foundation import Foundation
import Combine import Combine
import CryptoSwift import CryptoSwift
import PromiseKit
import SessionUtilitiesKit import SessionUtilitiesKit
internal extension OnionRequestAPI { 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> { 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 | // The encoding of V2 onion requests looks like: | 4 bytes: size N of ciphertext | N bytes: ciphertext | json as utf8 |
guard guard
@ -35,101 +24,40 @@ internal extension OnionRequestAPI {
.eraseToAnyPublisher() .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. /// Encrypts `payload` for `destination` and returns the result. Use this to build the core of an onion request.
static func encrypt( static func encrypt(
_ payload: Data, _ payload: Data,
for destination: OnionRequestAPIDestination for destination: OnionRequestAPIDestination
) -> AnyPublisher<AESGCM.EncryptionResult, Error> { ) -> AnyPublisher<AESGCM.EncryptionResult, Error> {
return Future { resolver in // TODO: Test performance
DispatchQueue.global(qos: .userInitiated).async {
do {
switch destination { switch destination {
case .snode(let snode): case .snode(let snode):
// Need to wrap the payload for snode requests // Need to wrap the payload for snode requests
let data: Data = try encodeLegacy(ciphertext: payload, json: [ "headers" : "" ]) return encode(ciphertext: payload, json: [ "headers" : "" ])
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(data, for: snode.x25519PublicKey) .flatMap { data -> AnyPublisher<AESGCM.EncryptionResult, Error> in
resolver(Swift.Result.success(result)) do {
return Just(try AESGCM.encrypt(data, for: snode.x25519PublicKey))
case .server(_, _, let serverX25519PublicKey, _, _): .setFailureType(to: Error.self)
let result: AESGCM.EncryptionResult = try AESGCM.encrypt(payload, for: serverX25519PublicKey) .eraseToAnyPublisher()
resolver(Swift.Result.success(result))
}
}
catch (let error) {
resolver(Swift.Result.failure(error))
} }
catch {
return Fail(error: error)
.eraseToAnyPublisher()
} }
} }
.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, _, _): case .server(_, _, let serverX25519PublicKey, _, _):
x25519PublicKey = serverX25519PublicKey
}
do { do {
let plaintext = try encodeLegacy(ciphertext: previousEncryptionResult.ciphertext, json: parameters) return Just(try AESGCM.encrypt(payload, for: serverX25519PublicKey))
let result = try AESGCM.encrypt(plaintext, for: x25519PublicKey) .setFailureType(to: Error.self)
seal.fulfill(result) .eraseToAnyPublisher()
} }
catch (let error) { catch {
seal.reject(error) return Fail(error: error)
.eraseToAnyPublisher()
} }
} }
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. /// 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,8 +66,7 @@ internal extension OnionRequestAPI {
to rhs: OnionRequestAPIDestination, to rhs: OnionRequestAPIDestination,
using previousEncryptionResult: AESGCM.EncryptionResult using previousEncryptionResult: AESGCM.EncryptionResult
) -> AnyPublisher<AESGCM.EncryptionResult, Error> { ) -> AnyPublisher<AESGCM.EncryptionResult, Error> {
return Future { resolver in // TODO: Test performance
DispatchQueue.global(qos: .userInitiated).async {
var parameters: JSON var parameters: JSON
switch rhs { switch rhs {
@ -155,25 +82,24 @@ internal extension OnionRequestAPI {
parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString() parameters["ephemeral_key"] = previousEncryptionResult.ephemeralPublicKey.toHexString()
let x25519PublicKey: String let x25519PublicKey: String = {
switch lhs { switch lhs {
case .snode(let snode): case .snode(let snode): return snode.x25519PublicKey
let snodeX25519PublicKey = snode.x25519PublicKey
x25519PublicKey = snodeX25519PublicKey
case .server(_, _, let serverX25519PublicKey, _, _): case .server(_, _, let serverX25519PublicKey, _, _):
x25519PublicKey = serverX25519PublicKey return serverX25519PublicKey
} }
}()
return encode(ciphertext: previousEncryptionResult.ciphertext, json: parameters)
.flatMap { data -> AnyPublisher<AESGCM.EncryptionResult, Error> in
do { do {
let plaintext = try encodeLegacy(ciphertext: previousEncryptionResult.ciphertext, json: parameters) return Just(try AESGCM.encrypt(data, for: x25519PublicKey))
let result = try AESGCM.encrypt(plaintext, for: x25519PublicKey) .setFailureType(to: Error.self)
resolver(Swift.Result.success(result)) .eraseToAnyPublisher()
} }
catch (let error) { catch (let error) {
resolver(Swift.Result.failure(error)) return Fail(error: error)
} .eraseToAnyPublisher()
} }
} }
.eraseToAnyPublisher() .eraseToAnyPublisher()

View File

@ -4,7 +4,6 @@ import Foundation
import Combine import Combine
import CryptoSwift import CryptoSwift
import GRDB import GRDB
import PromiseKit
import SessionUtilitiesKit import SessionUtilitiesKit
public protocol OnionRequestAPIType { 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. /// See the "Onion Requests" section of [The Session Whitepaper](https://arxiv.org/pdf/2002.04609.pdf) for more information.
public enum OnionRequestAPI: OnionRequestAPIType { public enum OnionRequestAPI: OnionRequestAPIType {
private static var buildPathsPromise: Promise<[[Snode]]>? = nil
private static var buildPathsPublisher: Atomic<AnyPublisher<[[Snode]], Error>?> = Atomic(nil) private static var buildPathsPublisher: Atomic<AnyPublisher<[[Snode]], Error>?> = Atomic(nil)
/// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions.
@ -63,40 +61,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
// MARK: - Private API // 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. /// 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> { private static func testSnode(_ snode: Snode) -> AnyPublisher<Void, Error> {
let url = "\(snode.address):\(snode.port)/get_stats/v1" let url = "\(snode.address):\(snode.port)/get_stats/v1"
@ -127,51 +91,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.eraseToAnyPublisher() .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 /// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with
/// `Error.insufficientSnodes` if not enough (reliable) snodes are available. /// `Error.insufficientSnodes` if not enough (reliable) snodes are available.
private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> AnyPublisher<Set<Snode>, Error> { private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> AnyPublisher<Set<Snode>, Error> {
@ -228,59 +147,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
.eraseToAnyPublisher() .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` /// Builds and returns `targetPathCount` paths. The returned promise errors out with `Error.insufficientSnodes`
/// if not enough (reliable) snodes are available. /// if not enough (reliable) snodes are available.
@discardableResult @discardableResult
@ -348,74 +214,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
return publisher 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. /// 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> { private static func getPath(excluding snode: Snode?) -> AnyPublisher<[Snode], Error> {
guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") }
@ -567,50 +365,6 @@ public enum OnionRequestAPI: OnionRequestAPIType {
} }
} }
/// 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. /// Builds an onion around `payload` and returns the result.
private static func buildOnion( private static func buildOnion(
around payload: Data, around payload: Data,
@ -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( private static func handleResponse(
responseData: Data, responseData: Data,
destinationSymmetricKey: Data, destinationSymmetricKey: Data,

View File

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

View File

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

View File

@ -3,7 +3,7 @@
import Foundation import Foundation
import Combine import Combine
import GRDB import GRDB
import PromiseKit import SignalCoreKit
open class Storage { open class Storage {
private static let dbFileName: String = "Session.sqlite" 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 // MARK: - Combine Extensions
public extension Storage { public extension Storage {

View File

@ -1,5 +1,7 @@
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
import Foundation import Foundation
import PromiseKit import Combine
public enum HTTP { public enum HTTP {
private static let seedNodeURLSession = URLSession(configuration: .ephemeral, delegate: seedNodeURLSessionDelegate, delegateQueue: nil) 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 // MARK: - Execution
public static func execute( public static func execute(

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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