Refactored the remaining references to PromiseKit
# Conflicts: # Session.xcodeproj/project.pbxproj # Session/Media Viewing & Editing/PhotoCapture.swift # SessionMessagingKit/Jobs/Types/GarbageCollectionJob.swift # SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift # SessionMessagingKit/Utilities/Promise+Utilities.swift # SessionShareExtension/ShareVC.swift # SessionShareExtension/ThreadPickerVC.swift # SessionSnodeKit/OnionRequestAPI+Encryption.swift # SessionSnodeKit/OnionRequestAPI.swift # SessionUtilitiesKit/Database/Storage.swift # SessionUtilitiesKit/Networking/HTTP.swift
This commit is contained in:
parent
8b37002d89
commit
6970ff22cc
|
@ -107,7 +107,6 @@
|
||||||
7B1B52DF28580D51006069F2 /* EmojiPickerCollectionView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7B1B52DB28580D50006069F2 /* EmojiPickerCollectionView.swift */; };
|
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 */,
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import GRDB
|
import GRDB
|
||||||
import PromiseKit
|
import SignalCoreKit
|
||||||
import SessionUtilitiesKit
|
import SessionUtilitiesKit
|
||||||
import SessionSnodeKit
|
import SessionSnodeKit
|
||||||
|
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
|
||||||
// )
|
|
||||||
// )
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import PromiseKit
|
|
||||||
import SessionSnodeKit
|
|
||||||
import SessionUtilitiesKit
|
|
||||||
|
|
||||||
extension Promise where T == Data {
|
|
||||||
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<R> {
|
|
||||||
self.map(on: queue) { data -> R in
|
|
||||||
try data.decoded(as: type, using: dependencies)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Promise where T == (OnionRequestResponseInfoType, Data?) {
|
|
||||||
func decoded<R: Decodable>(as type: R.Type, on queue: DispatchQueue? = nil, using dependencies: Dependencies = Dependencies()) -> Promise<(OnionRequestResponseInfoType, R)> {
|
|
||||||
self.map(on: queue) { responseInfo, maybeData -> (OnionRequestResponseInfoType, R) in
|
|
||||||
guard let data: Data = maybeData else { throw HTTP.Error.parsingFailed }
|
|
||||||
|
|
||||||
do {
|
|
||||||
return (responseInfo, try data.decoded(as: type, using: dependencies))
|
|
||||||
}
|
|
||||||
catch {
|
|
||||||
throw HTTP.Error.parsingFailed
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,15 +1,15 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
// 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)
|
||||||
|
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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()
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
extension Promise : Hashable {
|
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
let reference = ObjectIdentifier(self)
|
|
||||||
hasher.combine(reference.hashValue)
|
|
||||||
}
|
|
||||||
|
|
||||||
public static func == (lhs: Promise, rhs: Promise) -> Bool {
|
|
||||||
return ObjectIdentifier(lhs) == ObjectIdentifier(rhs)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,91 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
public extension Thenable {
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func then2<U>(_ body: @escaping (T) throws -> U) -> Promise<U.T> where U : Thenable {
|
|
||||||
return then(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func map2<U>(_ transform: @escaping (T) throws -> U) -> Promise<U> {
|
|
||||||
return map(on: Threading.workQueue, transform)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func done2(_ body: @escaping (T) throws -> Void) -> Promise<Void> {
|
|
||||||
return done(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func get2(_ body: @escaping (T) throws -> Void) -> Promise<T> {
|
|
||||||
return get(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Thenable where T: Sequence {
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func mapValues2<U>(_ transform: @escaping (T.Iterator.Element) throws -> U) -> Promise<[U]> {
|
|
||||||
return mapValues(on: Threading.workQueue, transform)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Guarantee {
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func then2<U>(_ body: @escaping (T) -> Guarantee<U>) -> Guarantee<U> {
|
|
||||||
return then(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func map2<U>(_ body: @escaping (T) -> U) -> Guarantee<U> {
|
|
||||||
return map(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func done2(_ body: @escaping (T) -> Void) -> Guarantee<Void> {
|
|
||||||
return done(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func get2(_ body: @escaping (T) -> Void) -> Guarantee<T> {
|
|
||||||
return get(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension CatchMixin {
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func catch2(_ body: @escaping (Error) -> Void) -> PMKFinalizer {
|
|
||||||
return self.catch(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func recover2<U: Thenable>(_ body: @escaping(Error) throws -> U) -> Promise<T> where U.T == T {
|
|
||||||
return recover(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func recover2(_ body: @escaping(Error) -> Guarantee<T>) -> Guarantee<T> {
|
|
||||||
return recover(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func ensure2(_ body: @escaping () -> Void) -> Promise<T> {
|
|
||||||
return ensure(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension CatchMixin where T == Void {
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func recover2(_ body: @escaping(Error) -> Void) -> Guarantee<Void> {
|
|
||||||
return recover(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
|
|
||||||
@discardableResult
|
|
||||||
func recover2(_ body: @escaping(Error) throws -> Void) -> Promise<Void> {
|
|
||||||
return recover(on: Threading.workQueue, body)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,7 +3,7 @@
|
||||||
import Foundation
|
import 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 {
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public extension URLResponse {
|
||||||
|
var stringEncoding: String.Encoding? {
|
||||||
|
guard let encodingName = textEncodingName else { return nil }
|
||||||
|
|
||||||
|
let encoding = CFStringConvertIANACharSetNameToEncoding(encodingName as CFString)
|
||||||
|
guard encoding != kCFStringEncodingInvalidId else { return nil }
|
||||||
|
|
||||||
|
return String.Encoding(rawValue: CFStringConvertEncodingToNSStringEncoding(encoding))
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,10 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
public extension AnyPromise {
|
|
||||||
|
|
||||||
static func from<T : Any>(_ promise: Promise<T>) -> AnyPromise {
|
|
||||||
let result = AnyPromise(promise)
|
|
||||||
result.retainUntilComplete()
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
/// Delay the execution of the promise constructed in `body` by `delay` seconds.
|
|
||||||
public func withDelay<T>(_ delay: TimeInterval, completionQueue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
|
||||||
let (promise, seal) = Promise<T>.pending()
|
|
||||||
Timer.scheduledTimerOnMainThread(withTimeInterval: delay, repeats: false) { _ in
|
|
||||||
body().done(on: completionQueue) {
|
|
||||||
seal.fulfill($0)
|
|
||||||
}.catch(on: completionQueue) {
|
|
||||||
seal.reject($0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return promise
|
|
||||||
}
|
|
|
@ -1,45 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
public extension AnyPromise {
|
|
||||||
|
|
||||||
@objc func retainUntilComplete() {
|
|
||||||
var retainCycle: AnyPromise? = self
|
|
||||||
_ = self.ensure {
|
|
||||||
assert(retainCycle != nil)
|
|
||||||
retainCycle = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension PMKFinalizer {
|
|
||||||
|
|
||||||
func retainUntilComplete() {
|
|
||||||
var retainCycle: PMKFinalizer? = self
|
|
||||||
self.finally {
|
|
||||||
assert(retainCycle != nil)
|
|
||||||
retainCycle = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Promise {
|
|
||||||
|
|
||||||
func retainUntilComplete() {
|
|
||||||
var retainCycle: Promise<T>? = self
|
|
||||||
_ = self.ensure {
|
|
||||||
assert(retainCycle != nil)
|
|
||||||
retainCycle = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public extension Guarantee {
|
|
||||||
|
|
||||||
func retainUntilComplete() {
|
|
||||||
var retainCycle: Guarantee<T>? = self
|
|
||||||
_ = self.done { _ in
|
|
||||||
assert(retainCycle != nil)
|
|
||||||
retainCycle = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +0,0 @@
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
/// Retry the promise constructed in `body` up to `maxRetryCount` times.
|
|
||||||
public func attempt<T>(maxRetryCount: UInt, recoveringOn queue: DispatchQueue, body: @escaping () -> Promise<T>) -> Promise<T> {
|
|
||||||
var retryCount = 0
|
|
||||||
func attempt() -> Promise<T> {
|
|
||||||
return body().recover(on: queue) { error -> Promise<T> in
|
|
||||||
guard retryCount < maxRetryCount else { throw error }
|
|
||||||
retryCount += 1
|
|
||||||
return attempt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return attempt()
|
|
||||||
}
|
|
|
@ -1,19 +0,0 @@
|
||||||
// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.
|
|
||||||
|
|
||||||
import PromiseKit
|
|
||||||
|
|
||||||
public extension Promise {
|
|
||||||
|
|
||||||
func timeout(seconds: TimeInterval, timeoutError: Error) -> Promise<T> {
|
|
||||||
return Promise<T> { seal in
|
|
||||||
after(seconds: seconds).done {
|
|
||||||
seal.reject(timeoutError)
|
|
||||||
}
|
|
||||||
self.done { result in
|
|
||||||
seal.fulfill(result)
|
|
||||||
}.catch { err in
|
|
||||||
seal.reject(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -6,7 +6,6 @@ import Foundation
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import MediaPlayer
|
import MediaPlayer
|
||||||
import CoreServices
|
import CoreServices
|
||||||
import PromiseKit
|
|
||||||
import SessionUIKit
|
import SessionUIKit
|
||||||
import SessionMessagingKit
|
import SessionMessagingKit
|
||||||
import SignalCoreKit
|
import SignalCoreKit
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue