Merge pull request #401 from oxen-io/cleanup
Remove V1 OGS & File Server Support
This commit is contained in:
commit
641a3f4d06
|
@ -18,9 +18,6 @@
|
|||
341341EF2187467A00192D59 /* ConversationViewModel.m in Sources */ = {isa = PBXBuildFile; fileRef = 341341EE2187467900192D59 /* ConversationViewModel.m */; };
|
||||
3427C64320F500E000EEC730 /* OWSMessageTimerView.m in Sources */ = {isa = PBXBuildFile; fileRef = 3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */; };
|
||||
3430FE181F7751D4000EC51B /* GiphyAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3430FE171F7751D4000EC51B /* GiphyAPI.swift */; };
|
||||
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
|
||||
34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; };
|
||||
34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; };
|
||||
34330AA31E79686200DF2FB9 /* OWSProgressView.m in Sources */ = {isa = PBXBuildFile; fileRef = 34330AA21E79686200DF2FB9 /* OWSProgressView.m */; };
|
||||
34386A54207D271D009F5D9C /* NeverClearView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34386A53207D271C009F5D9C /* NeverClearView.swift */; };
|
||||
3441FD9F21A3604F00BB9542 /* BackupRestoreViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3441FD9E21A3604F00BB9542 /* BackupRestoreViewController.swift */; };
|
||||
|
@ -30,9 +27,6 @@
|
|||
34641E1F2088DA6D00E2EDE5 /* SAEScreenLockViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 34641E1E2088DA6D00E2EDE5 /* SAEScreenLockViewController.m */; };
|
||||
34661FB820C1C0D60056EDD6 /* message_sent.aiff in Resources */ = {isa = PBXBuildFile; fileRef = 34661FB720C1C0D60056EDD6 /* message_sent.aiff */; };
|
||||
346B66311F4E29B200E5122F /* CropScaleImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 346B66301F4E29B200E5122F /* CropScaleImageViewController.swift */; };
|
||||
347850311FD7494A007B8332 /* dripicons-v2.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */; };
|
||||
347850321FD7494A007B8332 /* ElegantIcons.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */; };
|
||||
347850331FD7494A007B8332 /* fontawesome-webfont.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */; };
|
||||
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B66DBF4919D5BBC8006EA940 /* Images.xcassets */; };
|
||||
347850551FD749C0007B8332 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = B6F509951AA53F760068F56A /* Localizable.strings */; };
|
||||
3488F9362191CC4000E524CC /* MediaView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3488F9352191CC4000E524CC /* MediaView.swift */; };
|
||||
|
@ -184,7 +178,6 @@
|
|||
B835247925C38D880089A44F /* MessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B835247825C38D880089A44F /* MessageCell.swift */; };
|
||||
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B835249A25C3AB650089A44F /* VisibleMessageCell.swift */; };
|
||||
B83524A525C3BA4B0089A44F /* InfoMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83524A425C3BA4B0089A44F /* InfoMessageCell.swift */; };
|
||||
B83786802586D296003CE78E /* KeyPairMigrationSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B837867F2586D296003CE78E /* KeyPairMigrationSheet.swift */; };
|
||||
B83F2B88240CB75A000A54AB /* UIImage+Scaling.swift in Sources */ = {isa = PBXBuildFile; fileRef = B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */; };
|
||||
B84664F5235022F30083A1CD /* MentionUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = B84664F4235022F30083A1CD /* MentionUtilities.swift */; };
|
||||
B849789625D4A2F500D0D0B3 /* LinkPreviewView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B849789525D4A2F500D0D0B3 /* LinkPreviewView.swift */; };
|
||||
|
@ -198,7 +191,6 @@
|
|||
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */; };
|
||||
B8569AD325CBA13D00DBA3DB /* MediaTextOverlayView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */; };
|
||||
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8569AE225CBB19A00DBA3DB /* DocumentView.swift */; };
|
||||
B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = B85A68B02587141A008CC492 /* Storage+Resetting.swift */; };
|
||||
B866CE112581C1A900535CC4 /* Sodium+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3E7134E251C867C009649BB /* Sodium+Conversion.swift */; };
|
||||
B86BD08423399ACF000F5AE3 /* Modal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08323399ACF000F5AE3 /* Modal.swift */; };
|
||||
B86BD08623399CEF000F5AE3 /* SeedModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B86BD08523399CEF000F5AE3 /* SeedModal.swift */; };
|
||||
|
@ -241,7 +233,6 @@
|
|||
B893063F2383961A005EAA8E /* ScanQRCodeWrapperVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */; };
|
||||
B894D0752339EDCF00B4D94D /* NukeDataModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = B894D0742339EDCF00B4D94D /* NukeDataModal.swift */; };
|
||||
B897621C25D201F7004F83B2 /* ScrollToBottomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */; };
|
||||
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */; };
|
||||
B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */; };
|
||||
B8AE760B25ABFB5A001A84D2 /* GeneralUtilities.m in Sources */ = {isa = PBXBuildFile; fileRef = B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */; };
|
||||
B8AE761425ABFBB9001A84D2 /* GeneralUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -254,7 +245,6 @@
|
|||
B8C2B2C82563685C00551B4D /* CircleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B2C72563685C00551B4D /* CircleView.swift */; };
|
||||
B8C2B332256376F000551B4D /* ThreadUtil.m in Sources */ = {isa = PBXBuildFile; fileRef = B8C2B331256376F000551B4D /* ThreadUtil.m */; };
|
||||
B8C2B3442563782400551B4D /* ThreadUtil.h in Headers */ = {isa = PBXBuildFile; fileRef = B8C2B33B2563770800551B4D /* ThreadUtil.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
B8CADAE925AFADF400AAFA15 /* OpenGroupManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */; };
|
||||
B8CCF6352396005F0091D419 /* SpaceMono-Regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */; };
|
||||
B8CCF63723961D6D0091D419 /* NewDMVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63623961D6D0091D419 /* NewDMVC.swift */; };
|
||||
B8CCF63F23975CFB0091D419 /* JoinOpenGroupVC.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8CCF63E23975CFB0091D419 /* JoinOpenGroupVC.swift */; };
|
||||
|
@ -383,7 +373,6 @@
|
|||
C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDBBC255A581600E217F9 /* SSKKeychainStorage.swift */; };
|
||||
C32C5D9C256DD6DC003C73A2 /* OWSOutgoingReceiptManager.m in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB6F255A580F00E217F9 /* OWSOutgoingReceiptManager.m */; };
|
||||
C32C5DA5256DD6E5003C73A2 /* OWSOutgoingReceiptManager.h in Headers */ = {isa = PBXBuildFile; fileRef = C33FDABD255A580100E217F9 /* OWSOutgoingReceiptManager.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
C32C5DBE256DD743003C73A2 /* OpenGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */; };
|
||||
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */; };
|
||||
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDB3A255A580B00E217F9 /* Poller.swift */; };
|
||||
C32C5DC9256DD935003C73A2 /* ProxiedContentDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = C33FDAF2255A580500E217F9 /* ProxiedContentDownloader.swift */; };
|
||||
|
@ -678,15 +667,8 @@
|
|||
C3A71D1F25589AC30043A11F /* WebSocketResources.pb.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D1D25589AC30043A11F /* WebSocketResources.pb.swift */; };
|
||||
C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71F882558BA9F0043A11F /* Mnemonic.swift */; };
|
||||
C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A71D662558A0170043A11F /* DiffieHellman.swift */; };
|
||||
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721342558BDF90043A11F /* OpenGroupMessage.swift */; };
|
||||
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721352558BDF90043A11F /* OpenGroupAPI.swift */; };
|
||||
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */; };
|
||||
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721372558BDFA0043A11F /* OpenGroup.swift */; };
|
||||
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */; };
|
||||
C3A7219A2558C1660043A11F /* AnyPromise+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */; };
|
||||
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A722292558C1E40043A11F /* DotNetAPI.swift */; };
|
||||
C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */; };
|
||||
C3A7229C2558E4310043A11F /* OpenGroupMessage+Conversion.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */; };
|
||||
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */; };
|
||||
C3AABDDF2553ECF00042FF4C /* Array+Description.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3C2A5D12553860800C340D1 /* Array+Description.swift */; };
|
||||
C3AAFFE825AE975D0089E6DD /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */; };
|
||||
|
@ -964,9 +946,6 @@
|
|||
3427C64120F500DE00EEC730 /* OWSMessageTimerView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSMessageTimerView.h; sourceTree = "<group>"; };
|
||||
3427C64220F500DF00EEC730 /* OWSMessageTimerView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSMessageTimerView.m; sourceTree = "<group>"; };
|
||||
3430FE171F7751D4000EC51B /* GiphyAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GiphyAPI.swift; sourceTree = "<group>"; };
|
||||
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "fontawesome-webfont.ttf"; sourceTree = "<group>"; };
|
||||
34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = "dripicons-v2.ttf"; sourceTree = "<group>"; };
|
||||
34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */ = {isa = PBXFileReference; lastKnownFileType = file; path = ElegantIcons.ttf; sourceTree = "<group>"; };
|
||||
34330AA11E79686200DF2FB9 /* OWSProgressView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSProgressView.h; sourceTree = "<group>"; };
|
||||
34330AA21E79686200DF2FB9 /* OWSProgressView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSProgressView.m; sourceTree = "<group>"; };
|
||||
34386A53207D271C009F5D9C /* NeverClearView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NeverClearView.swift; sourceTree = "<group>"; };
|
||||
|
@ -1179,7 +1158,6 @@
|
|||
B835247825C38D880089A44F /* MessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageCell.swift; sourceTree = "<group>"; };
|
||||
B835249A25C3AB650089A44F /* VisibleMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VisibleMessageCell.swift; sourceTree = "<group>"; };
|
||||
B83524A425C3BA4B0089A44F /* InfoMessageCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InfoMessageCell.swift; sourceTree = "<group>"; };
|
||||
B837867F2586D296003CE78E /* KeyPairMigrationSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSheet.swift; sourceTree = "<group>"; };
|
||||
B83F2B85240C7B8F000A54AB /* NewConversationButtonSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewConversationButtonSet.swift; sourceTree = "<group>"; };
|
||||
B83F2B87240CB75A000A54AB /* UIImage+Scaling.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIImage+Scaling.swift"; sourceTree = "<group>"; };
|
||||
B84072952565E9F50037CB17 /* TSOutgoingMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TSOutgoingMessage+Conversion.swift"; sourceTree = "<group>"; };
|
||||
|
@ -1196,7 +1174,6 @@
|
|||
B8569AC225CB5D2900DBA3DB /* ConversationVC+Interaction.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConversationVC+Interaction.swift"; sourceTree = "<group>"; };
|
||||
B8569AD225CBA13D00DBA3DB /* MediaTextOverlayView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaTextOverlayView.swift; sourceTree = "<group>"; };
|
||||
B8569AE225CBB19A00DBA3DB /* DocumentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DocumentView.swift; sourceTree = "<group>"; };
|
||||
B85A68B02587141A008CC492 /* Storage+Resetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Storage+Resetting.swift"; sourceTree = "<group>"; };
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Modal.swift; sourceTree = "<group>"; };
|
||||
B86BD08523399CEF000F5AE3 /* SeedModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SeedModal.swift; sourceTree = "<group>"; };
|
||||
B87588582644CA9D000E60D0 /* nl */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = nl; path = nl.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
|
@ -1216,7 +1193,6 @@
|
|||
B893063E2383961A005EAA8E /* ScanQRCodeWrapperVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScanQRCodeWrapperVC.swift; sourceTree = "<group>"; };
|
||||
B894D0742339EDCF00B4D94D /* NukeDataModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NukeDataModal.swift; sourceTree = "<group>"; };
|
||||
B897621B25D201F7004F83B2 /* ScrollToBottomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollToBottomButton.swift; sourceTree = "<group>"; };
|
||||
B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyPairMigrationSuccessSheet.swift; sourceTree = "<group>"; };
|
||||
B8AE75A325A6C6A6001A84D2 /* Data+Trimming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Trimming.swift"; sourceTree = "<group>"; };
|
||||
B8AE760925ABFB00001A84D2 /* GeneralUtilities.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneralUtilities.h; sourceTree = "<group>"; };
|
||||
B8AE760A25ABFB5A001A84D2 /* GeneralUtilities.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = GeneralUtilities.m; sourceTree = "<group>"; };
|
||||
|
@ -1329,7 +1305,6 @@
|
|||
C33FDA87255A57FC00E217F9 /* TypingIndicators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TypingIndicators.swift; sourceTree = "<group>"; };
|
||||
C33FDA88255A57FD00E217F9 /* YapDatabaseTransaction+OWS.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "YapDatabaseTransaction+OWS.h"; sourceTree = "<group>"; };
|
||||
C33FDA8B255A57FD00E217F9 /* AppVersion.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppVersion.m; sourceTree = "<group>"; };
|
||||
C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupPoller.swift; sourceTree = "<group>"; };
|
||||
C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OWSFileSystem.m; sourceTree = "<group>"; };
|
||||
C33FDA90255A57FD00E217F9 /* TSYapDatabaseObject.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TSYapDatabaseObject.m; sourceTree = "<group>"; };
|
||||
C33FDA96255A57FE00E217F9 /* OWSDispatch.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OWSDispatch.h; sourceTree = "<group>"; };
|
||||
|
@ -1683,18 +1658,10 @@
|
|||
C3A71D4E25589FF30043A11F /* NSData+messagePadding.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSData+messagePadding.h"; 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>"; };
|
||||
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupMessage.swift; sourceTree = "<group>"; };
|
||||
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupAPI.swift; sourceTree = "<group>"; };
|
||||
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroupInfo.swift; sourceTree = "<group>"; };
|
||||
C3A721372558BDFA0043A11F /* OpenGroup.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = OpenGroup.swift; sourceTree = "<group>"; };
|
||||
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileServerAPI.swift; sourceTree = "<group>"; };
|
||||
C3A721992558C1660043A11F /* AnyPromise+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AnyPromise+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C3A722292558C1E40043A11F /* DotNetAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DotNetAPI.swift; sourceTree = "<group>"; };
|
||||
C3A7225D2558C38D0043A11F /* Promise+Retaining.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Promise+Retaining.swift"; sourceTree = "<group>"; };
|
||||
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "OpenGroupMessage+Conversion.swift"; sourceTree = "<group>"; };
|
||||
C3A76A8C25DB83F90074CB90 /* PermissionMissingModal.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PermissionMissingModal.swift; sourceTree = "<group>"; };
|
||||
C3AA6BB824CE8F1B002358B6 /* Migrating Translations from Android.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "Migrating Translations from Android.md"; path = "Meta/Translations/Migrating Translations from Android.md"; sourceTree = "<group>"; };
|
||||
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenGroupManager.swift; sourceTree = "<group>"; };
|
||||
C3AAFFDE25AE96FF0089E6DD /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = "<group>"; };
|
||||
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
C3ADC66026426688005F1414 /* ShareVC.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareVC.swift; sourceTree = "<group>"; };
|
||||
|
@ -1967,9 +1934,6 @@
|
|||
34330A581E7875FB00DF2FB9 /* Fonts */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
34330A5B1E787A9800DF2FB9 /* dripicons-v2.ttf */,
|
||||
34330A5D1E787BD800DF2FB9 /* ElegantIcons.ttf */,
|
||||
34330A591E7875FB00DF2FB9 /* fontawesome-webfont.ttf */,
|
||||
B8CCF6342396005F0091D419 /* SpaceMono-Regular.ttf */,
|
||||
C34C8F7323A7830A00D82669 /* SpaceMono-Bold.ttf */,
|
||||
);
|
||||
|
@ -2467,31 +2431,6 @@
|
|||
path = Meta;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3227FF4260AAD58006EA627 /* V2 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */,
|
||||
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */,
|
||||
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */,
|
||||
C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */,
|
||||
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */,
|
||||
);
|
||||
path = V2;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C3228005260AAD7E006EA627 /* V1 */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3A721372558BDFA0043A11F /* OpenGroup.swift */,
|
||||
C3A721352558BDF90043A11F /* OpenGroupAPI.swift */,
|
||||
C3A721362558BDFA0043A11F /* OpenGroupInfo.swift */,
|
||||
C3AAFFCB25AE92150089E6DD /* OpenGroupManager.swift */,
|
||||
C3A721342558BDF90043A11F /* OpenGroupMessage.swift */,
|
||||
C3A7229B2558E4310043A11F /* OpenGroupMessage+Conversion.swift */,
|
||||
);
|
||||
path = V1;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C328252E25CA54F70062D0A7 /* Context Menu */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2539,7 +2478,6 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
C33FDB34255A580B00E217F9 /* ClosedGroupPoller.swift */,
|
||||
C33FDA8C255A57FD00E217F9 /* OpenGroupPoller.swift */,
|
||||
C3DB66C2260ACCE6001EFC55 /* OpenGroupPollerV2.swift */,
|
||||
C33FDB3A255A580B00E217F9 /* Poller.swift */,
|
||||
);
|
||||
|
@ -2674,14 +2612,6 @@
|
|||
path = Mentions;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C32C5D49256DD522003C73A2 /* Database */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B85A68B02587141A008CC492 /* Storage+Resetting.swift */,
|
||||
);
|
||||
path = Database;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
C331FF1C2558F9D300070591 /* SessionUIKit */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -2868,8 +2798,6 @@
|
|||
C36096AF25AD1932008B62B2 /* Sheets & Modals */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B837867F2586D296003CE78E /* KeyPairMigrationSheet.swift */,
|
||||
B8A14D6F2589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift */,
|
||||
B86BD08323399ACF000F5AE3 /* Modal.swift */,
|
||||
C3DFFAC523E96F0D0058DAF8 /* Sheet.swift */,
|
||||
);
|
||||
|
@ -3150,8 +3078,11 @@
|
|||
C3A721332558BDDF0043A11F /* Open Groups */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3228005260AAD7E006EA627 /* V1 */,
|
||||
C3227FF4260AAD58006EA627 /* V2 */,
|
||||
C3DB6694260AC923001EFC55 /* OpenGroupV2.swift */,
|
||||
B88FA7B726045D100049422F /* OpenGroupAPIV2.swift */,
|
||||
C3DB66CB260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift */,
|
||||
C3DB66AB260ACA42001EFC55 /* OpenGroupManagerV2.swift */,
|
||||
C3227FF5260AAD66006EA627 /* OpenGroupMessageV2.swift */,
|
||||
);
|
||||
path = "Open Groups";
|
||||
sourceTree = "<group>";
|
||||
|
@ -3159,7 +3090,6 @@
|
|||
C3A7215C2558C0AC0043A11F /* File Server */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
C3A7218F2558C0CD0043A11F /* FileServerAPI.swift */,
|
||||
B87EF17026367CF800124B3C /* FileServerAPIV2.swift */,
|
||||
);
|
||||
path = "File Server";
|
||||
|
@ -3171,7 +3101,6 @@
|
|||
C33FDB01255A580700E217F9 /* AppReadiness.h */,
|
||||
C33FDB75255A581000E217F9 /* AppReadiness.m */,
|
||||
C38EF309255B6DBE007E1867 /* DeviceSleepManager.swift */,
|
||||
C3A722292558C1E40043A11F /* DotNetAPI.swift */,
|
||||
C37F53E8255BA9BB002AEA92 /* Environment.h */,
|
||||
C37F5402255BA9ED002AEA92 /* Environment.m */,
|
||||
C3BBE0C62554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift */,
|
||||
|
@ -3451,11 +3380,11 @@
|
|||
C3F0A58F255C8E3D007BE2A3 /* Meta */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
B81D260326158DF5004D1FE1 /* Certificates */,
|
||||
76EB03C218170B33006006FC /* AppDelegate.h */,
|
||||
76EB03C318170B33006006FC /* AppDelegate.m */,
|
||||
C3AAFFF125AE99710089E6DD /* AppDelegate.swift */,
|
||||
34D99CE3217509C1000AFB39 /* AppEnvironment.swift */,
|
||||
B81D260326158DF5004D1FE1 /* Certificates */,
|
||||
B8FF8E6025C10D8B004D1F22 /* Countries */,
|
||||
34330A581E7875FB00DF2FB9 /* Fonts */,
|
||||
B66DBF4919D5BBC8006EA940 /* Images.xcassets */,
|
||||
|
@ -3571,7 +3500,6 @@
|
|||
C36096BC25AD1C3E008B62B2 /* Backups */,
|
||||
C360969C25AD18BA008B62B2 /* Closed Groups */,
|
||||
B835246C25C38AA20089A44F /* Conversations */,
|
||||
C32C5D49256DD522003C73A2 /* Database */,
|
||||
C32B405424A961E1001117B5 /* Dependencies */,
|
||||
C36096A525AD18D7008B62B2 /* DMs */,
|
||||
C360968E25AD16E8008B62B2 /* Home */,
|
||||
|
@ -4067,12 +3995,9 @@
|
|||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
347850321FD7494A007B8332 /* ElegantIcons.ttf in Resources */,
|
||||
4535186E1FC635DD00210559 /* MainInterface.storyboard in Resources */,
|
||||
347850551FD749C0007B8332 /* Localizable.strings in Resources */,
|
||||
347850331FD7494A007B8332 /* fontawesome-webfont.ttf in Resources */,
|
||||
3478504C1FD7496D007B8332 /* Images.xcassets in Resources */,
|
||||
347850311FD7494A007B8332 /* dripicons-v2.ttf in Resources */,
|
||||
C3D90A1125773888002C9DF5 /* Colors.xcassets in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -4131,9 +4056,7 @@
|
|||
4C63CC00210A620B003AE45C /* SignalTSan.supp in Resources */,
|
||||
4C6F527C20FFE8400097DEEE /* SignalUBSan.supp in Resources */,
|
||||
34CF078A203E6B78005C4D61 /* end_call_tone_cept.caf in Resources */,
|
||||
34330A5A1E7875FB00DF2FB9 /* fontawesome-webfont.ttf in Resources */,
|
||||
A5509ECA1A69AB8B00ABA4BC /* Main.storyboard in Resources */,
|
||||
34330A5C1E787A9800DF2FB9 /* dripicons-v2.ttf in Resources */,
|
||||
C3CA3AA2255CDADA00F4C6D4 /* english.txt in Resources */,
|
||||
B6F509971AA53F760068F56A /* Localizable.strings in Resources */,
|
||||
C3A01E05261D24C400290BEB /* public-loki-foundation.der in Resources */,
|
||||
|
@ -4146,7 +4069,6 @@
|
|||
C3CA3AB4255CDAE600F4C6D4 /* japanese.txt in Resources */,
|
||||
B67EBF5D19194AC60084CCFD /* Settings.bundle in Resources */,
|
||||
34CF0787203E6B78005C4D61 /* busy_tone_ansi.caf in Resources */,
|
||||
34330A5E1E787BD800DF2FB9 /* ElegantIcons.ttf in Resources */,
|
||||
45A2F005204473A3002E978A /* NewMessage.aifc in Resources */,
|
||||
45B74A882044AAB600CD42F8 /* aurora.aifc in Resources */,
|
||||
45B74A742044AAB600CD42F8 /* aurora-quiet.aifc in Resources */,
|
||||
|
@ -4684,8 +4606,6 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
C3A7229C2558E4310043A11F /* OpenGroupMessage+Conversion.swift in Sources */,
|
||||
C32C5DBE256DD743003C73A2 /* OpenGroupPoller.swift in Sources */,
|
||||
B8856D08256F10F1001CE70E /* DeviceSleepManager.swift in Sources */,
|
||||
C3471F4C25553AB000297E91 /* MessageReceiver+Decryption.swift in Sources */,
|
||||
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
|
||||
|
@ -4710,10 +4630,8 @@
|
|||
C300A5FC2554B0A000555489 /* MessageReceiver.swift in Sources */,
|
||||
C32C5A76256DBBCF003C73A2 /* SignalAttachment.swift in Sources */,
|
||||
C32C5CA4256DD1DC003C73A2 /* TSAccountManager.m in Sources */,
|
||||
C3A7213B2558BDFA0043A11F /* OpenGroup.swift in Sources */,
|
||||
C352A3892557876500338F3E /* JobQueue.swift in Sources */,
|
||||
C3BBE0B52554F0E10050F1E3 /* ProofOfWork.swift in Sources */,
|
||||
C3A721902558C0CD0043A11F /* FileServerAPI.swift in Sources */,
|
||||
C32C59C1256DB41F003C73A2 /* TSGroupThread.m in Sources */,
|
||||
C3A3A08F256E1728004D228D /* FullTextSearchFinder.swift in Sources */,
|
||||
B8856D1A256F114D001CE70E /* ProximityMonitoringManager.swift in Sources */,
|
||||
|
@ -4734,12 +4652,10 @@
|
|||
C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */,
|
||||
C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */,
|
||||
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
|
||||
C3A721392558BDFA0043A11F /* OpenGroupAPI.swift in Sources */,
|
||||
C3A71D0B2558989C0043A11F /* MessageWrapper.swift in Sources */,
|
||||
B8F5F60325EDE16F003BF8D4 /* DataExtractionNotification.swift in Sources */,
|
||||
C32C5D24256DD4C0003C73A2 /* MentionsManager.swift in Sources */,
|
||||
C3A71D1E25589AC30043A11F /* WebSocketProto.swift in Sources */,
|
||||
C3A721382558BDFA0043A11F /* OpenGroupMessage.swift in Sources */,
|
||||
C3C2A7852553AAF300C340D1 /* SessionProtos.pb.swift in Sources */,
|
||||
B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */,
|
||||
C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */,
|
||||
|
@ -4759,7 +4675,6 @@
|
|||
C32C5EDC256DF501003C73A2 /* YapDatabaseConnection+OWS.m in Sources */,
|
||||
C3BBE0762554CDA60050F1E3 /* Configuration.swift in Sources */,
|
||||
B8B32033258B235D0020074B /* Storage+Contacts.swift in Sources */,
|
||||
B8CADAE925AFADF400AAFA15 /* OpenGroupManager.swift in Sources */,
|
||||
B8856D69256F141F001CE70E /* OWSWindowManager.m in Sources */,
|
||||
C3D9E3BE25676AD70040E4F3 /* TSAttachmentPointer.m in Sources */,
|
||||
C3ECBF7B257056B700EA7FCE /* Threading.swift in Sources */,
|
||||
|
@ -4773,7 +4688,6 @@
|
|||
C32C599E256DB02B003C73A2 /* TypingIndicators.swift in Sources */,
|
||||
C3DB66CC260AF1F3001EFC55 /* OpenGroupAPIV2+ObjC.swift in Sources */,
|
||||
C32C5BEF256DC8EE003C73A2 /* OWSDisappearingMessagesJob.m in Sources */,
|
||||
C3A7222A2558C1E40043A11F /* DotNetAPI.swift in Sources */,
|
||||
C34A977425A3E34A00852C71 /* ClosedGroupControlMessage.swift in Sources */,
|
||||
B88FA7B826045D100049422F /* OpenGroupAPIV2.swift in Sources */,
|
||||
C32C5E97256DE0CB003C73A2 /* OWSPrimaryStorage.m in Sources */,
|
||||
|
@ -4807,7 +4721,6 @@
|
|||
C32C5F11256DF79A003C73A2 /* SSKIncrementingIdFinder.swift in Sources */,
|
||||
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
|
||||
C32C5EEE256DF54E003C73A2 /* TSDatabaseView.m in Sources */,
|
||||
C3A7213A2558BDFA0043A11F /* OpenGroupInfo.swift in Sources */,
|
||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||
C3A3A13C256E1B27004D228D /* OWSMediaGalleryFinder.m in Sources */,
|
||||
C32C5AAE256DBE8F003C73A2 /* TSInteraction.m in Sources */,
|
||||
|
@ -4871,7 +4784,6 @@
|
|||
34B0796D1FCF46B100E248C2 /* MainAppContext.m in Sources */,
|
||||
34A8B3512190A40E00218A25 /* MediaAlbumView.swift in Sources */,
|
||||
4C4AEC4520EC343B0020E72B /* DismissableTextField.swift in Sources */,
|
||||
B85A68B12587141A008CC492 /* Storage+Resetting.swift in Sources */,
|
||||
3496955E219B605E00DCFE74 /* PhotoLibrary.swift in Sources */,
|
||||
C3A76A8D25DB83F90074CB90 /* PermissionMissingModal.swift in Sources */,
|
||||
340FC8A9204DAC8D007AEB0F /* NotificationSettingsOptionsViewController.m in Sources */,
|
||||
|
@ -4951,7 +4863,6 @@
|
|||
B82B4090239DD75000A248E7 /* RestoreVC.swift in Sources */,
|
||||
3488F9362191CC4000E524CC /* MediaView.swift in Sources */,
|
||||
B8569AC325CB5D2900DBA3DB /* ConversationVC+Interaction.swift in Sources */,
|
||||
B8A14D702589CE9000E70D57 /* KeyPairMigrationSuccessSheet.swift in Sources */,
|
||||
3496955C219B605E00DCFE74 /* ImagePickerController.swift in Sources */,
|
||||
C31D1DE32521718E005D4DA8 /* UserSelectionVC.swift in Sources */,
|
||||
34A6C28021E503E700B5B12E /* OWSImagePickerController.swift in Sources */,
|
||||
|
@ -4971,7 +4882,6 @@
|
|||
340FC8AC204DAC8D007AEB0F /* PrivacySettingsTableViewController.m in Sources */,
|
||||
B8569AE325CBB19A00DBA3DB /* DocumentView.swift in Sources */,
|
||||
B85357BF23A1AE0800AAF6CD /* SeedReminderView.swift in Sources */,
|
||||
B83786802586D296003CE78E /* KeyPairMigrationSheet.swift in Sources */,
|
||||
B821494F25D4E163009C0F2A /* BodyTextView.swift in Sources */,
|
||||
C35E8AAE2485E51D00ACB629 /* IP2Country.swift in Sources */,
|
||||
B835249B25C3AB650089A44F /* VisibleMessageCell.swift in Sources */,
|
||||
|
|
|
@ -536,11 +536,8 @@ extension ConversationVC : InputViewDelegate, MessageCellDelegate, ContextMenuAc
|
|||
let threadID = thread.uniqueId!
|
||||
alert.addAction(UIAlertAction(title: "OK", style: .default, handler: { _ in
|
||||
let publicKey = message.authorId
|
||||
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) {
|
||||
OpenGroupAPIV2.ban(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
|
||||
} else if let openGroup = Storage.shared.getOpenGroup(for: threadID) {
|
||||
OpenGroupAPI.ban(publicKey, from: openGroup.server).retainUntilComplete()
|
||||
}
|
||||
guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID) else { return }
|
||||
OpenGroupAPIV2.ban(publicKey, from: openGroupV2.room, on: openGroupV2.server).retainUntilComplete()
|
||||
}))
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
|
|
|
@ -988,7 +988,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (!message.isOpenGroupMessage) return;
|
||||
|
||||
// Get the open group
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (openGroup == nil && openGroupV2 == nil) return;
|
||||
|
||||
|
@ -997,8 +996,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
NSString *userPublicKey = [LKStorage.shared getUserPublicKey];
|
||||
if (openGroupV2 != nil) {
|
||||
if (![SNOpenGroupAPIV2 isUserModerator:userPublicKey forRoom:openGroupV2.room onServer:openGroupV2.server]) { return; }
|
||||
} else if (openGroup != nil) {
|
||||
if (![SNOpenGroupAPI isUserModerator:userPublicKey forChannel:openGroup.channel onServer:openGroup.server]) { return; }
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1009,11 +1006,6 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
// Roll back
|
||||
[self.interaction save];
|
||||
}) retainUntilComplete];
|
||||
} else if (openGroup != nil) {
|
||||
[[SNOpenGroupAPI deleteMessageWithID:message.openGroupServerMessageID forGroup:openGroup.channel onServer:openGroup.server isSentByUser:wasSentByUser].catch(^(NSError *error) {
|
||||
// Roll back
|
||||
[self.interaction save];
|
||||
}) retainUntilComplete];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1070,16 +1062,13 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (!message.isOpenGroupMessage) return true;
|
||||
|
||||
// Ensure we have the details needed to contact the server
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (openGroup == nil && openGroupV2 == nil) return true;
|
||||
if (openGroupV2 == nil) return true;
|
||||
|
||||
if (interationType == OWSInteractionType_IncomingMessage) {
|
||||
// Only allow deletion on incoming messages if the user has moderation permission
|
||||
if (openGroupV2 != nil) {
|
||||
return [SNOpenGroupAPIV2 isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server];
|
||||
} else {
|
||||
return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forChannel:openGroup.channel onServer:openGroup.server];
|
||||
}
|
||||
} else {
|
||||
return YES;
|
||||
|
@ -1096,15 +1085,12 @@ NSString *NSStringForOWSMessageCellType(OWSMessageCellType cellType)
|
|||
if (!message.isOpenGroupMessage) return false;
|
||||
|
||||
// Ensure we have the details needed to contact the server
|
||||
SNOpenGroup *openGroup = [LKStorage.shared getOpenGroupForThreadID:groupThread.uniqueId];
|
||||
SNOpenGroupV2 *openGroupV2 = [LKStorage.shared getV2OpenGroupForThreadID:groupThread.uniqueId];
|
||||
if (openGroup == nil && openGroupV2 == nil) return false;
|
||||
if (openGroupV2 == nil) return false;
|
||||
|
||||
// Check that we're a moderator
|
||||
if (openGroupV2 != nil) {
|
||||
return [SNOpenGroupAPIV2 isUserModerator:[SNGeneralUtilities getUserPublicKey] forRoom:openGroupV2.room onServer:openGroupV2.server];
|
||||
} else {
|
||||
return [SNOpenGroupAPI isUserModerator:[SNGeneralUtilities getUserPublicKey] forChannel:openGroup.channel onServer:openGroup.server];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -312,9 +312,6 @@ final class InputView : UIView, InputViewButtonDelegate, InputTextViewDelegate,
|
|||
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
mentionsView.openGroupServer = openGroupV2.server
|
||||
mentionsView.openGroupRoom = openGroupV2.room
|
||||
} else if let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!) {
|
||||
mentionsView.openGroupServer = openGroup.server
|
||||
mentionsView.openGroupChannel = openGroup.channel
|
||||
}
|
||||
mentionsView.candidates = candidates
|
||||
let mentionCellHeight = Values.smallProfilePictureSize + 2 * Values.smallSpacing
|
||||
|
|
|
@ -165,9 +165,6 @@ private extension MentionSelectionView {
|
|||
if let server = openGroupServer, let room = openGroupRoom {
|
||||
let isUserModerator = OpenGroupAPIV2.isUserModerator(mentionCandidate.publicKey, for: room, on: server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator
|
||||
} else if let server = openGroupServer, let channel = openGroupChannel {
|
||||
let isUserModerator = OpenGroupAPI.isUserModerator(mentionCandidate.publicKey, for: channel, on: server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator
|
||||
} else {
|
||||
moderatorIconImageView.isHidden = true
|
||||
}
|
||||
|
|
|
@ -222,9 +222,6 @@ final class VisibleMessageCell : MessageCell, LinkPreviewViewDelegate {
|
|||
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
let isUserModerator = OpenGroupAPIV2.isUserModerator(senderSessionID, for: openGroupV2.room, on: openGroupV2.server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden
|
||||
} else if let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!) {
|
||||
let isUserModerator = OpenGroupAPI.isUserModerator(senderSessionID, for: openGroup.channel, on: openGroup.server)
|
||||
moderatorIconImageView.isHidden = !isUserModerator || profilePictureView.isHidden
|
||||
} else {
|
||||
moderatorIconImageView.isHidden = true
|
||||
}
|
||||
|
|
|
@ -100,11 +100,8 @@ final class ConversationTitleView : UIView {
|
|||
switch thread.groupModel.groupType {
|
||||
case .closedGroup: userCount = UInt64(thread.groupModel.groupMemberIds.count)
|
||||
case .openGroup:
|
||||
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: self.thread.uniqueId!) {
|
||||
userCount = Storage.shared.getUserCount(forV2OpenGroupWithID: openGroupV2.id)
|
||||
} else if let openGroup = Storage.shared.getOpenGroup(for: self.thread.uniqueId!) {
|
||||
userCount = given(Storage.shared.getUserCount(forOpenGroupWithID: openGroup.id)) { UInt64($0) }
|
||||
}
|
||||
guard let openGroupV2 = Storage.shared.getV2OpenGroup(for: self.thread.uniqueId!) else { return nil }
|
||||
userCount = Storage.shared.getUserCount(forV2OpenGroupWithID: openGroupV2.id)
|
||||
default: break
|
||||
}
|
||||
if let userCount = userCount {
|
||||
|
|
|
@ -63,7 +63,7 @@ final class JoinOpenGroupModal : Modal {
|
|||
|
||||
// MARK: Interaction
|
||||
@objc private func joinOpenGroup() {
|
||||
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url), Features.useV2OpenGroups else {
|
||||
guard let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: url) else {
|
||||
let alert = UIAlertController(title: "Couldn't Join", message: nil, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: nil))
|
||||
return presentingViewController!.present(alert, animated: true, completion: nil)
|
||||
|
|
|
@ -1,42 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
extension Storage {
|
||||
|
||||
static func prepareForV2KeyPairMigration() {
|
||||
let userDefaults = UserDefaults.standard
|
||||
let isUsingAPNs = userDefaults[.isUsingFullAPNs]
|
||||
if isUsingAPNs, let hexEncodedToken = userDefaults[.deviceToken] {
|
||||
let token = Data(hex: hexEncodedToken)
|
||||
PushNotificationAPI.unregister(token).retainUntilComplete() // TODO: Wait for this to complete?
|
||||
}
|
||||
let name = Storage.shared.getUser()!.name!
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.stopPoller()
|
||||
appDelegate.stopClosedGroupPoller()
|
||||
appDelegate.stopOpenGroupPollers()
|
||||
OWSStorage.resetAllStorage()
|
||||
OWSUserProfile.resetProfileStorage()
|
||||
Environment.shared.preferences.clear()
|
||||
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
|
||||
userDefaults[.isUsingFullAPNs] = isUsingAPNs
|
||||
userDefaults[.displayName] = name
|
||||
userDefaults[.isMigratingToV2KeyPair] = true
|
||||
exit(0)
|
||||
}
|
||||
|
||||
static func finishV2KeyPairMigration(navigationController: UINavigationController) {
|
||||
let seed = Data.getSecureRandomData(ofSize: 16)!
|
||||
let (ed25519KeyPair, x25519KeyPair) = KeyPairUtilities.generate(from: seed)
|
||||
KeyPairUtilities.store(seed: seed, ed25519KeyPair: ed25519KeyPair, x25519KeyPair: x25519KeyPair)
|
||||
TSAccountManager.sharedInstance().phoneNumberAwaitingVerification = x25519KeyPair.hexEncodedPublicKey
|
||||
UserDefaults.standard[.hasViewedSeed] = false
|
||||
let displayName = UserDefaults.standard[.displayName]! // Checked earlier
|
||||
OWSProfileManager.shared().updateLocalProfileName(displayName, avatarImage: nil, success: { }, failure: { _ in }, requiresSync: false)
|
||||
TSAccountManager.sharedInstance().didRegister()
|
||||
let homeVC = HomeVC()
|
||||
navigationController.setViewControllers([ homeVC ], animated: true)
|
||||
let syncTokensJob = SyncPushTokensJob(accountManager: AppEnvironment.shared.accountManager, preferences: Environment.shared.preferences)
|
||||
syncTokensJob.uploadOnlyIfStale = false
|
||||
let _: Promise<Void> = syncTokensJob.run()
|
||||
}
|
||||
}
|
|
@ -156,9 +156,7 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
let _ = IP2Country.shared.populateCacheIfNeeded()
|
||||
}
|
||||
// Get default open group rooms if needed
|
||||
if Features.useV2OpenGroups {
|
||||
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
|
||||
}
|
||||
OpenGroupAPIV2.getDefaultRoomsIfNeeded()
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
|
@ -380,13 +378,10 @@ final class HomeVC : BaseVC, UITableViewDataSource, UITableViewDelegate, NewConv
|
|||
|
||||
private func delete(_ thread: TSThread) {
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)
|
||||
let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)
|
||||
Storage.write { transaction in
|
||||
Storage.shared.cancelPendingMessageSendJobs(for: thread.uniqueId!, using: transaction)
|
||||
if let openGroupV2 = openGroupV2 {
|
||||
OpenGroupManagerV2.shared.delete(openGroupV2, associatedWith: thread, using: transaction)
|
||||
} else if let openGroup = openGroup {
|
||||
OpenGroupManager.shared.delete(openGroup, associatedWith: thread, using: transaction)
|
||||
} else if let thread = thread as? TSGroupThread, thread.isClosedGroup == true {
|
||||
let groupID = thread.groupModel.groupId
|
||||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
|
|
|
@ -151,10 +151,9 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
[LKAppModeManager configureWithDelegate:self];
|
||||
|
||||
// OWSLinkPreview and OpenGroup are now in SessionMessagingKit, so to still be able to deserialize them we
|
||||
// OWSLinkPreview is now in SessionMessagingKit, so to still be able to deserialize them we
|
||||
// need to tell NSKeyedUnarchiver about the changes.
|
||||
[NSKeyedUnarchiver setClass:OWSLinkPreview.class forClassName:@"SessionServiceKit.OWSLinkPreview"];
|
||||
[NSKeyedUnarchiver setClass:SNOpenGroup.class forClassName:@"LKPublicChat"];
|
||||
|
||||
BOOL isLoggingEnabled;
|
||||
#ifdef DEBUG
|
||||
|
@ -406,7 +405,7 @@ static NSTimeInterval launchStartedAt;
|
|||
} requiresSync:YES];
|
||||
}
|
||||
|
||||
if (CurrentAppContext().isMainApp && SNFeatures.useV2OpenGroups) {
|
||||
if (CurrentAppContext().isMainApp) {
|
||||
[SNOpenGroupAPIV2 getDefaultRoomsIfNeeded];
|
||||
}
|
||||
|
||||
|
@ -728,12 +727,10 @@ static NSTimeInterval launchStartedAt;
|
|||
|
||||
- (void)startOpenGroupPollersIfNeeded
|
||||
{
|
||||
[SNOpenGroupManager.shared startPolling];
|
||||
[SNOpenGroupManagerV2.shared startPolling];
|
||||
}
|
||||
|
||||
- (void)stopOpenGroupPollers {
|
||||
[SNOpenGroupManager.shared stopPolling];
|
||||
[SNOpenGroupManagerV2.shared stopPolling];
|
||||
}
|
||||
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
@ -82,13 +82,6 @@ final class LandingVC : BaseVC {
|
|||
view.addSubview(mainStackView)
|
||||
mainStackView.pin(to: view)
|
||||
topSpacer.heightAnchor.constraint(equalTo: bottomSpacer.heightAnchor, multiplier: 1).isActive = true
|
||||
// Auto-migrate if needed
|
||||
let userDefaults = UserDefaults.standard
|
||||
if userDefaults[.isMigratingToV2KeyPair] {
|
||||
if userDefaults[.displayName] != nil {
|
||||
Storage.finishV2KeyPairMigration(navigationController: navigationController!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Interaction
|
||||
|
|
|
@ -125,19 +125,10 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
}
|
||||
|
||||
fileprivate func joinOpenGroup(with string: String) {
|
||||
// A V1 open group URL will look like: https:// + <host>
|
||||
// A V2 open group URL will look like: <optional scheme> + <host> + <optional port> + <room> + <public key>
|
||||
// The host doesn't parse if no explicit scheme is provided
|
||||
if let url = URL(string: string), let host = url.host ?? given(string.split(separator: "/").first, { String($0) }) {
|
||||
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: string), Features.useV2OpenGroups {
|
||||
joinV2OpenGroup(room: room, server: server, publicKey: publicKey)
|
||||
} else {
|
||||
// Inputs that should work:
|
||||
// loki.opensession.id
|
||||
// https://loki.opensession.id
|
||||
// http://loki.opensession.id (still goes to HTTPS)
|
||||
joinV1OpenGroup("https://" + host)
|
||||
}
|
||||
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: string) {
|
||||
joinV2OpenGroup(room: room, server: server, publicKey: publicKey)
|
||||
} else {
|
||||
let title = NSLocalizedString("invalid_url", comment: "")
|
||||
let message = "Please check the URL you entered and try again."
|
||||
|
@ -145,32 +136,6 @@ final class JoinOpenGroupVC : BaseVC, UIPageViewControllerDataSource, UIPageView
|
|||
}
|
||||
}
|
||||
|
||||
private func joinV1OpenGroup(_ string: String) {
|
||||
guard !isJoining else { return }
|
||||
isJoining = true
|
||||
ModalActivityIndicatorViewController.present(fromViewController: navigationController!, canCancel: false) { [weak self] _ in
|
||||
Storage.shared.write { transaction in
|
||||
OpenGroupManager.shared.add(with: string, using: transaction)
|
||||
.done(on: DispatchQueue.main) { [weak self] _ in
|
||||
self?.presentingViewController!.dismiss(animated: true, completion: nil)
|
||||
let appDelegate = UIApplication.shared.delegate as! AppDelegate
|
||||
appDelegate.forceSyncConfigurationNowIfNeeded().retainUntilComplete() // FIXME: It's probably cleaner to do this inside addOpenGroup(...)
|
||||
}
|
||||
.catch(on: DispatchQueue.main) { [weak self] error in
|
||||
self?.dismiss(animated: true, completion: nil) // Dismiss the loader
|
||||
var title = "Couldn't Join"
|
||||
var message = ""
|
||||
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
|
||||
title = "Unauthorized"
|
||||
message = "Please ask the open group operator to add you to the group."
|
||||
}
|
||||
self?.isJoining = false
|
||||
self?.showError(title: title, message: message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func joinV2OpenGroup(room: String, server: String, publicKey: String) {
|
||||
guard !isJoining else { return }
|
||||
isJoining = true
|
||||
|
@ -242,13 +207,8 @@ private final class EnterURLVC : UIViewController, UIGestureRecognizerDelegate,
|
|||
nextButtonContainer.pin(.trailing, to: .trailing, of: nextButton, withInset: 80)
|
||||
nextButtonContainer.pin(.bottom, to: .bottom, of: nextButton)
|
||||
// Stack view
|
||||
let stackView: UIStackView
|
||||
if Features.useV2OpenGroups {
|
||||
stackView = UIStackView(arrangedSubviews: [ urlTextView, UIView.spacer(withHeight: Values.mediumSpacing), suggestionGridTitleLabel,
|
||||
UIView.spacer(withHeight: Values.mediumSpacing), suggestionGrid, UIView.vStretchingSpacer(), nextButtonContainer ])
|
||||
} else {
|
||||
stackView = UIStackView(arrangedSubviews: [ urlTextView, UIView.vStretchingSpacer(), nextButtonContainer ])
|
||||
}
|
||||
let stackView = UIStackView(arrangedSubviews: [ urlTextView, UIView.spacer(withHeight: Values.mediumSpacing), suggestionGridTitleLabel,
|
||||
UIView.spacer(withHeight: Values.mediumSpacing), suggestionGrid, UIView.vStretchingSpacer(), nextButtonContainer ])
|
||||
stackView.axis = .vertical
|
||||
stackView.alignment = .fill
|
||||
stackView.layoutMargins = UIEdgeInsets(uniform: Values.largeSpacing)
|
||||
|
|
|
@ -204,26 +204,17 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
button.set(.height, to: SettingsVC.buttonHeight)
|
||||
return button
|
||||
}
|
||||
var result = [
|
||||
return [
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_privacy_button_title", comment: ""), color: Colors.text, action: #selector(showPrivacySettings)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_notifications_button_title", comment: ""), color: Colors.text, action: #selector(showNotificationSettings)),
|
||||
getSeparator()
|
||||
]
|
||||
if !KeyPairUtilities.hasV2KeyPair() {
|
||||
result += [
|
||||
getSettingButton(withTitle: "Upgrade Session ID", color: Colors.text, action: #selector(upgradeSessionID)),
|
||||
getSeparator()
|
||||
]
|
||||
}
|
||||
result += [
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_recovery_phrase_button_title", comment: ""), color: Colors.text, action: #selector(showSeed)),
|
||||
getSeparator(),
|
||||
getSettingButton(withTitle: NSLocalizedString("vc_settings_clear_all_data_button_title", comment: ""), color: Colors.destructive, action: #selector(clearAllData)),
|
||||
getSeparator()
|
||||
]
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: General
|
||||
|
@ -346,7 +337,7 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
DispatchQueue.main.async {
|
||||
modalActivityIndicator.dismiss {
|
||||
var isMaxFileSizeExceeded = false
|
||||
if let error = error as? DotNetAPI.Error {
|
||||
if let error = error as? FileServerAPIV2.Error {
|
||||
isMaxFileSizeExceeded = (error == .maxFileSizeExceeded)
|
||||
}
|
||||
let title = isMaxFileSizeExceeded ? "Maximum File Size Exceeded" : "Couldn't Update Profile"
|
||||
|
@ -458,16 +449,6 @@ final class SettingsVC : BaseVC, AvatarViewHelperDelegate {
|
|||
UIApplication.shared.open(url)
|
||||
}
|
||||
|
||||
@objc private func upgradeSessionID() {
|
||||
let message = "You’re upgrading to a new Session ID. This will give you improved privacy and security, but it will clear ALL app data. Contacts and conversations will be lost. Proceed?"
|
||||
let alert = UIAlertController(title: "Upgrade Session ID?", message: message, preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "Yes", style: .destructive) { _ in
|
||||
Storage.prepareForV2KeyPairMigration()
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "Cancel", style: .default, handler: nil))
|
||||
present(alert, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc private func showSeed() {
|
||||
let seedModal = SeedModal()
|
||||
seedModal.modalPresentationStyle = .overFullScreen
|
||||
|
|
|
@ -1,75 +0,0 @@
|
|||
|
||||
final class KeyPairMigrationSheet : Sheet {
|
||||
|
||||
override class var isDismissable: Bool { false }
|
||||
|
||||
override func populateContentView() {
|
||||
// Image view
|
||||
let imageView = UIImageView(image: #imageLiteral(resourceName: "Shield").withTint(Colors.text))
|
||||
imageView.set(.width, to: 64)
|
||||
imageView.set(.height, to: 64)
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
// Title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
|
||||
titleLabel.text = "Session IDs Just Got Better"
|
||||
titleLabel.textAlignment = .center
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
topStackView.alignment = .center
|
||||
// Explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.text = """
|
||||
We’ve upgraded Session IDs to make them even more private and secure. To ensure your continued privacy you're now required to upgrade.
|
||||
|
||||
Your existing contacts and conversations will be lost, but you’ll be able to use Session knowing you have the best privacy and security possible.
|
||||
"""
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Upgrade now button
|
||||
let upgradeNowButton = Button(style: .prominentOutline, size: .large)
|
||||
upgradeNowButton.set(.width, to: 240)
|
||||
upgradeNowButton.setTitle("Upgrade Now", for: UIControl.State.normal)
|
||||
upgradeNowButton.addTarget(self, action: #selector(upgradeNow), for: UIControl.Event.touchUpInside)
|
||||
// Upgrade now button
|
||||
let upgradeLaterButton = Button(style: .prominentOutline, size: .large)
|
||||
upgradeLaterButton.set(.width, to: 240)
|
||||
upgradeLaterButton.setTitle("Upgrade Later", for: UIControl.State.normal)
|
||||
upgradeLaterButton.addTarget(self, action: #selector(upgradeLater), for: UIControl.Event.touchUpInside)
|
||||
// Button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ upgradeNowButton, upgradeLaterButton ])
|
||||
buttonStackView.axis = .vertical
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.alignment = .center
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ topStackView, explanationLabel, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.veryLargeSpacing
|
||||
stackView.alignment = .center
|
||||
// Constraints
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.veryLargeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.veryLargeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.veryLargeSpacing + overshoot)
|
||||
}
|
||||
|
||||
@objc private func upgradeNow() {
|
||||
Storage.prepareForV2KeyPairMigration()
|
||||
}
|
||||
|
||||
@objc private func upgradeLater() {
|
||||
let alert = UIAlertController(title: "Warning", message: "You won't be able to send or receive messages until you upgrade.", preferredStyle: .alert)
|
||||
alert.addAction(UIAlertAction(title: "OK", accessibilityIdentifier: nil, style: .default, handler: nil))
|
||||
presentingViewController?.dismiss(animated: true, completion: nil) // Dismiss self
|
||||
presentingViewController?.present(alert, animated: true, completion: nil)
|
||||
}
|
||||
}
|
|
@ -1,94 +0,0 @@
|
|||
|
||||
final class KeyPairMigrationSuccessSheet : Sheet {
|
||||
|
||||
private lazy var sessionIDLabel: UILabel = {
|
||||
let result = UILabel()
|
||||
result.textColor = Colors.text
|
||||
result.font = Fonts.spaceMono(ofSize: isIPhone5OrSmaller ? Values.mediumFontSize : 20)
|
||||
result.numberOfLines = 0
|
||||
result.lineBreakMode = .byCharWrapping
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var copyButton: Button = {
|
||||
let result = Button(style: .prominentOutline, size: .large)
|
||||
result.set(.width, to: 240)
|
||||
result.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
|
||||
result.addTarget(self, action: #selector(copySessionID), for: UIControl.Event.touchUpInside)
|
||||
return result
|
||||
}()
|
||||
|
||||
override func populateContentView() {
|
||||
// Image view
|
||||
let imageView = UIImageView(image: #imageLiteral(resourceName: "Shield").withTint(Colors.text))
|
||||
imageView.set(.width, to: 64)
|
||||
imageView.set(.height, to: 64)
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
// Title label
|
||||
let titleLabel = UILabel()
|
||||
titleLabel.textColor = Colors.text
|
||||
titleLabel.font = .boldSystemFont(ofSize: isIPhone5OrSmaller ? Values.largeFontSize : Values.veryLargeFontSize)
|
||||
titleLabel.text = "Upgrade Successful!"
|
||||
titleLabel.numberOfLines = 0
|
||||
titleLabel.lineBreakMode = .byWordWrapping
|
||||
// Top stack view
|
||||
let topStackView = UIStackView(arrangedSubviews: [ imageView, titleLabel ])
|
||||
topStackView.axis = .vertical
|
||||
topStackView.spacing = Values.largeSpacing
|
||||
topStackView.alignment = .center
|
||||
// Explanation label
|
||||
let explanationLabel = UILabel()
|
||||
explanationLabel.textColor = Colors.text
|
||||
explanationLabel.font = .systemFont(ofSize: Values.smallFontSize)
|
||||
explanationLabel.textAlignment = .center
|
||||
explanationLabel.text = "Your new and improved Session ID is:"
|
||||
explanationLabel.numberOfLines = 0
|
||||
explanationLabel.lineBreakMode = .byWordWrapping
|
||||
// Session ID label
|
||||
sessionIDLabel.text = getUserHexEncodedPublicKey()
|
||||
// Session ID container
|
||||
let sessionIDContainer = UIView()
|
||||
sessionIDContainer.addSubview(sessionIDLabel)
|
||||
sessionIDLabel.pin(to: sessionIDContainer, withInset: Values.mediumSpacing)
|
||||
sessionIDContainer.layer.cornerRadius = TextField.cornerRadius
|
||||
sessionIDContainer.layer.borderWidth = 1
|
||||
sessionIDContainer.layer.borderColor = Colors.text.cgColor
|
||||
// OK button
|
||||
let okButton = Button(style: .prominentOutline, size: .large)
|
||||
okButton.set(.width, to: 240)
|
||||
okButton.setTitle("OK", for: UIControl.State.normal)
|
||||
okButton.addTarget(self, action: #selector(close), for: UIControl.Event.touchUpInside)
|
||||
// Button stack view
|
||||
let buttonStackView = UIStackView(arrangedSubviews: [ copyButton, okButton ])
|
||||
buttonStackView.axis = .vertical
|
||||
buttonStackView.spacing = Values.mediumSpacing
|
||||
buttonStackView.alignment = .center
|
||||
// Main stack view
|
||||
let stackView = UIStackView(arrangedSubviews: [ topStackView, explanationLabel, sessionIDContainer, buttonStackView ])
|
||||
stackView.axis = .vertical
|
||||
stackView.spacing = Values.veryLargeSpacing
|
||||
stackView.alignment = .center
|
||||
// Constraints
|
||||
contentView.addSubview(stackView)
|
||||
stackView.pin(.leading, to: .leading, of: contentView, withInset: Values.veryLargeSpacing)
|
||||
stackView.pin(.top, to: .top, of: contentView, withInset: Values.largeSpacing)
|
||||
contentView.pin(.trailing, to: .trailing, of: stackView, withInset: Values.veryLargeSpacing)
|
||||
contentView.pin(.bottom, to: .bottom, of: stackView, withInset: Values.veryLargeSpacing + overshoot)
|
||||
}
|
||||
|
||||
@objc private func copySessionID() {
|
||||
UIPasteboard.general.string = getUserHexEncodedPublicKey()
|
||||
copyButton.isUserInteractionEnabled = false
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle("Copied", for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
Timer.scheduledTimer(timeInterval: 4, target: self, selector: #selector(enableCopyButton), userInfo: nil, repeats: false)
|
||||
}
|
||||
|
||||
@objc private func enableCopyButton() {
|
||||
copyButton.isUserInteractionEnabled = true
|
||||
UIView.transition(with: copyButton, duration: 0.25, options: .transitionCrossDissolve, animations: {
|
||||
self.copyButton.setTitle(NSLocalizedString("copy", comment: ""), for: UIControl.State.normal)
|
||||
}, completion: nil)
|
||||
}
|
||||
}
|
|
@ -9,7 +9,6 @@ public final class MentionUtilities : NSObject {
|
|||
}
|
||||
|
||||
@objc public static func highlightMentions(in string: String, isOutgoingMessage: Bool, threadID: String, attributes: [NSAttributedString.Key:Any]) -> NSAttributedString {
|
||||
let openGroup = Storage.shared.getOpenGroup(for: threadID)
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
|
||||
OWSPrimaryStorage.shared().dbReadConnection.read { transaction in
|
||||
MentionsManager.populateUserPublicKeyCacheIfNeeded(for: threadID, in: transaction)
|
||||
|
@ -23,7 +22,7 @@ public final class MentionUtilities : NSObject {
|
|||
let publicKey = String((string as NSString).substring(with: match.range).dropFirst()) // Drop the @
|
||||
let matchEnd: Int
|
||||
if knownPublicKeys.contains(publicKey) {
|
||||
let context: Contact.Context = (openGroupV2 != nil || openGroup != nil) ? .openGroup : .regular
|
||||
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
||||
let displayName = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
||||
if let displayName = displayName {
|
||||
string = (string as NSString).replacingCharacters(in: match.range, with: "@\(displayName)")
|
||||
|
|
|
@ -7,7 +7,7 @@ extension Storage {
|
|||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
var threadOrNil: TSThread?
|
||||
if let openGroupID = openGroupID {
|
||||
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID) ?? Storage.shared.getThreadID(for: openGroupID),
|
||||
if let threadID = Storage.shared.v2GetThreadID(for: openGroupID),
|
||||
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) {
|
||||
threadOrNil = thread
|
||||
}
|
||||
|
|
|
@ -49,26 +49,6 @@ extension Storage {
|
|||
|
||||
|
||||
|
||||
// MARK: - Quotes
|
||||
|
||||
@objc(getServerIDForQuoteWithID:quoteeHexEncodedPublicKey:threadID:transaction:)
|
||||
public func getServerID(quoteID: UInt64, quoteeHexEncodedPublicKey: String, threadID: String, transaction: YapDatabaseReadTransaction) -> UInt64 {
|
||||
guard let message = TSInteraction.interactions(withTimestamp: quoteID, filter: { interaction in
|
||||
let senderPublicKey: String
|
||||
if let message = interaction as? TSIncomingMessage {
|
||||
senderPublicKey = message.authorId
|
||||
} else if interaction is TSOutgoingMessage {
|
||||
senderPublicKey = getUserHexEncodedPublicKey()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return (senderPublicKey == quoteeHexEncodedPublicKey) && (interaction.uniqueThreadId == threadID)
|
||||
}, with: transaction).first as! TSMessage? else { return 0 }
|
||||
return message.openGroupServerMessageID
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Authorization
|
||||
|
||||
private static let authTokenCollection = "SNAuthTokenCollection"
|
||||
|
@ -192,10 +172,6 @@ extension Storage {
|
|||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: openGroupID, inCollection: Storage.openGroupUserCountCollection)
|
||||
}
|
||||
|
||||
public func setLastProfilePictureUploadDate(_ date: Date) {
|
||||
UserDefaults.standard[.lastProfilePictureUpload] = date
|
||||
}
|
||||
|
||||
public func getOpenGroupImage(for room: String, on server: String) -> Data? {
|
||||
var result: Data?
|
||||
Storage.read { transaction in
|
||||
|
@ -207,148 +183,4 @@ extension Storage {
|
|||
public func setOpenGroupImage(to data: Data, for room: String, on server: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(data, forKey: "\(server).\(room)", inCollection: Storage.openGroupImageCollection)
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: - Deprecated
|
||||
|
||||
private static let oldOpenGroupUserCountCollection = "LokiPublicChatUserCountCollection"
|
||||
|
||||
public func getUserCount(forOpenGroupWithID openGroupID: String) -> Int? {
|
||||
var result: Int?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: openGroupID, inCollection: Storage.oldOpenGroupUserCountCollection) as? Int
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any) {
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
transaction.setObject(newValue, forKey: openGroupID, inCollection: Storage.oldOpenGroupUserCountCollection)
|
||||
transaction.addCompletionQueue(.main) {
|
||||
NotificationCenter.default.post(name: .groupThreadUpdated, object: nil)
|
||||
}
|
||||
}
|
||||
|
||||
private static let oldOpenGroupCollection = "LokiPublicChatCollection"
|
||||
|
||||
@objc public func getAllUserOpenGroups() -> [String:OpenGroup] {
|
||||
var result = [String:OpenGroup]()
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateKeysAndObjects(inCollection: Storage.oldOpenGroupCollection) { threadID, object, _ in
|
||||
guard let openGroup = object as? OpenGroup else { return }
|
||||
result[threadID] = openGroup
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(getOpenGroupForThreadID:)
|
||||
public func getOpenGroup(for threadID: String) -> OpenGroup? {
|
||||
var result: OpenGroup?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: threadID, inCollection: Storage.oldOpenGroupCollection) as? OpenGroup
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func getThreadID(for openGroupID: String) -> String? {
|
||||
var result: String?
|
||||
Storage.read { transaction in
|
||||
transaction.enumerateKeysAndObjects(inCollection: Storage.oldOpenGroupCollection, using: { threadID, object, stop in
|
||||
guard let openGroup = object as? OpenGroup, "\(openGroup.server).\(openGroup.channel)" == openGroupID else { return }
|
||||
result = threadID
|
||||
stop.pointee = true
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@objc(setOpenGroup:forThreadWithID:using:)
|
||||
public func setOpenGroup(_ openGroup: OpenGroup, for threadID: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(openGroup, forKey: threadID, inCollection: Storage.oldOpenGroupCollection)
|
||||
}
|
||||
|
||||
@objc(removeOpenGroupForThreadID:using:)
|
||||
public func removeOpenGroup(for threadID: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: threadID, inCollection: Storage.oldOpenGroupCollection)
|
||||
}
|
||||
|
||||
private static func getAuthTokenCollection(for server: String) -> String {
|
||||
return (server == FileServerAPI.server) ? "LokiStorageAuthTokenCollection" : "LokiGroupChatAuthTokenCollection"
|
||||
}
|
||||
|
||||
public func getAuthToken(for server: String) -> String? {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
var result: String? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: server, inCollection: collection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setAuthToken(for server: String, to newValue: String, using transaction: Any) {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: server, inCollection: collection)
|
||||
}
|
||||
|
||||
public func removeAuthToken(for server: String, using transaction: Any) {
|
||||
let collection = Storage.getAuthTokenCollection(for: server)
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: server, inCollection: collection)
|
||||
}
|
||||
|
||||
public static let oldLastMessageServerIDCollection = "LokiGroupChatLastMessageServerIDCollection"
|
||||
|
||||
public func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64? {
|
||||
var result: UInt64? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
public func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.oldLastMessageServerIDCollection)
|
||||
}
|
||||
|
||||
public static let oldLastDeletionServerIDCollection = "LokiGroupChatLastDeletionServerIDCollection"
|
||||
|
||||
public func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64? {
|
||||
var result: UInt64? = nil
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection) as? UInt64
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(newValue, forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
public func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: "\(server).\(group)", inCollection: Storage.oldLastDeletionServerIDCollection)
|
||||
}
|
||||
|
||||
public func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any) {
|
||||
let collection = openGroupID
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(displayName, forKey: publicKey, inCollection: collection)
|
||||
}
|
||||
|
||||
private static let openGroupProfilePictureURLCollection = "LokiPublicChatAvatarURLCollection"
|
||||
|
||||
public func getProfilePictureURL(forOpenGroupWithID openGroupID: String) -> String? {
|
||||
var result: String?
|
||||
Storage.read { transaction in
|
||||
result = transaction.object(forKey: openGroupID, inCollection: Storage.openGroupProfilePictureURLCollection) as? String
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
public func setProfilePictureURL(to profilePictureURL: String?, forOpenGroupWithID openGroupID: String, using transaction: Any) {
|
||||
(transaction as! YapDatabaseReadWriteTransaction).setObject(profilePictureURL, forKey: openGroupID, inCollection: Storage.openGroupProfilePictureURLCollection)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,76 +0,0 @@
|
|||
import AFNetworking
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNFileServerAPI)
|
||||
public final class FileServerAPI : DotNetAPI {
|
||||
|
||||
// MARK: Settings
|
||||
private static let attachmentType = "net.app.core.oembed"
|
||||
private static let deviceLinkType = "network.loki.messenger.devicemapping"
|
||||
|
||||
internal static let publicKey = "62509D59BDEEC404DD0D489C1E15BA8F94FD3D619B01C1BF48A9922BFCB7311C"
|
||||
|
||||
public static let maxFileSize = 10_000_000 // 10 MB
|
||||
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
public static let fileSizeORMultiplier: Double = 2
|
||||
|
||||
@objc public static let server = "https://file.getsession.org"
|
||||
@objc public static let fileStorageBucketURL = "https://file-static.lokinet.org"
|
||||
|
||||
// MARK: Profile Pictures
|
||||
@objc(uploadProfilePicture:)
|
||||
public static func objc_uploadProfilePicture(_ profilePicture: Data) -> AnyPromise {
|
||||
return AnyPromise.from(uploadProfilePicture(profilePicture))
|
||||
}
|
||||
|
||||
public static func uploadProfilePicture(_ profilePicture: Data) -> Promise<String> {
|
||||
guard Double(profilePicture.count) < Double(maxFileSize) / fileSizeORMultiplier else { return Promise(error: Error.maxFileSizeExceeded) }
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
formData.appendPart(withFileData: profilePicture, name: "content", fileName: UUID().uuidString, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
// Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
request.addValue("Bearer loki", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
SNLog("Couldn't upload profile picture due to error: \(error).")
|
||||
return Promise(error: error)
|
||||
}
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let data = json["data"] as? JSON, let downloadURL = data["url"] as? String else {
|
||||
SNLog("Couldn't parse profile picture from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
SNMessagingKitConfiguration.shared.storage.setLastProfilePictureUploadDate(Date())
|
||||
return downloadURL
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Open Group Server Public Key
|
||||
public static func getPublicKey(for openGroupServer: String) -> Promise<String> {
|
||||
guard let host = URL(string: openGroupServer)?.host,
|
||||
let url = URL(string: "\(server)/loki/v1/getOpenGroupKey/\(host)") else { return Promise(error: DotNetAPI.Error.invalidURL) }
|
||||
let request = TSRequest(url: url)
|
||||
let token = "loki" // Tokenless request; use a dummy token
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let bodyAsString = json["data"] as? String, let bodyAsData = bodyAsString.data(using: .utf8),
|
||||
let body = try JSONSerialization.jsonObject(with: bodyAsData, options: [ .fragmentsAllowed ]) as? JSON else { throw HTTP.Error.invalidJSON }
|
||||
guard let base64EncodedPublicKey = body["data"] as? String else {
|
||||
SNLog("Couldn't parse open group public key from: \(body).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let prefixedPublicKey = Data(base64Encoded: base64EncodedPublicKey)!
|
||||
let hexEncodedPrefixedPublicKey = prefixedPublicKey.toHexString()
|
||||
return hexEncodedPrefixedPublicKey.removing05PrefixIfNeeded()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,6 +9,14 @@ public final class FileServerAPIV2 : NSObject {
|
|||
public static let oldServerPublicKey = "7cb31905b55cd5580c686911debf672577b3fb0bff81df4ce2d5c4cb3a7aaa69"
|
||||
@objc public static let server = "http://filev2.getsession.org"
|
||||
public static let serverPublicKey = "da21e1d886c6fbaea313f75298bd64aab03a97ce985b46bb2dad9f2089c8ee59"
|
||||
public static let maxFileSize = 10_000_000 // 10 MB
|
||||
/// The file server has a file size limit of `maxFileSize`, which the Service Nodes try to enforce as well. However, the limit applied by the Service Nodes
|
||||
/// is on the **HTTP request** and not the actual file size. Because the file server expects the file data to be base 64 encoded, the size of the HTTP
|
||||
/// request for a given file will be at least `ceil(n / 3) * 4` bytes, where n is the file size in bytes. This is the minimum size because there might also
|
||||
/// be other parameters in the request. On average the multiplier appears to be about 1.5, so when checking whether the file will exceed the file size limit when
|
||||
/// uploading a file we just divide the size of the file by this number. The alternative would be to actually check the size of the HTTP request but that's only
|
||||
/// possible after proof of work has been calculated and the onion request encryption has happened, which takes several seconds.
|
||||
public static let fileSizeORMultiplier: Double = 2
|
||||
|
||||
// MARK: Initialization
|
||||
private override init() { }
|
||||
|
@ -17,11 +25,13 @@ public final class FileServerAPIV2 : NSObject {
|
|||
public enum Error : LocalizedError {
|
||||
case parsingFailed
|
||||
case invalidURL
|
||||
case maxFileSizeExceeded
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .parsingFailed: return "Invalid response."
|
||||
case .invalidURL: return "Invalid URL."
|
||||
case .maxFileSizeExceeded: return "Maximum file size exceeded."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -80,13 +80,6 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
|||
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: { })
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else if let error = error as? DotNetAPI.Error, case .parsingFailed = error {
|
||||
// No need to retry if the response is invalid. Most likely this means we (incorrectly)
|
||||
// got a "Cannot GET ..." error from the file server.
|
||||
storage.write(with: { transaction in
|
||||
storage.setAttachmentState(to: .failed, for: pointer, associatedWith: self.tsMessageID, using: transaction)
|
||||
}, completion: { })
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else {
|
||||
self.handleFailure(error: error)
|
||||
}
|
||||
|
@ -100,7 +93,7 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
|||
}.catch(on: DispatchQueue.global()) { error in
|
||||
handleFailure(error)
|
||||
}
|
||||
} else if pointer.downloadURL.contains(FileServerAPIV2.server) || pointer.downloadURL.contains(FileServerAPIV2.oldServer) {
|
||||
} else {
|
||||
guard let fileAsString = pointer.downloadURL.split(separator: "/").last, let file = UInt64(fileAsString) else {
|
||||
return handleFailure(Error.invalidURL)
|
||||
}
|
||||
|
@ -110,12 +103,6 @@ public final class AttachmentDownloadJob : NSObject, Job, NSCoding { // NSObject
|
|||
}.catch(on: DispatchQueue.global()) { error in
|
||||
handleFailure(error)
|
||||
}
|
||||
} else { // Legacy
|
||||
FileServerAPI.downloadAttachment(from: pointer.downloadURL).done(on: DispatchQueue.global(qos: .userInitiated)) { data in
|
||||
self.handleDownloadedAttachment(data: data, temporaryFilePath: temporaryFilePath, pointer: pointer, failureHandler: handleFailure)
|
||||
}.catch(on: DispatchQueue.global()) { error in
|
||||
handleFailure(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -67,23 +67,8 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
|
|||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: threadID) {
|
||||
AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: handleSuccess, onFailure: handleFailure)
|
||||
} else if Features.useV2FileServer && storage.getOpenGroup(for: threadID) == nil {
|
||||
} else {
|
||||
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: handleSuccess, onFailure: handleFailure)
|
||||
} else { // Legacy
|
||||
let openGroup = storage.getOpenGroup(for: threadID)
|
||||
let server = openGroup?.server ?? FileServerAPI.server
|
||||
// FIXME: A lot of what's currently happening in FileServerAPI should really be happening here
|
||||
FileServerAPI.uploadAttachment(stream, with: attachmentID, to: server).done(on: DispatchQueue.global(qos: .userInitiated)) { // Intentionally capture self
|
||||
self.handleSuccess()
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
if let error = error as? Error, case .noAttachment = error {
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else if let error = error as? DotNetAPI.Error, !error.isRetryable {
|
||||
self.handlePermanentFailure(error: error)
|
||||
} else {
|
||||
self.handleFailure(error: error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,8 +92,8 @@ public final class AttachmentUploadJob : NSObject, Job, NSCoding { // NSObject/N
|
|||
}
|
||||
// Check the file size
|
||||
SNLog("File size: \(data.count) bytes.")
|
||||
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
|
||||
onFailure?(FileServerAPI.Error.maxFileSizeExceeded); return
|
||||
if Double(data.count) > Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier {
|
||||
onFailure?(FileServerAPIV2.Error.maxFileSizeExceeded); return
|
||||
}
|
||||
// Send the request
|
||||
stream.isUploaded = false
|
||||
|
|
|
@ -15,12 +15,8 @@ public extension Message {
|
|||
let groupPublicKey = LKGroupUtilities.getDecodedGroupID(groupID)
|
||||
return .closedGroup(groupPublicKey: groupPublicKey)
|
||||
} else if let thread = thread as? TSGroupThread, thread.isOpenGroup {
|
||||
if let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
return .openGroupV2(room: openGroupV2.room, server: openGroupV2.server)
|
||||
} else {
|
||||
let openGroup = Storage.shared.getOpenGroup(for: thread.uniqueId!)!
|
||||
return .openGroup(channel: openGroup.channel, server: openGroup.server)
|
||||
}
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: thread.uniqueId!)!
|
||||
return .openGroupV2(room: openGroupV2.room, server: openGroupV2.server)
|
||||
} else {
|
||||
preconditionFailure("TODO: Handle legacy closed groups.")
|
||||
}
|
||||
|
|
|
@ -19,9 +19,6 @@ typedef NS_ENUM(NSInteger, TSOutgoingMessageState) {
|
|||
TSOutgoingMessageStateSending,
|
||||
// The failure state.
|
||||
TSOutgoingMessageStateFailed,
|
||||
// These two enum values have been combined into TSOutgoingMessageStateSent.
|
||||
TSOutgoingMessageStateSent_OBSOLETE,
|
||||
TSOutgoingMessageStateDelivered_OBSOLETE,
|
||||
// The message has been sent to the service.
|
||||
TSOutgoingMessageStateSent,
|
||||
};
|
||||
|
@ -135,42 +132,8 @@ typedef NS_ENUM(NSInteger, TSGroupMetaMessage) {
|
|||
|
||||
@property (nonatomic, readonly) BOOL isVoiceMessage;
|
||||
|
||||
// This property won't be accurate for legacy messages.
|
||||
@property (atomic, readonly) BOOL isFromLinkedDevice;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isSilent;
|
||||
|
||||
@property (nonatomic, readonly) BOOL isOnline;
|
||||
|
||||
@property (nonatomic, readonly) uint ttl;
|
||||
|
||||
+ (nullable instancetype)findMessageWithTimestamp:(uint64_t)timestamp;
|
||||
|
||||
/**
|
||||
* The data representation of this message, to be encrypted, before being sent.
|
||||
*/
|
||||
- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient;
|
||||
|
||||
/**
|
||||
* Intermediate protobuf representation
|
||||
* Subclasses can augment if they want to manipulate the data message before building.
|
||||
*/
|
||||
- (nullable id)dataMessageBuilder;
|
||||
|
||||
- (nullable SNProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId;
|
||||
|
||||
/**
|
||||
* Allows subclasses to supply a custom content builder that has already prepared part of the message.
|
||||
*/
|
||||
- (nullable id)prepareCustomContentBuilder:(SignalRecipient *)recipient;
|
||||
|
||||
/**
|
||||
* Should this message be synced to the users other registered devices? This is
|
||||
* generally always true, except in the case of the sync messages themseleves
|
||||
* (so we don't end up in an infinite loop).
|
||||
*/
|
||||
- (BOOL)shouldSyncTranscript;
|
||||
|
||||
- (BOOL)shouldBeSaved;
|
||||
|
||||
// All recipients of this message.
|
||||
|
|
|
@ -37,10 +37,6 @@ NSString *NSStringForOutgoingMessageState(TSOutgoingMessageState value)
|
|||
return @"TSOutgoingMessageStateSending";
|
||||
case TSOutgoingMessageStateFailed:
|
||||
return @"TSOutgoingMessageStateFailed";
|
||||
case TSOutgoingMessageStateSent_OBSOLETE:
|
||||
return @"TSOutgoingMessageStateSent_OBSOLETE";
|
||||
case TSOutgoingMessageStateDelivered_OBSOLETE:
|
||||
return @"TSOutgoingMessageStateDelivered_OBSOLETE";
|
||||
case TSOutgoingMessageStateSent:
|
||||
return @"TSOutgoingMessageStateSent";
|
||||
}
|
||||
|
@ -82,13 +78,7 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
@property (atomic) BOOL hasSyncedTranscript;
|
||||
@property (atomic) NSString *customMessage;
|
||||
@property (atomic) NSString *mostRecentFailureText;
|
||||
@property (atomic) BOOL isFromLinkedDevice;
|
||||
@property (atomic) TSGroupMetaMessage groupMetaMessage;
|
||||
|
||||
@property (nonatomic, readonly) TSOutgoingMessageState legacyMessageState;
|
||||
@property (nonatomic, readonly) BOOL legacyWasDelivered;
|
||||
@property (nonatomic, readonly) BOOL hasLegacyMessageState;
|
||||
|
||||
@property (atomic, nullable) NSDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap;
|
||||
|
||||
@end
|
||||
|
@ -105,118 +95,11 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
if (!_attachmentFilenameMap) {
|
||||
_attachmentFilenameMap = [NSMutableDictionary new];
|
||||
}
|
||||
|
||||
if (!self.recipientStateMap) {
|
||||
[self migrateRecipientStateMapWithCoder:coder];
|
||||
}
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
- (void)migrateRecipientStateMapWithCoder:(NSCoder *)coder
|
||||
{
|
||||
// Determine the "overall message state."
|
||||
TSOutgoingMessageState oldMessageState = TSOutgoingMessageStateFailed;
|
||||
NSNumber *_Nullable messageStateValue = [coder decodeObjectForKey:@"messageState"];
|
||||
if (messageStateValue) {
|
||||
oldMessageState = (TSOutgoingMessageState)messageStateValue.intValue;
|
||||
}
|
||||
_hasLegacyMessageState = YES;
|
||||
_legacyMessageState = oldMessageState;
|
||||
|
||||
OWSOutgoingMessageRecipientState defaultState;
|
||||
switch (oldMessageState) {
|
||||
case TSOutgoingMessageStateFailed:
|
||||
defaultState = OWSOutgoingMessageRecipientStateFailed;
|
||||
break;
|
||||
case TSOutgoingMessageStateSending:
|
||||
defaultState = OWSOutgoingMessageRecipientStateSending;
|
||||
break;
|
||||
case TSOutgoingMessageStateSent:
|
||||
case TSOutgoingMessageStateSent_OBSOLETE:
|
||||
case TSOutgoingMessageStateDelivered_OBSOLETE:
|
||||
// Convert legacy values.
|
||||
defaultState = OWSOutgoingMessageRecipientStateSent;
|
||||
break;
|
||||
}
|
||||
|
||||
// Try to leverage the "per-recipient state."
|
||||
NSDictionary<NSString *, NSNumber *> *_Nullable recipientDeliveryMap =
|
||||
[coder decodeObjectForKey:@"recipientDeliveryMap"];
|
||||
NSDictionary<NSString *, NSNumber *> *_Nullable recipientReadMap = [coder decodeObjectForKey:@"recipientReadMap"];
|
||||
NSArray<NSString *> *_Nullable sentRecipients = [coder decodeObjectForKey:@"sentRecipients"];
|
||||
|
||||
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap = [NSMutableDictionary new];
|
||||
__block BOOL isGroupThread = NO;
|
||||
// Our default recipient list is the current thread members.
|
||||
__block NSArray<NSString *> *recipientIds = @[];
|
||||
// To avoid deadlock while migrating these records, we use a dedicated
|
||||
// migration connection. For legacy records (created more than ~9 months
|
||||
// before the migration), we need to infer the recipient list for this
|
||||
// message from the current thread membership. This inference isn't
|
||||
// always accurate, so not using the same connection for both reads is
|
||||
// acceptable.
|
||||
[TSOutgoingMessage.dbMigrationConnection readWithBlock:^(YapDatabaseReadTransaction *transaction) {
|
||||
TSThread *thread = [self threadWithTransaction:transaction];
|
||||
recipientIds = [thread recipientIdentifiers];
|
||||
isGroupThread = [thread isGroupThread];
|
||||
}];
|
||||
|
||||
NSNumber *_Nullable wasDelivered = [coder decodeObjectForKey:@"wasDelivered"];
|
||||
_legacyWasDelivered = wasDelivered && wasDelivered.boolValue;
|
||||
BOOL wasDeliveredToContact = NO;
|
||||
if (isGroupThread) {
|
||||
// If we have a `sentRecipients` list, prefer that as it is more accurate.
|
||||
if (sentRecipients) {
|
||||
recipientIds = sentRecipients;
|
||||
}
|
||||
} else {
|
||||
// Special-case messages in contact threads; if "was delivered", we know
|
||||
// it was delivered to the contact.
|
||||
wasDeliveredToContact = _legacyWasDelivered;
|
||||
}
|
||||
|
||||
NSString *_Nullable singleGroupRecipient = [coder decodeObjectForKey:@"singleGroupRecipient"];
|
||||
if (singleGroupRecipient) {
|
||||
// If this is a "single group recipient message", treat it as such.
|
||||
recipientIds = @[
|
||||
singleGroupRecipient,
|
||||
];
|
||||
}
|
||||
|
||||
for (NSString *recipientId in recipientIds) {
|
||||
TSOutgoingMessageRecipientState *recipientState = [TSOutgoingMessageRecipientState new];
|
||||
|
||||
NSNumber *_Nullable readTimestamp = recipientReadMap[recipientId];
|
||||
NSNumber *_Nullable deliveryTimestamp = recipientDeliveryMap[recipientId];
|
||||
if (readTimestamp) {
|
||||
// If we have a read timestamp for this recipient, mark it as read.
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
recipientState.readTimestamp = readTimestamp;
|
||||
// deliveryTimestamp might be nil here.
|
||||
recipientState.deliveryTimestamp = deliveryTimestamp;
|
||||
} else if (deliveryTimestamp) {
|
||||
// If we have a delivery timestamp for this recipient, mark it as delivered.
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
recipientState.deliveryTimestamp = deliveryTimestamp;
|
||||
} else if (wasDeliveredToContact) {
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
// Use message time as an estimate of delivery time.
|
||||
recipientState.deliveryTimestamp = @(self.timestamp);
|
||||
} else if ([sentRecipients containsObject:recipientId]) {
|
||||
// If this recipient is in `sentRecipients`, mark it as sent.
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
} else {
|
||||
// Use the default state for this message.
|
||||
recipientState.state = defaultState;
|
||||
}
|
||||
|
||||
recipientStateMap[recipientId] = recipientState;
|
||||
}
|
||||
self.recipientStateMap = [recipientStateMap copy];
|
||||
}
|
||||
|
||||
+ (YapDatabaseConnection *)dbMigrationConnection
|
||||
{
|
||||
return SSKEnvironment.shared.migrationDBConnection;
|
||||
|
@ -388,30 +271,17 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
|
||||
- (TSOutgoingMessageState)messageState
|
||||
{
|
||||
TSOutgoingMessageState newMessageState =
|
||||
[TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues];
|
||||
if (self.hasLegacyMessageState) {
|
||||
if (newMessageState == TSOutgoingMessageStateSent || self.legacyMessageState == TSOutgoingMessageStateSent) {
|
||||
return TSOutgoingMessageStateSent;
|
||||
}
|
||||
}
|
||||
return newMessageState;
|
||||
return [TSOutgoingMessage messageStateForRecipientStates:self.recipientStateMap.allValues];
|
||||
}
|
||||
|
||||
- (BOOL)wasDeliveredToAnyRecipient
|
||||
{
|
||||
if ([self deliveredRecipientIds].count > 0) {
|
||||
return YES;
|
||||
}
|
||||
return (self.hasLegacyMessageState && self.legacyWasDelivered && self.messageState == TSOutgoingMessageStateSent);
|
||||
return [self deliveredRecipientIds].count > 0;
|
||||
}
|
||||
|
||||
- (BOOL)wasSentToAnyRecipient
|
||||
{
|
||||
if ([self sentRecipientIds].count > 0) {
|
||||
return YES;
|
||||
}
|
||||
return (self.hasLegacyMessageState && self.messageState == TSOutgoingMessageStateSent);
|
||||
return [self sentRecipientIds].count > 0;
|
||||
}
|
||||
|
||||
+ (TSOutgoingMessageState)messageStateForRecipientStates:(NSArray<TSOutgoingMessageRecipientState *> *)recipientStates
|
||||
|
@ -485,16 +355,6 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
}
|
||||
}
|
||||
|
||||
- (BOOL)isSilent
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
- (BOOL)isOnline
|
||||
{
|
||||
return NO;
|
||||
}
|
||||
|
||||
+ (nullable instancetype)findMessageWithTimestamp:(uint64_t)timestamp
|
||||
{
|
||||
__block TSOutgoingMessage *result;
|
||||
|
@ -628,15 +488,6 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)updateWithHasSyncedTranscript:(BOOL)hasSyncedTranscript
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self applyChangeToSelfAndLatestCopy:transaction
|
||||
changeBlock:^(TSOutgoingMessage *message) {
|
||||
[message setHasSyncedTranscript:hasSyncedTranscript];
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateWithCustomMessage:(NSString *)customMessage transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self applyChangeToSelfAndLatestCopy:transaction
|
||||
|
@ -708,325 +559,6 @@ NSString *NSStringForOutgoingMessageRecipientState(OWSOutgoingMessageRecipientSt
|
|||
}];
|
||||
}
|
||||
|
||||
- (void)updateWithWasSentFromLinkedDeviceWithUDRecipientIds:(nullable NSArray<NSString *> *)udRecipientIds
|
||||
nonUdRecipientIds:(nullable NSArray<NSString *> *)nonUdRecipientIds
|
||||
isSentUpdate:(BOOL)isSentUpdate
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self
|
||||
applyChangeToSelfAndLatestCopy:transaction
|
||||
changeBlock:^(TSOutgoingMessage *message) {
|
||||
if (udRecipientIds.count > 0 || nonUdRecipientIds.count > 0) {
|
||||
// If we have specific recipient info from the transcript,
|
||||
// build a new recipient state map.
|
||||
NSMutableDictionary<NSString *, TSOutgoingMessageRecipientState *> *recipientStateMap
|
||||
= [NSMutableDictionary new];
|
||||
for (NSString *recipientId in udRecipientIds) {
|
||||
if (recipientStateMap[recipientId]) {
|
||||
continue;
|
||||
}
|
||||
TSOutgoingMessageRecipientState *recipientState =
|
||||
[TSOutgoingMessageRecipientState new];
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
recipientState.wasSentByUD = YES;
|
||||
recipientStateMap[recipientId] = recipientState;
|
||||
}
|
||||
for (NSString *recipientId in nonUdRecipientIds) {
|
||||
if (recipientStateMap[recipientId]) {
|
||||
continue;
|
||||
}
|
||||
TSOutgoingMessageRecipientState *recipientState =
|
||||
[TSOutgoingMessageRecipientState new];
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
recipientState.wasSentByUD = NO;
|
||||
recipientStateMap[recipientId] = recipientState;
|
||||
}
|
||||
|
||||
if (isSentUpdate) {
|
||||
// If this is a "sent update", make sure that:
|
||||
//
|
||||
// a) "Sent updates" should never remove any recipients. We end up with the
|
||||
// union of the existing and new recipients.
|
||||
// b) "Sent updates" should never downgrade the "recipient state" for any
|
||||
// recipients. Prefer existing "recipient state"; "sent updates" only
|
||||
// add new recipients at the "sent" state.
|
||||
//
|
||||
// Therefore we retain all existing entries in the recipient state map.
|
||||
[recipientStateMap addEntriesFromDictionary:self.recipientStateMap];
|
||||
}
|
||||
|
||||
[message setRecipientStateMap:recipientStateMap];
|
||||
} else {
|
||||
// Otherwise assume this is a legacy message before UD was introduced, and mark
|
||||
// any "sending" recipient as "sent". Note that this will apply to non-legacy
|
||||
// messages with no recipients.
|
||||
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
|
||||
.allValues) {
|
||||
if (recipientState.state == OWSOutgoingMessageRecipientStateSending) {
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isSentUpdate) {
|
||||
[message setIsFromLinkedDevice:YES];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
- (void)updateWithSendingToSingleGroupRecipient:(NSString *)singleGroupRecipient
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self applyChangeToSelfAndLatestCopy:transaction
|
||||
changeBlock:^(TSOutgoingMessage *message) {
|
||||
TSOutgoingMessageRecipientState *recipientState =
|
||||
[TSOutgoingMessageRecipientState new];
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSending;
|
||||
[message setRecipientStateMap:@{
|
||||
singleGroupRecipient : recipientState,
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
- (nullable NSNumber *)firstRecipientReadTimestamp
|
||||
{
|
||||
NSNumber *result = nil;
|
||||
for (TSOutgoingMessageRecipientState *recipientState in self.recipientStateMap.allValues) {
|
||||
if (!recipientState.readTimestamp) {
|
||||
continue;
|
||||
}
|
||||
if (!result || (result.unsignedLongLongValue > recipientState.readTimestamp.unsignedLongLongValue)) {
|
||||
result = recipientState.readTimestamp;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
- (void)updateWithFakeMessageState:(TSOutgoingMessageState)messageState
|
||||
transaction:(YapDatabaseReadWriteTransaction *)transaction
|
||||
{
|
||||
[self applyChangeToSelfAndLatestCopy:transaction
|
||||
changeBlock:^(TSOutgoingMessage *message) {
|
||||
for (TSOutgoingMessageRecipientState *recipientState in message.recipientStateMap
|
||||
.allValues) {
|
||||
switch (messageState) {
|
||||
case TSOutgoingMessageStateSending:
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSending;
|
||||
break;
|
||||
case TSOutgoingMessageStateFailed:
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateFailed;
|
||||
break;
|
||||
case TSOutgoingMessageStateSent:
|
||||
recipientState.state = OWSOutgoingMessageRecipientStateSent;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
#pragma mark -
|
||||
|
||||
- (nullable id)dataMessageBuilder
|
||||
{
|
||||
TSThread *thread = self.thread;
|
||||
|
||||
SNProtoDataMessageBuilder *builder = [SNProtoDataMessage builder];
|
||||
[builder setTimestamp:self.timestamp];
|
||||
|
||||
if ([self.body lengthOfBytesUsingEncoding:NSUTF8StringEncoding] <= kOversizeTextMessageSizeThreshold) {
|
||||
[builder setBody:self.body];
|
||||
} else {
|
||||
NSString *truncatedBody = [self.body copy];
|
||||
while ([truncatedBody lengthOfBytesUsingEncoding:NSUTF8StringEncoding] > kOversizeTextMessageSizeThreshold) {
|
||||
truncatedBody = [truncatedBody substringToIndex:truncatedBody.length / 2];
|
||||
}
|
||||
[builder setBody:truncatedBody];
|
||||
}
|
||||
[builder setExpireTimer:self.expiresInSeconds];
|
||||
|
||||
// Group Messages
|
||||
BOOL attachmentWasGroupAvatar = NO;
|
||||
if ([thread isKindOfClass:[TSGroupThread class]]) {
|
||||
TSGroupThread *gThread = (TSGroupThread *)thread;
|
||||
SNProtoGroupContextType groupMessageType;
|
||||
switch (self.groupMetaMessage) {
|
||||
case TSGroupMetaMessageQuit:
|
||||
groupMessageType = SNProtoGroupContextTypeQuit;
|
||||
break;
|
||||
case TSGroupMetaMessageUpdate:
|
||||
case TSGroupMetaMessageNew:
|
||||
groupMessageType = SNProtoGroupContextTypeUpdate;
|
||||
break;
|
||||
default:
|
||||
groupMessageType = SNProtoGroupContextTypeDeliver;
|
||||
break;
|
||||
}
|
||||
SNProtoGroupContextBuilder *groupBuilder =
|
||||
[SNProtoGroupContext builderWithId:gThread.groupModel.groupId type:groupMessageType];
|
||||
if (groupMessageType == SNProtoGroupContextTypeUpdate) {
|
||||
if (gThread.groupModel.groupImage != nil && self.attachmentIds.count == 1) {
|
||||
attachmentWasGroupAvatar = YES;
|
||||
SNProtoAttachmentPointer *_Nullable attachmentProto =
|
||||
[TSAttachmentStream buildProtoForAttachmentId:self.attachmentIds.firstObject];
|
||||
if (!attachmentProto) {
|
||||
return nil;
|
||||
}
|
||||
[groupBuilder setAvatar:attachmentProto];
|
||||
}
|
||||
|
||||
[groupBuilder setMembers:gThread.groupModel.groupMemberIds];
|
||||
[groupBuilder setName:gThread.groupModel.groupName];
|
||||
[groupBuilder setAdmins:gThread.groupModel.groupAdminIds];
|
||||
}
|
||||
NSError *error;
|
||||
SNProtoGroupContext *_Nullable groupContextProto = [groupBuilder buildAndReturnError:&error];
|
||||
if (error || !groupContextProto) {
|
||||
return nil;
|
||||
}
|
||||
[builder setGroup:groupContextProto];
|
||||
}
|
||||
|
||||
// Message Attachments
|
||||
if (!attachmentWasGroupAvatar) {
|
||||
NSMutableArray *attachments = [NSMutableArray new];
|
||||
for (NSString *attachmentId in self.attachmentIds) {
|
||||
SNProtoAttachmentPointer *_Nullable attachmentProto =
|
||||
[TSAttachmentStream buildProtoForAttachmentId:attachmentId];
|
||||
if (!attachmentProto) {
|
||||
return nil;
|
||||
}
|
||||
[attachments addObject:attachmentProto];
|
||||
}
|
||||
[builder setAttachments:attachments];
|
||||
}
|
||||
|
||||
// Quoted Reply
|
||||
SNProtoDataMessageQuoteBuilder *_Nullable quotedMessageBuilder = self.quotedMessageBuilder;
|
||||
if (quotedMessageBuilder) {
|
||||
NSError *error;
|
||||
SNProtoDataMessageQuote *_Nullable quoteProto = [quotedMessageBuilder buildAndReturnError:&error];
|
||||
if (error || !quoteProto) {
|
||||
return nil;
|
||||
}
|
||||
[builder setQuote:quoteProto];
|
||||
}
|
||||
|
||||
// Link Preview
|
||||
if (self.linkPreview) {
|
||||
SNProtoDataMessagePreviewBuilder *previewBuilder =
|
||||
[SNProtoDataMessagePreview builderWithUrl:self.linkPreview.urlString];
|
||||
if (self.linkPreview.title.length > 0) {
|
||||
[previewBuilder setTitle:self.linkPreview.title];
|
||||
}
|
||||
if (self.linkPreview.imageAttachmentId) {
|
||||
SNProtoAttachmentPointer *_Nullable attachmentProto =
|
||||
[TSAttachmentStream buildProtoForAttachmentId:self.linkPreview.imageAttachmentId];
|
||||
if (!attachmentProto) {
|
||||
|
||||
} else {
|
||||
[previewBuilder setImage:attachmentProto];
|
||||
}
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
SNProtoDataMessagePreview *_Nullable previewProto = [previewBuilder buildAndReturnError:&error];
|
||||
if (error || !previewProto) {
|
||||
|
||||
} else {
|
||||
[builder addPreview:previewProto];
|
||||
}
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
- (nullable SNProtoDataMessageQuoteBuilder *)quotedMessageBuilder
|
||||
{
|
||||
if (!self.quotedMessage) {
|
||||
return nil;
|
||||
}
|
||||
TSQuotedMessage *quotedMessage = self.quotedMessage;
|
||||
|
||||
SNProtoDataMessageQuoteBuilder *quoteBuilder =
|
||||
[SNProtoDataMessageQuote builderWithId:quotedMessage.timestamp author:quotedMessage.authorId];
|
||||
|
||||
BOOL hasQuotedText = NO;
|
||||
BOOL hasQuotedAttachment = NO;
|
||||
if (self.quotedMessage.body.length > 0) {
|
||||
hasQuotedText = YES;
|
||||
[quoteBuilder setText:quotedMessage.body];
|
||||
}
|
||||
|
||||
if (quotedMessage.quotedAttachments) {
|
||||
for (OWSAttachmentInfo *attachment in quotedMessage.quotedAttachments) {
|
||||
hasQuotedAttachment = YES;
|
||||
|
||||
SNProtoDataMessageQuoteQuotedAttachmentBuilder *quotedAttachmentBuilder =
|
||||
[SNProtoDataMessageQuoteQuotedAttachment builder];
|
||||
|
||||
quotedAttachmentBuilder.contentType = attachment.contentType;
|
||||
quotedAttachmentBuilder.fileName = attachment.sourceFilename;
|
||||
if (attachment.thumbnailAttachmentStreamId) {
|
||||
quotedAttachmentBuilder.thumbnail =
|
||||
[TSAttachmentStream buildProtoForAttachmentId:attachment.thumbnailAttachmentStreamId];
|
||||
}
|
||||
|
||||
NSError *error;
|
||||
SNProtoDataMessageQuoteQuotedAttachment *_Nullable quotedAttachmentMessage =
|
||||
[quotedAttachmentBuilder buildAndReturnError:&error];
|
||||
if (error || !quotedAttachmentMessage) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
[quoteBuilder addAttachments:quotedAttachmentMessage];
|
||||
}
|
||||
}
|
||||
|
||||
if (hasQuotedText || hasQuotedAttachment) {
|
||||
return quoteBuilder;
|
||||
} else {
|
||||
return nil;
|
||||
}
|
||||
}
|
||||
|
||||
// recipientId is nil when building "sent" sync messages for messages sent to groups.
|
||||
- (nullable SNProtoDataMessage *)buildDataMessage:(NSString *_Nullable)recipientId
|
||||
{
|
||||
return nil; // Shouldn't be in use anymore
|
||||
}
|
||||
|
||||
- (nullable NSData *)buildPlainTextData:(SignalRecipient *)recipient
|
||||
{
|
||||
SNProtoContentBuilder *contentBuilder = [self prepareCustomContentBuilder:recipient];
|
||||
|
||||
NSError *error;
|
||||
NSData *_Nullable contentData = [contentBuilder buildSerializedDataAndReturnError:&error];
|
||||
if (error != nil || contentData == nil) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return contentData;
|
||||
}
|
||||
|
||||
- (BOOL)shouldSyncTranscript
|
||||
{
|
||||
return YES;
|
||||
}
|
||||
|
||||
- (NSString *)statusDescription
|
||||
{
|
||||
NSMutableString *result = [NSMutableString new];
|
||||
[result appendFormat:@"[status: %@", NSStringForOutgoingMessageState(self.messageState)];
|
||||
for (NSString *recipientId in self.recipientStateMap) {
|
||||
TSOutgoingMessageRecipientState *recipientState = self.recipientStateMap[recipientId];
|
||||
[result appendFormat:@", %@: %@", recipientId, NSStringForOutgoingMessageRecipientState(recipientState.state)];
|
||||
}
|
||||
[result appendString:@"]"];
|
||||
return [result copy];
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
|
@ -1,43 +0,0 @@
|
|||
|
||||
@objc(SNOpenGroup)
|
||||
public final class OpenGroup : NSObject, NSCoding {
|
||||
@objc public let id: String
|
||||
@objc public let idAsData: Data
|
||||
@objc public let channel: UInt64
|
||||
@objc public let server: String
|
||||
@objc public let displayName: String
|
||||
@objc public let isDeletable: Bool
|
||||
|
||||
@objc public init?(channel: UInt64, server: String, displayName: String, isDeletable: Bool) {
|
||||
let id = "\(server).\(channel)"
|
||||
self.id = id
|
||||
guard let idAsData = id.data(using: .utf8) else { return nil }
|
||||
self.idAsData = idAsData
|
||||
self.channel = channel
|
||||
self.server = server.lowercased()
|
||||
self.displayName = displayName
|
||||
self.isDeletable = isDeletable
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
@objc public init?(coder: NSCoder) {
|
||||
channel = UInt64(coder.decodeInt64(forKey: "channel"))
|
||||
server = coder.decodeObject(forKey: "server") as! String
|
||||
let id = "\(server).\(channel)"
|
||||
self.id = id
|
||||
guard let idAsData = id.data(using: .utf8) else { return nil }
|
||||
self.idAsData = idAsData
|
||||
displayName = coder.decodeObject(forKey: "displayName") as! String
|
||||
isDeletable = coder.decodeBool(forKey: "isDeletable")
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc public func encode(with coder: NSCoder) {
|
||||
coder.encode(Int64(channel), forKey: "channel")
|
||||
coder.encode(server, forKey: "server")
|
||||
coder.encode(displayName, forKey: "displayName")
|
||||
coder.encode(isDeletable, forKey: "isDeletable")
|
||||
}
|
||||
|
||||
override public var description: String { "\(displayName) (\(server))" }
|
||||
}
|
|
@ -1,514 +0,0 @@
|
|||
import AFNetworking
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNOpenGroupAPI)
|
||||
public final class OpenGroupAPI : DotNetAPI {
|
||||
private static var moderators: [String:[UInt64:Set<String>]] = [:] // Server URL to (channel ID to set of moderator IDs)
|
||||
|
||||
public static var displayNameUpdatees: [String:Set<String>] = [:]
|
||||
|
||||
// MARK: Settings
|
||||
private static let attachmentType = "net.app.core.oembed"
|
||||
private static let channelInfoType = "net.patter-app.settings"
|
||||
private static let fallbackBatchCount = 64
|
||||
private static let maxRetryCount: UInt = 4
|
||||
|
||||
public static let profilePictureType = "network.loki.messenger.avatar"
|
||||
@objc public static let openGroupMessageType = "network.loki.messenger.publicChat"
|
||||
|
||||
// MARK: Open Group Public Key Validation
|
||||
public static func getOpenGroupServerPublicKey(for server: String) -> Promise<String> {
|
||||
if let publicKey = SNMessagingKitConfiguration.shared.storage.getOpenGroupPublicKey(for: server) {
|
||||
return Promise.value(publicKey)
|
||||
} else {
|
||||
return FileServerAPI.getPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { publicKey -> Promise<String> in
|
||||
let url = URL(string: server)!
|
||||
let request = TSRequest(url: url)
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: publicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .default)) { _ -> String in
|
||||
SNMessagingKitConfiguration.shared.storage.writeSync { transaction in
|
||||
SNMessagingKitConfiguration.shared.storage.setOpenGroupPublicKey(for: server, to: publicKey, using: transaction)
|
||||
}
|
||||
return publicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Receiving
|
||||
@objc(getMessagesForGroup:onServer:)
|
||||
public static func objc_getMessages(for group: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(getMessages(for: group, on: server))
|
||||
}
|
||||
|
||||
public static func getMessages(for channel: UInt64, on server: String) -> Promise<[OpenGroupMessage]> {
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
var queryParameters = "include_annotations=1"
|
||||
if let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server) {
|
||||
queryParameters += "&since_id=\(lastMessageServerID)"
|
||||
} else {
|
||||
queryParameters += "&count=\(fallbackBatchCount)&include_deleted=0"
|
||||
}
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[OpenGroupMessage]> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let rawMessages = json["data"] as? [JSON] else {
|
||||
SNLog("Couldn't parse messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return rawMessages.compactMap { message in
|
||||
let isDeleted = (message["is_deleted"] as? Int == 1)
|
||||
guard !isDeleted else { return nil }
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let annotations = message["annotations"] as? [JSON], let annotation = annotations.first(where: { $0["type"] as? String == openGroupMessageType }), let value = annotation["value"] as? JSON,
|
||||
let serverID = message["id"] as? UInt64, let hexEncodedSignatureData = value["sig"] as? String, let signatureVersion = value["sigver"] as? UInt64,
|
||||
let body = message["text"] as? String, let user = message["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String,
|
||||
let timestamp = value["timestamp"] as? UInt64, let dateAsString = message["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(message).")
|
||||
return nil
|
||||
}
|
||||
let serverTimestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
var profilePicture: OpenGroupMessage.ProfilePicture? = nil
|
||||
let displayName = user["name"] as? String ?? NSLocalizedString("Anonymous", comment: "")
|
||||
if let userAnnotations = user["annotations"] as? [JSON], let profilePictureAnnotation = userAnnotations.first(where: { $0["type"] as? String == profilePictureType }),
|
||||
let profilePictureValue = profilePictureAnnotation["value"] as? JSON, let profileKeyString = profilePictureValue["profileKey"] as? String, let profileKey = Data(base64Encoded: profileKeyString), let url = profilePictureValue["url"] as? String {
|
||||
profilePicture = OpenGroupMessage.ProfilePicture(profileKey: profileKey, url: url)
|
||||
}
|
||||
let lastMessageServerID = storage.getLastMessageServerID(for: channel, on: server)
|
||||
if serverID > (lastMessageServerID ?? 0) {
|
||||
storage.writeSync { transaction in
|
||||
storage.setLastMessageServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
}
|
||||
}
|
||||
let quote: OpenGroupMessage.Quote?
|
||||
if let quoteAsJSON = value["quote"] as? JSON, let quotedMessageTimestamp = quoteAsJSON["id"] as? UInt64, let quoteePublicKey = quoteAsJSON["author"] as? String,
|
||||
let quotedMessageBody = quoteAsJSON["text"] as? String {
|
||||
let quotedMessageServerID = message["reply_to"] as? UInt64
|
||||
quote = OpenGroupMessage.Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteePublicKey, quotedMessageBody: quotedMessageBody,
|
||||
quotedMessageServerID: quotedMessageServerID)
|
||||
} else {
|
||||
quote = nil
|
||||
}
|
||||
let signature = OpenGroupMessage.Signature(data: Data(hex: hexEncodedSignatureData), version: signatureVersion)
|
||||
let attachmentsAsJSON = annotations.filter { $0["type"] as? String == attachmentType }
|
||||
let attachments: [OpenGroupMessage.Attachment] = attachmentsAsJSON.compactMap { attachmentAsJSON in
|
||||
guard let value = attachmentAsJSON["value"] as? JSON, let kindAsString = value["lokiType"] as? String, let kind = OpenGroupMessage.Attachment.Kind(rawValue: kindAsString),
|
||||
let serverID = value["id"] as? UInt64, let contentType = value["contentType"] as? String, let size = value["size"] as? UInt, let url = value["url"] as? String else { return nil }
|
||||
let fileName = value["fileName"] as? String ?? UUID().description
|
||||
let width = value["width"] as? UInt ?? 0
|
||||
let height = value["height"] as? UInt ?? 0
|
||||
let flags = (value["flags"] as? UInt) ?? 0
|
||||
let caption = value["caption"] as? String
|
||||
let linkPreviewURL = value["linkPreviewUrl"] as? String
|
||||
let linkPreviewTitle = value["linkPreviewTitle"] as? String
|
||||
if kind == .linkPreview {
|
||||
guard linkPreviewURL != nil && linkPreviewTitle != nil else {
|
||||
SNLog("Ignoring open group message with invalid link preview.")
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return OpenGroupMessage.Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags,
|
||||
width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
|
||||
}
|
||||
let result = OpenGroupMessage(serverID: serverID, senderPublicKey: hexEncodedPublicKey, displayName: displayName, profilePicture: profilePicture,
|
||||
body: body, type: openGroupMessageType, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
guard result.hasValidSignature() else {
|
||||
SNLog("Ignoring open group message with invalid signature.")
|
||||
return nil
|
||||
}
|
||||
return result
|
||||
}.sorted { $0.serverTimestamp < $1.serverTimestamp}
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
|
||||
// MARK: Sending
|
||||
@objc(sendMessage:toGroup:onServer:)
|
||||
public static func objc_sendMessage(_ message: OpenGroupMessage, to group: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(sendMessage(message, to: group, on: server))
|
||||
}
|
||||
|
||||
public static func sendMessage(_ message: OpenGroupMessage, to channel: UInt64, on server: String) -> Promise<OpenGroupMessage> {
|
||||
SNLog("Sending message to open group channel with ID: \(channel) on server: \(server).")
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
guard let userKeyPair = storage.getUserKeyPair(),
|
||||
let userName = storage.getUser()?.name else { return Promise(error: Error.generic) }
|
||||
let (promise, seal) = Promise<OpenGroupMessage>.pending()
|
||||
DispatchQueue.global(qos: .userInitiated).async { [privateKey = userKeyPair.privateKey] in
|
||||
guard let signedMessage = message.sign(with: privateKey) else { return seal.reject(Error.signingFailed) }
|
||||
attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupMessage> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/messages")!
|
||||
let parameters = signedMessage.toJSON()
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let userName = userName
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
// ISO8601DateFormatter doesn't support milliseconds before iOS 11
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
guard let messageAsJSON = json["data"] as? JSON, let serverID = messageAsJSON["id"] as? UInt64, let body = messageAsJSON["text"] as? String,
|
||||
let dateAsString = messageAsJSON["created_at"] as? String, let date = dateFormatter.date(from: dateAsString) else {
|
||||
SNLog("Couldn't parse message for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let timestamp = UInt64(date.timeIntervalSince1970) * 1000
|
||||
return OpenGroupMessage(serverID: serverID, senderPublicKey: userKeyPair.publicKey.toHexString(), displayName: userName, profilePicture: signedMessage.profilePicture, body: body, type: openGroupMessageType, timestamp: timestamp, quote: signedMessage.quote, attachments: signedMessage.attachments, signature: signedMessage.signature, serverTimestamp: timestamp)
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}.done(on: DispatchQueue.global(qos: .default)) { message in
|
||||
seal.fulfill(message)
|
||||
}.catch(on: DispatchQueue.global(qos: .default)) { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
return promise
|
||||
}
|
||||
|
||||
// MARK: Deletion
|
||||
public static func getDeletedMessageServerIDs(for channel: UInt64, on server: String) -> Promise<[UInt64]> {
|
||||
SNLog("Getting deleted messages for open group channel with ID: \(channel) on server: \(server).")
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
let queryParameters: String
|
||||
if let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server) {
|
||||
queryParameters = "since_id=\(lastDeletionServerID)"
|
||||
} else {
|
||||
queryParameters = "count=\(fallbackBatchCount)"
|
||||
}
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<[UInt64]> in
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/deletes?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let body = json["body"] as? JSON, let deletions = body["data"] as? [JSON] else {
|
||||
SNLog("Couldn't parse deleted messages for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return deletions.compactMap { deletion in
|
||||
guard let serverID = deletion["id"] as? UInt64, let messageServerID = deletion["message_id"] as? UInt64 else {
|
||||
SNLog("Couldn't parse deleted message for open group channel with ID: \(channel) on server: \(server) from: \(deletion).")
|
||||
return nil
|
||||
}
|
||||
let lastDeletionServerID = storage.getLastDeletionServerID(for: channel, on: server)
|
||||
if serverID > (lastDeletionServerID ?? 0) {
|
||||
storage.writeSync { transaction in
|
||||
storage.setLastDeletionServerID(for: channel, on: server, to: serverID, using: transaction)
|
||||
}
|
||||
}
|
||||
return messageServerID
|
||||
}
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
|
||||
@objc(deleteMessageWithID:forGroup:onServer:isSentByUser:)
|
||||
public static func objc_deleteMessage(with messageID: UInt, for group: UInt64, on server: String, isSentByUser: Bool) -> AnyPromise {
|
||||
return AnyPromise.from(deleteMessage(with: messageID, for: group, on: server, wasSentByUser: isSentByUser))
|
||||
}
|
||||
|
||||
public static func deleteMessage(with messageID: UInt, for channel: UInt64, on server: String, wasSentByUser: Bool) -> Promise<Void> {
|
||||
let isModerationRequest = !wasSentByUser
|
||||
SNLog("Deleting message with ID: \(messageID) for open group channel with ID: \(channel) on server: \(server) (isModerationRequest = \(isModerationRequest)).")
|
||||
let urlAsString = wasSentByUser ? "\(server)/channels/\(channel)/messages/\(messageID)" : "\(server)/loki/v1/moderation/message/\(messageID)"
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: urlAsString)!
|
||||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
SNLog("Deleted message with ID: \(messageID) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Banning
|
||||
@objc(banPublicKey:fromServer:)
|
||||
public static func objc_ban(_ publicKey: String, from server: String) -> AnyPromise {
|
||||
return AnyPromise.from(ban(publicKey, from: server))
|
||||
}
|
||||
|
||||
public static func ban(_ publicKey: String, from server: String) -> Promise<Void> {
|
||||
SNLog("Banning user with ID: \(publicKey) from server: \(server).")
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/loki/v1/moderation/blacklist/@\(publicKey)")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
let promise = OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey, isJSONRequired: false)
|
||||
let _ = promise.done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
SNLog("Banned user with ID: \(publicKey) from server: \(server).")
|
||||
}
|
||||
promise.catch(on: DispatchQueue.main) { error in
|
||||
print(error)
|
||||
}
|
||||
return promise.map { _ in }
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Display Name & Profile Picture
|
||||
public static func getDisplayNames(for channel: UInt64, on server: String) -> Promise<Void> {
|
||||
let openGroupID = "\(server).\(channel)"
|
||||
guard let publicKeys = displayNameUpdatees[openGroupID] else { return Promise.value(()) }
|
||||
displayNameUpdatees[openGroupID] = []
|
||||
SNLog("Getting display names for: \(publicKeys).")
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let queryParameters = "ids=\(publicKeys.map { "@\($0)" }.joined(separator: ","))&include_user_annotations=1"
|
||||
let url = URL(string: "\(server)/users?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let data = json["data"] as? [JSON] else {
|
||||
SNLog("Couldn't parse display names for users: \(publicKeys) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
storage.writeSync { transaction in
|
||||
data.forEach { data in
|
||||
guard let user = data["user"] as? JSON, let hexEncodedPublicKey = user["username"] as? String, let rawDisplayName = user["name"] as? String else { return }
|
||||
let endIndex = hexEncodedPublicKey.endIndex
|
||||
let cutoffIndex = hexEncodedPublicKey.index(endIndex, offsetBy: -8)
|
||||
let displayName = "\(rawDisplayName) (...\(hexEncodedPublicKey[cutoffIndex..<endIndex]))"
|
||||
storage.setOpenGroupDisplayName(to: displayName, for: hexEncodedPublicKey, inOpenGroupWithID: "\(server).\(channel)", using: transaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
|
||||
@objc(setDisplayName:on:)
|
||||
public static func objc_setDisplayName(to newDisplayName: String?, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(setDisplayName(to: newDisplayName, on: server))
|
||||
}
|
||||
|
||||
public static func setDisplayName(to newDisplayName: String?, on server: String) -> Promise<Void> {
|
||||
SNLog("Updating display name on server: \(server).")
|
||||
let parameters: JSON = [ "name" : (newDisplayName ?? "") ]
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
|
||||
print("Couldn't update display name due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
@objc(setProfilePictureURL:usingProfileKey:on:)
|
||||
public static func objc_setProfilePicture(to url: String?, using profileKey: Data, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(setProfilePictureURL(to: url, using: profileKey, on: server))
|
||||
}
|
||||
|
||||
public static func setProfilePictureURL(to url: String?, using profileKey: Data, on server: String) -> Promise<Void> {
|
||||
SNLog("Updating profile picture on server: \(server).")
|
||||
var annotation: JSON = [ "type" : profilePictureType ]
|
||||
if let url = url {
|
||||
annotation["value"] = [ "profileKey" : profileKey.base64EncodedString(), "url" : url ]
|
||||
}
|
||||
let parameters: JSON = [ "annotations" : [ annotation ] ]
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/users/me")!
|
||||
let request = TSRequest(url: url, method: "PATCH", parameters: parameters)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }.recover(on: DispatchQueue.global(qos: .default)) { error in
|
||||
SNLog("Couldn't update profile picture due to error: \(error).")
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Joining & Leaving
|
||||
@objc(getInfoForChannelWithID:onServer:)
|
||||
public static func objc_getInfo(for channel: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(getInfo(for: channel, on: server))
|
||||
}
|
||||
|
||||
public static func getInfo(for channel: UInt64, on server: String) -> Promise<OpenGroupInfo> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<OpenGroupInfo> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)?include_annotations=1")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let data = json["data"] as? JSON,
|
||||
let annotations = data["annotations"] as? [JSON],
|
||||
let annotation = annotations.first,
|
||||
let info = annotation["value"] as? JSON,
|
||||
let displayName = info["name"] as? String,
|
||||
let profilePictureURL = info["avatar"] as? String,
|
||||
let countInfo = data["counts"] as? JSON,
|
||||
let memberCount = countInfo["subscribers"] as? Int else {
|
||||
SNLog("Couldn't parse info for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
storage.writeSync { transaction in
|
||||
storage.setUserCount(to: memberCount, forOpenGroupWithID: "\(server).\(channel)", using: transaction)
|
||||
}
|
||||
let openGroupInfo = OpenGroupInfo(displayName: displayName, profilePictureURL: profilePictureURL, memberCount: memberCount)
|
||||
OpenGroupAPI.updateProfileIfNeeded(for: channel, on: server, from: openGroupInfo)
|
||||
return openGroupInfo
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
public static func updateProfileIfNeeded(for channel: UInt64, on server: String, from info: OpenGroupInfo) {
|
||||
let openGroupID = "\(server).\(channel)"
|
||||
SNMessagingKitConfiguration.shared.storage.write { transaction in
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
// Update user count
|
||||
Storage.shared.setUserCount(to: info.memberCount, forOpenGroupWithID: openGroupID, using: transaction)
|
||||
let thread = TSGroupThread.getOrCreateThread(withGroupId: openGroupID.data(using: .utf8)!, groupType: .openGroup, transaction: transaction)
|
||||
// Update display name if needed
|
||||
let model = thread.groupModel
|
||||
if model.groupName != info.displayName {
|
||||
let newGroupModel = TSGroupModel(title: info.displayName, memberIds: model.groupMemberIds, image: model.groupImage, groupId: model.groupId, groupType: model.groupType, adminIds: model.groupAdminIds)
|
||||
thread.groupModel = newGroupModel
|
||||
thread.save(with: transaction)
|
||||
}
|
||||
// Download and update profile picture if needed
|
||||
let oldProfilePictureURL = Storage.shared.getProfilePictureURL(forOpenGroupWithID: openGroupID)
|
||||
if oldProfilePictureURL != info.profilePictureURL || model.groupImage == nil {
|
||||
Storage.shared.setProfilePictureURL(to: info.profilePictureURL, forOpenGroupWithID: openGroupID, using: transaction)
|
||||
if let profilePictureURL = info.profilePictureURL {
|
||||
var sanitizedServerURL = server
|
||||
while sanitizedServerURL.hasSuffix("/") { sanitizedServerURL.removeLast() }
|
||||
var sanitizedProfilePictureURL = profilePictureURL
|
||||
while sanitizedProfilePictureURL.hasPrefix("/") { sanitizedProfilePictureURL.removeFirst() }
|
||||
let url = "\(sanitizedServerURL)/\(sanitizedProfilePictureURL)"
|
||||
FileServerAPI.downloadAttachment(from: url).map2 { rawData in
|
||||
let attachmentStream: TSAttachmentStream
|
||||
let data: Data
|
||||
if let rawImage = UIImage(data: rawData), let jpegData = rawImage.jpegData(compressionQuality: 0.8) {
|
||||
data = jpegData
|
||||
} else {
|
||||
data = rawData
|
||||
}
|
||||
attachmentStream = TSAttachmentStream(contentType: OWSMimeTypeImageJpeg, byteCount: UInt32(data.count), sourceFilename: nil, caption: nil, albumMessageId: nil)
|
||||
try attachmentStream.write(data)
|
||||
thread.updateAvatar(with: attachmentStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static func join(_ channel: UInt64, on server: String) -> Promise<Void> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
SNLog("Joined channel with ID: \(channel) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
public static func leave(_ channel: UInt64, on server: String) -> Promise<Void> {
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .default)) {
|
||||
getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Void> in
|
||||
let url = URL(string: "\(server)/channels/\(channel)/subscribe")!
|
||||
let request = TSRequest(url: url, method: "DELETE", parameters: [:])
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).done(on: DispatchQueue.global(qos: .default)) { _ -> Void in
|
||||
SNLog("Left channel with ID: \(channel) on server: \(server).")
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Reporting
|
||||
@objc(reportMessageWithID:inChannel:onServer:)
|
||||
public static func objc_reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> AnyPromise {
|
||||
return AnyPromise.from(reportMessageWithID(messageID, in: channel, on: server))
|
||||
}
|
||||
|
||||
public static func reportMessageWithID(_ messageID: UInt64, in channel: UInt64, on server: String) -> Promise<Void> {
|
||||
let url = URL(string: "\(server)/loki/v1/channels/\(channel)/messages/\(messageID)/report")!
|
||||
let request = TSRequest(url: url, method: "POST", parameters: [:])
|
||||
// Only used for the Loki Public Chat which doesn't require authentication
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Moderators
|
||||
public static func getModerators(for channel: UInt64, on server: String) -> Promise<Set<String>> {
|
||||
return getOpenGroupServerPublicKey(for: server).then(on: DispatchQueue.global(qos: .default)) { serverPublicKey in
|
||||
getAuthToken(for: server).then(on: DispatchQueue.global(qos: .default)) { token -> Promise<Set<String>> in
|
||||
let url = URL(string: "\(server)/loki/v1/channel/\(channel)/get_moderators")!
|
||||
let request = TSRequest(url: url)
|
||||
request.allHTTPHeaderFields = [ "Content-Type" : "application/json", "Authorization" : "Bearer \(token)" ]
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey).map(on: DispatchQueue.global(qos: .default)) { json in
|
||||
guard let moderators = json["moderators"] as? [String] else {
|
||||
SNLog("Couldn't parse moderators for open group channel with ID: \(channel) on server: \(server) from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
let moderatorsAsSet = Set(moderators);
|
||||
if self.moderators.keys.contains(server) {
|
||||
self.moderators[server]![channel] = moderatorsAsSet
|
||||
} else {
|
||||
self.moderators[server] = [ channel : moderatorsAsSet ]
|
||||
}
|
||||
return moderatorsAsSet
|
||||
}
|
||||
}
|
||||
}.handlingInvalidAuthTokenIfNeeded(for: server)
|
||||
}
|
||||
|
||||
@objc(isUserModerator:forChannel:onServer:)
|
||||
public static func isUserModerator(_ hexEncodedPublicString: String, for channel: UInt64, on server: String) -> Bool {
|
||||
return moderators[server]?[channel]?.contains(hexEncodedPublicString) ?? false
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Error Handling
|
||||
internal extension Promise {
|
||||
|
||||
func handlingInvalidAuthTokenIfNeeded(for server: String) -> Promise<T> {
|
||||
return recover(on: DispatchQueue.global(qos: .userInitiated)) { error -> Promise<T> in
|
||||
if case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, _) = error, statusCode == 401 || statusCode == 403 {
|
||||
SNLog("Auth token for: \(server) expired; dropping it.")
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
storage.writeSync { transaction in
|
||||
storage.removeAuthToken(for: server, using: transaction)
|
||||
}
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
|
||||
public struct OpenGroupInfo {
|
||||
public let displayName: String
|
||||
public let profilePictureURL: String?
|
||||
public let memberCount: Int
|
||||
}
|
|
@ -1,101 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(SNOpenGroupManager)
|
||||
public final class OpenGroupManager : NSObject {
|
||||
private var pollers: [String:OpenGroupPoller] = [:]
|
||||
private var isPolling = false
|
||||
|
||||
// MARK: Error
|
||||
public enum Error : LocalizedError {
|
||||
case invalidURL
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .invalidURL: return "Invalid URL."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
@objc public static let shared = OpenGroupManager()
|
||||
|
||||
private override init() { }
|
||||
|
||||
// MARK: Polling
|
||||
@objc public func startPolling() {
|
||||
guard !isPolling else { return }
|
||||
isPolling = true
|
||||
let openGroups = Storage.shared.getAllUserOpenGroups()
|
||||
for (_, openGroup) in openGroups {
|
||||
if let poller = pollers[openGroup.id] { poller.stop() } // Should never occur
|
||||
let poller = OpenGroupPoller(for: openGroup)
|
||||
poller.startIfNeeded()
|
||||
pollers[openGroup.id] = poller
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func stopPolling() {
|
||||
pollers.forEach { (_, openGroupPoller) in openGroupPoller.stop() }
|
||||
pollers.removeAll()
|
||||
}
|
||||
|
||||
// MARK: Adding & Removing
|
||||
public func add(with url: String, using transaction: Any) -> Promise<Void> {
|
||||
let storage = Storage.shared
|
||||
guard let url = URL(string: url), let scheme = url.scheme, scheme == "https", url.host != nil else {
|
||||
return Promise(error: Error.invalidURL)
|
||||
}
|
||||
let channel: UInt64 = 1
|
||||
let server = url.absoluteString
|
||||
let userPublicKey = getUserHexEncodedPublicKey()
|
||||
let name = storage.getUser()?.name
|
||||
let profilePictureURL = storage.getUser()?.profilePictureURL
|
||||
let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData
|
||||
storage.removeLastMessageServerID(for: channel, on: server, using: transaction)
|
||||
storage.removeLastDeletionServerID(for: channel, on: server, using: transaction)
|
||||
return OpenGroupAPI.getInfo(for: channel, on: server).done { info in
|
||||
let openGroup = OpenGroup(channel: channel, server: server, displayName: info.displayName, isDeletable: true)!
|
||||
let groupID = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
let model = TSGroupModel(title: openGroup.displayName, memberIds: [ userPublicKey ], image: nil, groupId: groupID, groupType: .openGroup, adminIds: [])
|
||||
storage.write(with: { transaction in
|
||||
let thread = TSGroupThread.getOrCreateThread(with: model, transaction: transaction as! YapDatabaseReadWriteTransaction)
|
||||
storage.setOpenGroup(openGroup, for: thread.uniqueId!, using: transaction)
|
||||
}, completion: {
|
||||
let _ = OpenGroupAPI.setDisplayName(to: name, on: server)
|
||||
if let profilePictureURL = profilePictureURL, let profileKey = profileKey {
|
||||
let _ = OpenGroupAPI.setProfilePictureURL(to: profilePictureURL, using: profileKey, on: server)
|
||||
}
|
||||
let _ = OpenGroupAPI.join(channel, on: server)
|
||||
if let poller = OpenGroupManager.shared.pollers[openGroup.id] {
|
||||
poller.stop()
|
||||
OpenGroupManager.shared.pollers[openGroup.id] = nil
|
||||
}
|
||||
let poller = OpenGroupPoller(for: openGroup)
|
||||
poller.startIfNeeded()
|
||||
OpenGroupManager.shared.pollers[openGroup.id] = poller
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
public func delete(_ openGroup: OpenGroup, associatedWith thread: TSThread, using transaction: YapDatabaseReadWriteTransaction) {
|
||||
if let poller = pollers[openGroup.id] {
|
||||
poller.stop()
|
||||
pollers[openGroup.id] = nil
|
||||
}
|
||||
var messageIDs: Set<String> = []
|
||||
var messageTimestamps: Set<UInt64> = []
|
||||
thread.enumerateInteractions(with: transaction) { interaction, _ in
|
||||
messageIDs.insert(interaction.uniqueId!)
|
||||
messageTimestamps.insert(interaction.timestamp)
|
||||
}
|
||||
SNMessagingKitConfiguration.shared.storage.updateMessageIDCollectionByPruningMessagesWithIDs(messageIDs, using: transaction)
|
||||
Storage.shared.removeReceivedMessageTimestamps(messageTimestamps, using: transaction)
|
||||
Storage.shared.removeLastMessageServerID(for: openGroup.channel, on: openGroup.server, using: transaction)
|
||||
Storage.shared.removeLastDeletionServerID(for: openGroup.channel, on: openGroup.server, using: transaction)
|
||||
let _ = OpenGroupAPI.leave(openGroup.channel, on: openGroup.server)
|
||||
Storage.shared.removeOpenGroupPublicKey(for: openGroup.server, using: transaction)
|
||||
thread.removeAllThreadInteractions(with: transaction)
|
||||
thread.remove(with: transaction)
|
||||
Storage.shared.removeOpenGroup(for: thread.uniqueId!, using: transaction)
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
|
||||
internal extension OpenGroupMessage {
|
||||
|
||||
static func from(_ message: VisibleMessage, for server: String, using transaction: YapDatabaseReadWriteTransaction) -> OpenGroupMessage? {
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
guard let userPublicKey = storage.getUserPublicKey() else { return nil }
|
||||
var attachmentIDs = message.attachmentIDs
|
||||
// Validation
|
||||
guard message.isValid else { return nil } // Should be valid at this point
|
||||
// Quote
|
||||
let quote: OpenGroupMessage.Quote? = {
|
||||
if let quote = message.quote {
|
||||
guard quote.isValid else { return nil }
|
||||
let quotedMessageBody = quote.text ?? String(quote.timestamp!) // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
if let quotedAttachmentID = quote.attachmentID, let index = attachmentIDs.firstIndex(of: quotedAttachmentID) {
|
||||
attachmentIDs.remove(at: index)
|
||||
}
|
||||
// FIXME: For some reason the server always returns a 500 if quotedMessageServerID is set...
|
||||
return OpenGroupMessage.Quote(quotedMessageTimestamp: quote.timestamp!, quoteePublicKey: quote.publicKey!, quotedMessageBody: quotedMessageBody, quotedMessageServerID: nil)
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}()
|
||||
// Message
|
||||
let name = storage.getUser()?.name ?? "Anonymous"
|
||||
let body = message.text ?? String(message.sentTimestamp!) // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
let result = OpenGroupMessage(serverID: nil, senderPublicKey: userPublicKey, displayName: name, profilePicture: nil, body: body,
|
||||
type: OpenGroupAPI.openGroupMessageType, timestamp: message.sentTimestamp!, quote: quote, attachments: [], signature: nil, serverTimestamp: 0)
|
||||
// Link preview
|
||||
if let linkPreview = message.linkPreview {
|
||||
guard linkPreview.isValid, let attachmentID = linkPreview.attachmentID,
|
||||
let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) as? TSAttachmentStream else { return nil }
|
||||
if let index = attachmentIDs.firstIndex(of: attachmentID) {
|
||||
attachmentIDs.remove(at: index)
|
||||
}
|
||||
let fileName = attachment.sourceFilename ?? UUID().uuidString
|
||||
let width = attachment.shouldHaveImageSize() ? attachment.imageSize().width : 0
|
||||
let height = attachment.shouldHaveImageSize() ? attachment.imageSize().height : 0
|
||||
let openGroupLinkPreview = OpenGroupMessage.Attachment(
|
||||
kind: .linkPreview,
|
||||
server: server,
|
||||
serverID: attachment.serverId,
|
||||
contentType: attachment.contentType,
|
||||
size: UInt(attachment.byteCount),
|
||||
fileName: fileName,
|
||||
flags: 0,
|
||||
width: UInt(width),
|
||||
height: UInt(height),
|
||||
caption: attachment.caption,
|
||||
url: attachment.downloadURL,
|
||||
linkPreviewURL: linkPreview.url,
|
||||
linkPreviewTitle: linkPreview.title
|
||||
)
|
||||
result.attachments.append(openGroupLinkPreview)
|
||||
}
|
||||
// Attachments
|
||||
let attachments: [OpenGroupMessage.Attachment] = attachmentIDs.compactMap { attachmentID in
|
||||
guard let attachment = TSAttachment.fetch(uniqueId: attachmentID, transaction: transaction) as? TSAttachmentStream else { return nil } // Should never occur
|
||||
let fileName = attachment.sourceFilename ?? UUID().uuidString
|
||||
let width = attachment.shouldHaveImageSize() ? attachment.imageSize().width : 0
|
||||
let height = attachment.shouldHaveImageSize() ? attachment.imageSize().height : 0
|
||||
return OpenGroupMessage.Attachment(
|
||||
kind: .attachment,
|
||||
server: server,
|
||||
serverID: attachment.serverId,
|
||||
contentType: attachment.contentType,
|
||||
size: UInt(attachment.byteCount),
|
||||
fileName: fileName,
|
||||
flags: 0,
|
||||
width: UInt(width),
|
||||
height: UInt(height),
|
||||
caption: attachment.caption,
|
||||
url: attachment.downloadURL,
|
||||
linkPreviewURL: nil,
|
||||
linkPreviewTitle: nil
|
||||
)
|
||||
}
|
||||
result.attachments += attachments
|
||||
// Return
|
||||
return result
|
||||
}
|
||||
}
|
|
@ -1,196 +0,0 @@
|
|||
import PromiseKit
|
||||
import Curve25519Kit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
@objc(SNOpenGroupMessage)
|
||||
public final class OpenGroupMessage : NSObject {
|
||||
public let serverID: UInt64?
|
||||
public let senderPublicKey: String
|
||||
public let displayName: String
|
||||
public let profilePicture: ProfilePicture?
|
||||
public let body: String
|
||||
/// - Note: Expressed as milliseconds since 00:00:00 UTC on 1 January 1970.
|
||||
public let timestamp: UInt64
|
||||
public let type: String
|
||||
public let quote: Quote?
|
||||
public var attachments: [Attachment] = []
|
||||
public let signature: Signature?
|
||||
/// - Note: Used for sorting.
|
||||
public let serverTimestamp: UInt64
|
||||
|
||||
@objc(serverID)
|
||||
public var objc_serverID: UInt64 { return serverID ?? 0 }
|
||||
|
||||
// MARK: Settings
|
||||
private let signatureVersion: UInt64 = 1
|
||||
private let attachmentType = "net.app.core.oembed"
|
||||
|
||||
// MARK: Types
|
||||
public struct ProfilePicture {
|
||||
public let profileKey: Data
|
||||
public let url: String
|
||||
}
|
||||
|
||||
public struct Quote {
|
||||
public let quotedMessageTimestamp: UInt64
|
||||
public let quoteePublicKey: String
|
||||
public let quotedMessageBody: String
|
||||
public let quotedMessageServerID: UInt64?
|
||||
}
|
||||
|
||||
public struct Attachment {
|
||||
public let kind: Kind
|
||||
public let server: String
|
||||
public let serverID: UInt64
|
||||
public let contentType: String
|
||||
public let size: UInt
|
||||
public let fileName: String
|
||||
public let flags: UInt
|
||||
public let width: UInt
|
||||
public let height: UInt
|
||||
public let caption: String?
|
||||
public let url: String
|
||||
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
|
||||
public let linkPreviewURL: String?
|
||||
/// Guaranteed to be non-`nil` if `kind` is `linkPreview`
|
||||
public let linkPreviewTitle: String?
|
||||
|
||||
public enum Kind : String { case attachment, linkPreview = "preview" }
|
||||
|
||||
public var dotNETType: String {
|
||||
if contentType.hasPrefix("image") {
|
||||
return "photo"
|
||||
} else if contentType.hasPrefix("video") {
|
||||
return "video"
|
||||
} else if contentType.hasPrefix("audio") {
|
||||
return "audio"
|
||||
} else {
|
||||
return "other"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct Signature {
|
||||
public let data: Data
|
||||
public let version: UInt64
|
||||
}
|
||||
|
||||
// MARK: Initialization
|
||||
public init(serverID: UInt64?, senderPublicKey: String, displayName: String, profilePicture: ProfilePicture?, body: String,
|
||||
type: String, timestamp: UInt64, quote: Quote?, attachments: [Attachment], signature: Signature?, serverTimestamp: UInt64) {
|
||||
self.serverID = serverID
|
||||
self.senderPublicKey = senderPublicKey
|
||||
self.displayName = displayName
|
||||
self.profilePicture = profilePicture
|
||||
self.body = body
|
||||
self.type = type
|
||||
self.timestamp = timestamp
|
||||
self.quote = quote
|
||||
self.attachments = attachments
|
||||
self.signature = signature
|
||||
self.serverTimestamp = serverTimestamp
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc public convenience init(senderPublicKey: String, displayName: String, body: String, type: String, timestamp: UInt64,
|
||||
quotedMessageTimestamp: UInt64, quoteePublicKey: String?, quotedMessageBody: String, quotedMessageServerID: UInt64,
|
||||
signatureData: Data?, signatureVersion: UInt64, serverTimestamp: UInt64) {
|
||||
let quote: Quote?
|
||||
if quotedMessageTimestamp != 0, let quoteeHexEncodedPublicKey = quoteePublicKey {
|
||||
let quotedMessageServerID = (quotedMessageServerID != 0) ? quotedMessageServerID : nil
|
||||
quote = Quote(quotedMessageTimestamp: quotedMessageTimestamp, quoteePublicKey: quoteeHexEncodedPublicKey, quotedMessageBody: quotedMessageBody, quotedMessageServerID: quotedMessageServerID)
|
||||
} else {
|
||||
quote = nil
|
||||
}
|
||||
let signature: Signature?
|
||||
if let signatureData = signatureData, signatureVersion != 0 {
|
||||
signature = Signature(data: signatureData, version: signatureVersion)
|
||||
} else {
|
||||
signature = nil
|
||||
}
|
||||
self.init(serverID: nil, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: nil, body: body, type: type, timestamp: timestamp, quote: quote, attachments: [], signature: signature, serverTimestamp: serverTimestamp)
|
||||
}
|
||||
|
||||
// MARK: Crypto
|
||||
internal func sign(with privateKey: Data) -> OpenGroupMessage? {
|
||||
guard let data = getValidationData(for: signatureVersion) else {
|
||||
SNLog("Failed to sign open group message.")
|
||||
return nil
|
||||
}
|
||||
let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair()!
|
||||
guard let signatureData = try? Ed25519.sign(data, with: userKeyPair) else {
|
||||
SNLog("Failed to sign open group message.")
|
||||
return nil
|
||||
}
|
||||
let signature = Signature(data: signatureData, version: signatureVersion)
|
||||
return OpenGroupMessage(serverID: serverID, senderPublicKey: senderPublicKey, displayName: displayName, profilePicture: profilePicture, body: body, type: type, timestamp: timestamp, quote: quote, attachments: attachments, signature: signature, serverTimestamp: serverTimestamp)
|
||||
}
|
||||
|
||||
internal func hasValidSignature() -> Bool {
|
||||
guard let signature = signature else { return false }
|
||||
guard let data = getValidationData(for: signature.version) else { return false }
|
||||
let publicKey = Data(hex: self.senderPublicKey.removing05PrefixIfNeeded())
|
||||
return (try? Ed25519.verifySignature(signature.data, publicKey: publicKey, data: data)) ?? false
|
||||
}
|
||||
|
||||
// MARK: JSON
|
||||
internal func toJSON() -> JSON {
|
||||
var value: JSON = [ "timestamp" : timestamp ]
|
||||
if let quote = quote {
|
||||
let quoteAsJSON: JSON = [ "id" : quote.quotedMessageTimestamp, "author" : quote.quoteePublicKey, "text" : quote.quotedMessageBody ]
|
||||
value["quote"] = quoteAsJSON
|
||||
}
|
||||
if let signature = signature {
|
||||
value["sig"] = signature.data.toHexString()
|
||||
value["sigver"] = signature.version
|
||||
}
|
||||
if let profilePicture = profilePicture {
|
||||
value["avatar"] = profilePicture;
|
||||
}
|
||||
let annotation: JSON = [ "type" : type, "value" : value ]
|
||||
let attachmentAnnotations: [JSON] = attachments.map { attachment in
|
||||
var attachmentValue: JSON = [
|
||||
// Fields required by the .NET API
|
||||
"version" : 1, "type" : attachment.dotNETType,
|
||||
// Custom fields
|
||||
"lokiType" : attachment.kind.rawValue, "server" : attachment.server, "id" : attachment.serverID, "contentType" : attachment.contentType, "size" : attachment.size, "fileName" : attachment.fileName, "width" : attachment.width, "height" : attachment.height, "url" : attachment.url
|
||||
]
|
||||
if let caption = attachment.caption {
|
||||
attachmentValue["caption"] = caption
|
||||
}
|
||||
if let linkPreviewURL = attachment.linkPreviewURL {
|
||||
attachmentValue["linkPreviewUrl"] = linkPreviewURL
|
||||
}
|
||||
if let linkPreviewTitle = attachment.linkPreviewTitle {
|
||||
attachmentValue["linkPreviewTitle"] = linkPreviewTitle
|
||||
}
|
||||
return [ "type" : attachmentType, "value" : attachmentValue ]
|
||||
}
|
||||
var result: JSON = [ "text" : body, "annotations": [ annotation ] + attachmentAnnotations ]
|
||||
if let quotedMessageServerID = quote?.quotedMessageServerID {
|
||||
result["reply_to"] = quotedMessageServerID
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// MARK: Convenience
|
||||
@objc public func addAttachment(kind: String, server: String, serverID: UInt64, contentType: String, size: UInt,
|
||||
fileName: String, flags: UInt, width: UInt, height: UInt, caption: String?, url: String, linkPreviewURL: String?, linkPreviewTitle: String?) {
|
||||
guard let kind = Attachment.Kind(rawValue: kind) else { preconditionFailure() }
|
||||
let attachment = Attachment(kind: kind, server: server, serverID: serverID, contentType: contentType, size: size, fileName: fileName, flags: flags, width: width, height: height, caption: caption, url: url, linkPreviewURL: linkPreviewURL, linkPreviewTitle: linkPreviewTitle)
|
||||
attachments.append(attachment)
|
||||
}
|
||||
|
||||
private func getValidationData(for signatureVersion: UInt64) -> Data? {
|
||||
var string = "\(body.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))\(timestamp)"
|
||||
if let quote = quote {
|
||||
string += "\(quote.quotedMessageTimestamp)\(quote.quoteePublicKey)\(quote.quotedMessageBody.trimmingCharacters(in: CharacterSet.whitespacesAndNewlines))"
|
||||
if let quotedMessageServerID = quote.quotedMessageServerID {
|
||||
string += "\(quotedMessageServerID)"
|
||||
}
|
||||
}
|
||||
string += attachments.sorted { $0.serverID < $1.serverID }.map { "\($0.serverID)" }.joined(separator: "")
|
||||
string += "\(signatureVersion)"
|
||||
return string.data(using: String.Encoding.utf8)
|
||||
}
|
||||
}
|
|
@ -30,11 +30,10 @@ public final class MentionsManager : NSObject {
|
|||
guard let cache = userPublicKeyCache[threadID] else { return [] }
|
||||
var candidates: [Mention] = []
|
||||
// Gather candidates
|
||||
let openGroup = Storage.shared.getOpenGroup(for: threadID)
|
||||
let openGroupV2 = Storage.shared.getV2OpenGroup(for: threadID)
|
||||
storage.dbReadConnection.read { transaction in
|
||||
candidates = cache.compactMap { publicKey in
|
||||
let context: Contact.Context = (openGroupV2 != nil || openGroup != nil) ? .openGroup : .regular
|
||||
let context: Contact.Context = (openGroupV2 != nil) ? .openGroup : .regular
|
||||
let displayNameOrNil = Storage.shared.getContact(with: publicKey)?.displayName(for: context)
|
||||
guard let displayName = displayNameOrNil else { return nil }
|
||||
guard !displayName.hasPrefix("Anonymous") else { return nil }
|
||||
|
|
|
@ -216,8 +216,6 @@ extension MessageReceiver {
|
|||
for openGroupURL in message.openGroups {
|
||||
if let (room, server, publicKey) = OpenGroupManagerV2.parseV2OpenGroup(from: openGroupURL) {
|
||||
OpenGroupManagerV2.shared.add(room: room, server: server, publicKey: publicKey, using: transaction).retainUntilComplete()
|
||||
} else {
|
||||
OpenGroupManager.shared.add(with: openGroupURL, using: transaction).retainUntilComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -244,16 +242,10 @@ extension MessageReceiver {
|
|||
message.attachmentIDs = attachmentIDs
|
||||
var attachmentsToDownload = attachmentIDs
|
||||
// Update profile if needed
|
||||
if let newProfile = message.profile {
|
||||
if let profile = message.profile {
|
||||
let sessionID = message.sender!
|
||||
updateProfileIfNeeded(publicKey: sessionID, name: newProfile.displayName, profilePictureURL: newProfile.profilePictureURL,
|
||||
profileKey: given(newProfile.profileKey) { OWSAES256Key(data: $0)! }, sentTimestamp: message.sentTimestamp!, transaction: transaction)
|
||||
if let rawDisplayName = newProfile.displayName, let openGroupID = openGroupID {
|
||||
let endIndex = sessionID.endIndex
|
||||
let cutoffIndex = sessionID.index(endIndex, offsetBy: -8)
|
||||
let displayName = "\(rawDisplayName) (...\(sessionID[cutoffIndex..<endIndex]))"
|
||||
Storage.shared.setOpenGroupDisplayName(to: displayName, for: sessionID, inOpenGroupWithID: openGroupID, using: transaction)
|
||||
}
|
||||
updateProfileIfNeeded(publicKey: sessionID, name: profile.displayName, profilePictureURL: profile.profilePictureURL,
|
||||
profileKey: given(profile.profileKey) { OWSAES256Key(data: $0)! }, sentTimestamp: message.sentTimestamp!, transaction: transaction)
|
||||
}
|
||||
// Get or create thread
|
||||
guard let threadID = storage.getOrCreateThread(for: message.syncTarget ?? message.sender!, groupPublicKey: message.groupPublicKey, openGroupID: openGroupID, using: transaction) else { throw Error.noThread }
|
||||
|
|
|
@ -281,67 +281,41 @@ public final class MessageSender : NSObject {
|
|||
#endif
|
||||
}
|
||||
guard message.isValid else { handleFailure(with: Error.invalidMessage, using: transaction); return promise }
|
||||
// There's quite a bit of overlap between the two clauses of this if statement for now, but that'll be fixed
|
||||
// when we remove support for V1 open groups
|
||||
if case .openGroup(let channel, let server) = destination {
|
||||
// The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
if message.text?.isEmpty != false {
|
||||
message.text = String(message.sentTimestamp!)
|
||||
}
|
||||
// Convert the message to an open group message
|
||||
guard let openGroupMessage = OpenGroupMessage.from(message, for: server, using: transaction) else { handleFailure(with: Error.invalidMessage, using: transaction); return promise }
|
||||
// Send the result
|
||||
OpenGroupAPI.sendMessage(openGroupMessage, to: channel, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
|
||||
message.openGroupServerMessageID = openGroupMessage.serverID
|
||||
storage.write(with: { transaction in
|
||||
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
||||
seal.fulfill(())
|
||||
}, completion: { })
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
storage.write(with: { transaction in
|
||||
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}, completion: { })
|
||||
}
|
||||
// Return
|
||||
return promise
|
||||
} else if case .openGroupV2(let room, let server) = destination {
|
||||
// Attach the user's profile
|
||||
guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise }
|
||||
if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
|
||||
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
||||
} else {
|
||||
message.profile = VisibleMessage.Profile(displayName: name)
|
||||
}
|
||||
// Convert it to protobuf
|
||||
guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise }
|
||||
// Serialize the protobuf
|
||||
let plaintext: Data
|
||||
do {
|
||||
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
|
||||
} catch {
|
||||
SNLog("Couldn't serialize proto due to error: \(error).")
|
||||
handleFailure(with: error, using: transaction)
|
||||
return promise
|
||||
}
|
||||
// Send the result
|
||||
let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!,
|
||||
base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil)
|
||||
OpenGroupAPIV2.send(openGroupMessage, to: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
|
||||
message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) }
|
||||
storage.write(with: { transaction in
|
||||
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
||||
seal.fulfill(())
|
||||
}, completion: { })
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
storage.write(with: { transaction in
|
||||
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}, completion: { })
|
||||
}
|
||||
// Return
|
||||
return promise
|
||||
// Attach the user's profile
|
||||
guard let name = storage.getUser()?.name else { handleFailure(with: Error.noUsername, using: transaction); return promise }
|
||||
if let profileKey = storage.getUser()?.profilePictureEncryptionKey?.keyData, let profilePictureURL = storage.getUser()?.profilePictureURL {
|
||||
message.profile = VisibleMessage.Profile(displayName: name, profileKey: profileKey, profilePictureURL: profilePictureURL)
|
||||
} else {
|
||||
preconditionFailure()
|
||||
message.profile = VisibleMessage.Profile(displayName: name)
|
||||
}
|
||||
// Convert it to protobuf
|
||||
guard let proto = message.toProto(using: transaction) else { handleFailure(with: Error.protoConversionFailed, using: transaction); return promise }
|
||||
// Serialize the protobuf
|
||||
let plaintext: Data
|
||||
do {
|
||||
plaintext = (try proto.serializedData() as NSData).paddedMessageBody()
|
||||
} catch {
|
||||
SNLog("Couldn't serialize proto due to error: \(error).")
|
||||
handleFailure(with: error, using: transaction)
|
||||
return promise
|
||||
}
|
||||
// Send the result
|
||||
guard case .openGroupV2(let room, let server) = destination else { preconditionFailure() }
|
||||
let openGroupMessage = OpenGroupMessageV2(serverID: nil, sender: nil, sentTimestamp: message.sentTimestamp!,
|
||||
base64EncodedData: plaintext.base64EncodedString(), base64EncodedSignature: nil)
|
||||
OpenGroupAPIV2.send(openGroupMessage, to: room, on: server).done(on: DispatchQueue.global(qos: .userInitiated)) { openGroupMessage in
|
||||
message.openGroupServerMessageID = given(openGroupMessage.serverID) { UInt64($0) }
|
||||
storage.write(with: { transaction in
|
||||
MessageSender.handleSuccessfulMessageSend(message, to: destination, using: transaction)
|
||||
seal.fulfill(())
|
||||
}, completion: { })
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
storage.write(with: { transaction in
|
||||
handleFailure(with: error, using: transaction as! YapDatabaseReadWriteTransaction)
|
||||
}, completion: { })
|
||||
}
|
||||
// Return
|
||||
return promise
|
||||
}
|
||||
|
||||
// MARK: Success & Failure Handling
|
||||
|
|
|
@ -1,198 +0,0 @@
|
|||
import PromiseKit
|
||||
|
||||
@objc(LKOpenGroupPoller)
|
||||
public final class OpenGroupPoller : NSObject {
|
||||
private let openGroup: OpenGroup
|
||||
private var pollForNewMessagesTimer: Timer? = nil
|
||||
private var pollForDeletedMessagesTimer: Timer? = nil
|
||||
private var pollForModeratorsTimer: Timer? = nil
|
||||
private var hasStarted = false
|
||||
private var isPolling = false
|
||||
|
||||
private var isMainAppAndActive: Bool {
|
||||
var isMainAppAndActive = false
|
||||
if let sharedUserDefaults = UserDefaults(suiteName: "group.com.loki-project.loki-messenger") {
|
||||
isMainAppAndActive = sharedUserDefaults.bool(forKey: "isMainAppActive")
|
||||
}
|
||||
return isMainAppAndActive
|
||||
}
|
||||
|
||||
// MARK: Settings
|
||||
private let pollForNewMessagesInterval: TimeInterval = 20
|
||||
private let pollForDeletedMessagesInterval: TimeInterval = 30
|
||||
private let pollForModeratorsInterval: TimeInterval = 10 * 60
|
||||
|
||||
// MARK: Lifecycle
|
||||
@objc(initForOpenGroup:)
|
||||
public init(for openGroup: OpenGroup) {
|
||||
self.openGroup = openGroup
|
||||
super.init()
|
||||
}
|
||||
|
||||
@objc public func startIfNeeded() {
|
||||
guard !hasStarted else { return }
|
||||
guard isMainAppAndActive else { stop(); return }
|
||||
DispatchQueue.main.async { [weak self] in // Timers don't do well on background queues
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.pollForNewMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForNewMessagesInterval, repeats: true) { _ in self?.pollForNewMessages() }
|
||||
strongSelf.pollForDeletedMessagesTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForDeletedMessagesInterval, repeats: true) { _ in self?.pollForDeletedMessages() }
|
||||
strongSelf.pollForModeratorsTimer = Timer.scheduledTimer(withTimeInterval: strongSelf.pollForModeratorsInterval, repeats: true) { _ in self?.pollForModerators() }
|
||||
// Perform initial updates
|
||||
strongSelf.pollForNewMessages()
|
||||
strongSelf.pollForDeletedMessages()
|
||||
strongSelf.pollForModerators()
|
||||
strongSelf.hasStarted = true
|
||||
}
|
||||
}
|
||||
|
||||
@objc public func stop() {
|
||||
pollForNewMessagesTimer?.invalidate()
|
||||
pollForDeletedMessagesTimer?.invalidate()
|
||||
pollForModeratorsTimer?.invalidate()
|
||||
hasStarted = false
|
||||
}
|
||||
|
||||
// MARK: Polling
|
||||
@objc(pollForNewMessages)
|
||||
public func objc_pollForNewMessages() -> AnyPromise {
|
||||
AnyPromise.from(pollForNewMessages())
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func pollForNewMessages() -> Promise<Void> {
|
||||
guard isMainAppAndActive else { stop(); return Promise.value(()) }
|
||||
return pollForNewMessages(isBackgroundPoll: false)
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
public func pollForNewMessages(isBackgroundPoll: Bool) -> Promise<Void> {
|
||||
guard !self.isPolling else { return Promise.value(()) }
|
||||
self.isPolling = true
|
||||
let openGroup = self.openGroup
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
promise.retainUntilComplete()
|
||||
OpenGroupAPI.getMessages(for: openGroup.channel, on: openGroup.server).done(on: DispatchQueue.global(qos: .default)) { messages in
|
||||
self.isPolling = false
|
||||
// Sorting the messages by timestamp before importing them fixes an issue where messages that quote older messages can't find those older messages
|
||||
messages.sorted { $0.serverTimestamp < $1.serverTimestamp }.forEach { message in
|
||||
let senderPublicKey = message.senderPublicKey
|
||||
let wasSentByCurrentUser = (senderPublicKey == getUserHexEncodedPublicKey())
|
||||
func generateDisplayName(from rawDisplayName: String) -> String {
|
||||
let endIndex = senderPublicKey.endIndex
|
||||
let cutoffIndex = senderPublicKey.index(endIndex, offsetBy: -8)
|
||||
return "\(rawDisplayName) (...\(senderPublicKey[cutoffIndex..<endIndex]))"
|
||||
}
|
||||
let senderDisplayName = Storage.shared.getContact(with: senderPublicKey)?.displayName(for: .openGroup)
|
||||
?? generateDisplayName(from: NSLocalizedString("Anonymous", comment: ""))
|
||||
let id = LKGroupUtilities.getEncodedOpenGroupIDAsData(openGroup.id)
|
||||
// Main message
|
||||
let dataMessageProto = SNProtoDataMessage.builder()
|
||||
let body = (message.body == message.timestamp.description) ? "" : message.body // The back-end doesn't accept messages without a body so we use this as a workaround
|
||||
dataMessageProto.setBody(body)
|
||||
dataMessageProto.setTimestamp(message.timestamp)
|
||||
// Attachments
|
||||
let attachmentProtos: [SNProtoAttachmentPointer] = message.attachments.compactMap { attachment in
|
||||
guard attachment.kind == .attachment else { return nil }
|
||||
let attachmentProto = SNProtoAttachmentPointer.builder(id: attachment.serverID)
|
||||
attachmentProto.setContentType(attachment.contentType)
|
||||
attachmentProto.setSize(UInt32(attachment.size))
|
||||
attachmentProto.setFileName(attachment.fileName)
|
||||
attachmentProto.setFlags(UInt32(attachment.flags))
|
||||
attachmentProto.setWidth(UInt32(attachment.width))
|
||||
attachmentProto.setHeight(UInt32(attachment.height))
|
||||
if let caption = attachment.caption { attachmentProto.setCaption(caption) }
|
||||
attachmentProto.setUrl(attachment.url)
|
||||
return try! attachmentProto.build()
|
||||
}
|
||||
dataMessageProto.setAttachments(attachmentProtos)
|
||||
// Link preview
|
||||
if let linkPreview = message.attachments.first(where: { $0.kind == .linkPreview }) {
|
||||
let linkPreviewProto = SNProtoDataMessagePreview.builder(url: linkPreview.linkPreviewURL!)
|
||||
linkPreviewProto.setTitle(linkPreview.linkPreviewTitle!)
|
||||
let attachmentProto = SNProtoAttachmentPointer.builder(id: linkPreview.serverID)
|
||||
attachmentProto.setContentType(linkPreview.contentType)
|
||||
attachmentProto.setSize(UInt32(linkPreview.size))
|
||||
attachmentProto.setFileName(linkPreview.fileName)
|
||||
attachmentProto.setFlags(UInt32(linkPreview.flags))
|
||||
attachmentProto.setWidth(UInt32(linkPreview.width))
|
||||
attachmentProto.setHeight(UInt32(linkPreview.height))
|
||||
if let caption = linkPreview.caption { attachmentProto.setCaption(caption) }
|
||||
attachmentProto.setUrl(linkPreview.url)
|
||||
linkPreviewProto.setImage(try! attachmentProto.build())
|
||||
dataMessageProto.setPreview([ try! linkPreviewProto.build() ])
|
||||
}
|
||||
// Quote
|
||||
if let quote = message.quote {
|
||||
let quoteProto = SNProtoDataMessageQuote.builder(id: quote.quotedMessageTimestamp, author: quote.quoteePublicKey)
|
||||
if quote.quotedMessageBody != String(quote.quotedMessageTimestamp) { quoteProto.setText(quote.quotedMessageBody) }
|
||||
dataMessageProto.setQuote(try! quoteProto.build())
|
||||
}
|
||||
// Profile
|
||||
let profileProto = SNProtoDataMessageLokiProfile.builder()
|
||||
profileProto.setDisplayName(message.displayName)
|
||||
if let profilePicture = message.profilePicture {
|
||||
profileProto.setProfilePicture(profilePicture.url)
|
||||
dataMessageProto.setProfileKey(profilePicture.profileKey)
|
||||
}
|
||||
dataMessageProto.setProfile(try! profileProto.build())
|
||||
// Signal group context
|
||||
let groupProto = SNProtoGroupContext.builder(id: id, type: .deliver)
|
||||
groupProto.setName(openGroup.displayName)
|
||||
dataMessageProto.setGroup(try! groupProto.build())
|
||||
// Sync target
|
||||
if wasSentByCurrentUser {
|
||||
dataMessageProto.setSyncTarget(openGroup.id)
|
||||
}
|
||||
// Content
|
||||
let content = SNProtoContent.builder()
|
||||
content.setDataMessage(try! dataMessageProto.build())
|
||||
// Envelope
|
||||
let envelope = SNProtoEnvelope.builder(type: .sessionMessage, timestamp: message.timestamp)
|
||||
envelope.setSource(senderPublicKey)
|
||||
envelope.setSourceDevice(1)
|
||||
envelope.setContent(try! content.build().serializedData())
|
||||
envelope.setServerTimestamp(message.serverTimestamp)
|
||||
SNMessagingKitConfiguration.shared.storage.write { transaction in
|
||||
Storage.shared.setOpenGroupDisplayName(to: senderDisplayName, for: senderPublicKey, inOpenGroupWithID: openGroup.id, using: transaction)
|
||||
let messageServerID = message.serverID
|
||||
let job = MessageReceiveJob(data: try! envelope.buildSerializedData(), openGroupMessageServerID: messageServerID, openGroupID: openGroup.id, isBackgroundPoll: isBackgroundPoll)
|
||||
if isBackgroundPoll {
|
||||
job.execute().done(on: DispatchQueue.global(qos: .userInitiated)) {
|
||||
seal.fulfill(())
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { _ in
|
||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||
}
|
||||
} else {
|
||||
SessionMessagingKit.JobQueue.shared.add(job, using: transaction)
|
||||
seal.fulfill(())
|
||||
}
|
||||
}
|
||||
}
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { _ in
|
||||
seal.fulfill(()) // The promise is just used to keep track of when we're done
|
||||
}.retainUntilComplete()
|
||||
return promise
|
||||
}
|
||||
|
||||
private func pollForDeletedMessages() {
|
||||
let openGroup = self.openGroup
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
OpenGroupAPI.getDeletedMessageServerIDs(for: openGroup.channel, on: openGroup.server).done(on: DispatchQueue.global(qos: .default)) { deletedMessageServerIDs in
|
||||
storage.write { transaction in
|
||||
let transaction = transaction as! YapDatabaseReadWriteTransaction
|
||||
guard let threadID = storage.getThreadID(for: openGroup.id),
|
||||
let thread = TSGroupThread.fetch(uniqueId: threadID, transaction: transaction) else { return }
|
||||
var messagesToRemove: [TSMessage] = []
|
||||
thread.enumerateInteractions(with: transaction) { interaction, stop in
|
||||
guard let message = interaction as? TSMessage, deletedMessageServerIDs.contains(message.openGroupServerMessageID) else { return }
|
||||
messagesToRemove.append(message)
|
||||
}
|
||||
messagesToRemove.forEach { $0.remove(with: transaction) }
|
||||
}
|
||||
}.retainUntilComplete()
|
||||
}
|
||||
|
||||
private func pollForModerators() {
|
||||
OpenGroupAPI.getModerators(for: openGroup.channel, on: openGroup.server).retainUntilComplete()
|
||||
}
|
||||
}
|
|
@ -48,7 +48,6 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func getAllV2OpenGroups() -> [String:OpenGroupV2]
|
||||
func getV2OpenGroup(for threadID: String) -> OpenGroupV2?
|
||||
func v2GetThreadID(for v2OpenGroupID: String) -> String?
|
||||
func getThreadID(for openGroupID: String) -> String?
|
||||
func updateMessageIDCollectionByPruningMessagesWithIDs(_ messageIDs: Set<String>, using transaction: Any)
|
||||
|
||||
// MARK: - Open Group Public Keys
|
||||
|
@ -71,8 +70,6 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
// MARK: - Open Group Metadata
|
||||
|
||||
func setUserCount(to newValue: UInt64, forV2OpenGroupWithID openGroupID: String, using transaction: Any)
|
||||
func setOpenGroupDisplayName(to displayName: String, for publicKey: String, inOpenGroupWithID openGroupID: String, using transaction: Any)
|
||||
func setLastProfilePictureUploadDate(_ date: Date) // Stored in user defaults so no transaction is needed
|
||||
|
||||
// MARK: - Message Handling
|
||||
|
||||
|
@ -88,23 +85,4 @@ public protocol SessionMessagingKitStorageProtocol {
|
|||
func setAttachmentState(to state: TSAttachmentPointerState, for pointer: TSAttachmentPointer, associatedWith tsIncomingMessageID: String, using transaction: Any)
|
||||
/// Also touches the associated message.
|
||||
func persist(_ stream: TSAttachmentStream, associatedWith tsIncomingMessageID: String, using transaction: Any)
|
||||
|
||||
// MARK: - Deprecated
|
||||
|
||||
func getAuthToken(for server: String) -> String?
|
||||
func setAuthToken(for server: String, to newValue: String, using transaction: Any)
|
||||
func removeAuthToken(for server: String, using transaction: Any)
|
||||
|
||||
func getLastMessageServerID(for group: UInt64, on server: String) -> UInt64?
|
||||
func setLastMessageServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
|
||||
func removeLastMessageServerID(for group: UInt64, on server: String, using transaction: Any)
|
||||
|
||||
func getLastDeletionServerID(for group: UInt64, on server: String) -> UInt64?
|
||||
func setLastDeletionServerID(for group: UInt64, on server: String, to newValue: UInt64, using transaction: Any)
|
||||
func removeLastDeletionServerID(for group: UInt64, on server: String, using transaction: Any)
|
||||
|
||||
func getAllUserOpenGroups() -> [String:OpenGroup]
|
||||
func getOpenGroup(for threadID: String) -> OpenGroup?
|
||||
|
||||
func setUserCount(to newValue: Int, forOpenGroupWithID openGroupID: String, using transaction: Any)
|
||||
}
|
||||
|
|
|
@ -1,226 +0,0 @@
|
|||
import AFNetworking
|
||||
import CryptoSwift
|
||||
import PromiseKit
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
import SignalCoreKit
|
||||
|
||||
/// Base class for `FileServerAPI` and `OpenGroupAPI`.
|
||||
public class DotNetAPI : NSObject {
|
||||
|
||||
// MARK: Settings
|
||||
private static let attachmentType = "network.loki"
|
||||
private static let maxRetryCount: UInt = 4
|
||||
|
||||
// MARK: Error
|
||||
public enum Error : LocalizedError {
|
||||
case generic
|
||||
case invalidURL
|
||||
case parsingFailed
|
||||
case signingFailed
|
||||
case encryptionFailed
|
||||
case decryptionFailed
|
||||
case maxFileSizeExceeded
|
||||
|
||||
internal var isRetryable: Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .generic: return "An error occurred."
|
||||
case .invalidURL: return "Invalid URL."
|
||||
case .parsingFailed: return "Invalid file server response."
|
||||
case .signingFailed: return "Couldn't sign message."
|
||||
case .encryptionFailed: return "Couldn't encrypt file."
|
||||
case .decryptionFailed: return "Couldn't decrypt file."
|
||||
case .maxFileSizeExceeded: return "Maximum file size exceeded."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Lifecycle
|
||||
override private init() { }
|
||||
|
||||
// MARK: Private API
|
||||
private static func requestNewAuthToken(for server: String) -> Promise<String> {
|
||||
SNLog("Requesting auth token for server: \(server).")
|
||||
guard let userKeyPair = SNMessagingKitConfiguration.shared.storage.getUserKeyPair() else { return Promise(error: Error.generic) }
|
||||
let queryParameters = "pubKey=\(userKeyPair.publicKey.toHexString())"
|
||||
let url = URL(string: "\(server)/loki/v1/get_challenge?\(queryParameters)")!
|
||||
let request = TSRequest(url: url)
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let base64EncodedChallenge = json["cipherText64"] as? String, let base64EncodedServerPublicKey = json["serverPubKey64"] as? String,
|
||||
let challenge = Data(base64Encoded: base64EncodedChallenge), var serverPublicKey = Data(base64Encoded: base64EncodedServerPublicKey) else {
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
// Discard the "05" prefix if needed
|
||||
if serverPublicKey.count == 33 {
|
||||
let hexEncodedServerPublicKey = serverPublicKey.toHexString()
|
||||
let startIndex = hexEncodedServerPublicKey.index(hexEncodedServerPublicKey.startIndex, offsetBy: 2)
|
||||
serverPublicKey = Data(hex: String(hexEncodedServerPublicKey[startIndex..<hexEncodedServerPublicKey.endIndex]))
|
||||
}
|
||||
// The challenge is prefixed by the 16 bit IV
|
||||
guard let tokenAsData = try? DiffieHellman.decrypt(challenge, publicKey: serverPublicKey, privateKey: userKeyPair.privateKey),
|
||||
let token = String(bytes: tokenAsData, encoding: .utf8) else {
|
||||
throw Error.decryptionFailed
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
|
||||
private static func submitAuthToken(_ token: String, for server: String) -> Promise<String> {
|
||||
SNLog("Submitting auth token for server: \(server).")
|
||||
let url = URL(string: "\(server)/loki/v1/submit_challenge")!
|
||||
guard let userPublicKey = SNMessagingKitConfiguration.shared.storage.getUserPublicKey() else { return Promise(error: Error.generic) }
|
||||
let parameters = [ "pubKey" : userPublicKey, "token" : token ]
|
||||
let request = TSRequest(url: url, method: "POST", parameters: parameters)
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
return serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.map(on: DispatchQueue.global(qos: .userInitiated)) { _ in token }
|
||||
}
|
||||
|
||||
// MARK: Public API
|
||||
public static func getAuthToken(for server: String) -> Promise<String> {
|
||||
let storage = SNMessagingKitConfiguration.shared.storage
|
||||
if let token = storage.getAuthToken(for: server) {
|
||||
return Promise.value(token)
|
||||
} else {
|
||||
return requestNewAuthToken(for: server).then(on: DispatchQueue.global(qos: .userInitiated)) { submitAuthToken($0, for: server) }.map(on: DispatchQueue.global(qos: .userInitiated)) { token in
|
||||
storage.writeSync { transaction in
|
||||
storage.setAuthToken(for: server, to: token, using: transaction)
|
||||
}
|
||||
return token
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(downloadAttachmentFrom:)
|
||||
public static func objc_downloadAttachment(from url: String) -> AnyPromise {
|
||||
return AnyPromise.from(downloadAttachment(from: url))
|
||||
}
|
||||
|
||||
public static func downloadAttachment(from urlAsString: String) -> Promise<Data> {
|
||||
guard let url = URL(string: urlAsString) else { return Promise(error: Error.invalidURL) }
|
||||
var host = "https://\(url.host!)"
|
||||
let sanitizedURL: String
|
||||
if FileServerAPI.fileStorageBucketURL.contains(host) {
|
||||
sanitizedURL = urlAsString.replacingOccurrences(of: FileServerAPI.fileStorageBucketURL, with: "\(FileServerAPI.server)/loki/v1")
|
||||
host = FileServerAPI.server
|
||||
} else {
|
||||
sanitizedURL = urlAsString.replacingOccurrences(of: host, with: "\(host)/loki/v1")
|
||||
}
|
||||
let request: NSMutableURLRequest
|
||||
do {
|
||||
request = try AFHTTPRequestSerializer().request(withMethod: "GET", urlString: sanitizedURL, parameters: nil)
|
||||
} catch {
|
||||
SNLog("Couldn't download attachment due to error: \(error).")
|
||||
return Promise(error: error)
|
||||
}
|
||||
let serverPublicKeyPromise = FileServerAPI.server.contains(host) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: host)
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) {
|
||||
serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
return OnionRequestAPI.sendOnionRequest(request, to: host, using: serverPublicKey, isJSONRequired: false).map(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
guard let body = json["result"] as? String, let data = Data(base64Encoded: body) else {
|
||||
SNLog("Couldn't parse attachment from: \(json).")
|
||||
throw Error.parsingFailed
|
||||
}
|
||||
return data
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@objc(uploadAttachment:withID:toServer:)
|
||||
public static func objc_uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> AnyPromise {
|
||||
return AnyPromise.from(uploadAttachment(attachment, with: attachmentID, to: server))
|
||||
}
|
||||
|
||||
public static func uploadAttachment(_ attachment: TSAttachmentStream, with attachmentID: String, to server: String) -> Promise<Void> {
|
||||
let isEncryptionRequired = (server == FileServerAPI.server)
|
||||
return Promise<Void>() { seal in
|
||||
func proceed(with token: String) {
|
||||
// Get the attachment
|
||||
let data: Data
|
||||
guard let unencryptedAttachmentData = try? attachment.readDataFromFile() else {
|
||||
SNLog("Couldn't read attachment from disk.")
|
||||
return seal.reject(Error.generic)
|
||||
}
|
||||
// Encrypt the attachment if needed
|
||||
if isEncryptionRequired {
|
||||
var encryptionKey = NSData()
|
||||
var digest = NSData()
|
||||
guard let encryptedAttachmentData = Cryptography.encryptAttachmentData(unencryptedAttachmentData, shouldPad: false, outKey: &encryptionKey, outDigest: &digest) else {
|
||||
SNLog("Couldn't encrypt attachment.")
|
||||
return seal.reject(Error.encryptionFailed)
|
||||
}
|
||||
attachment.encryptionKey = encryptionKey as Data
|
||||
attachment.digest = digest as Data
|
||||
data = encryptedAttachmentData
|
||||
} else {
|
||||
data = unencryptedAttachmentData
|
||||
}
|
||||
// Check the file size if needed
|
||||
SNLog("File size: \(data.count) bytes.")
|
||||
if Double(data.count) > Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier {
|
||||
return seal.reject(Error.maxFileSizeExceeded)
|
||||
}
|
||||
// Create the request
|
||||
let url = "\(server)/files"
|
||||
let parameters: JSON = [ "type" : attachmentType, "Content-Type" : "application/binary" ]
|
||||
var error: NSError?
|
||||
let request = AFHTTPRequestSerializer().multipartFormRequest(withMethod: "POST", urlString: url, parameters: parameters, constructingBodyWith: { formData in
|
||||
let uuid = UUID().uuidString
|
||||
SNLog("File UUID: \(uuid).")
|
||||
formData.appendPart(withFileData: data, name: "content", fileName: uuid, mimeType: "application/binary")
|
||||
}, error: &error)
|
||||
request.addValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||
if let error = error {
|
||||
SNLog("Couldn't upload attachment due to error: \(error).")
|
||||
return seal.reject(error)
|
||||
}
|
||||
// Send the request
|
||||
let serverPublicKeyPromise = (server == FileServerAPI.server) ? Promise.value(FileServerAPI.publicKey)
|
||||
: OpenGroupAPI.getOpenGroupServerPublicKey(for: server)
|
||||
attachment.isUploaded = false
|
||||
attachment.save()
|
||||
let _ = serverPublicKeyPromise.then(on: DispatchQueue.global(qos: .userInitiated)) { serverPublicKey in
|
||||
OnionRequestAPI.sendOnionRequest(request, to: server, using: serverPublicKey)
|
||||
}.done(on: DispatchQueue.global(qos: .userInitiated)) { json in
|
||||
// Parse the server ID & download URL
|
||||
guard let data = json["data"] as? JSON, let serverID = data["id"] as? UInt64, let downloadURL = data["url"] as? String else {
|
||||
SNLog("Couldn't parse attachment from: \(json).")
|
||||
return seal.reject(Error.parsingFailed)
|
||||
}
|
||||
// Update the attachment
|
||||
attachment.serverId = serverID
|
||||
attachment.isUploaded = true
|
||||
attachment.downloadURL = downloadURL
|
||||
attachment.save()
|
||||
seal.fulfill(())
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
if server == FileServerAPI.server {
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
proceed(with: "loki") // Uploads to the Loki File Server shouldn't include any personally identifiable information so use a dummy auth token
|
||||
}
|
||||
} else {
|
||||
getAuthToken(for: server).done(on: DispatchQueue.global(qos: .userInitiated)) { token in
|
||||
proceed(with: token)
|
||||
}.catch(on: DispatchQueue.global(qos: .userInitiated)) { error in
|
||||
SNLog("Couldn't upload attachment due to error: \(error).")
|
||||
seal.reject(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
//
|
||||
|
||||
@objc
|
||||
public protocol OWSProximityMonitoringManager: class {
|
||||
public protocol OWSProximityMonitoringManager: AnyObject {
|
||||
func add(lifetime: AnyObject)
|
||||
func remove(lifetime: AnyObject)
|
||||
}
|
||||
|
|
|
@ -3,6 +3,4 @@
|
|||
public final class Features : NSObject {
|
||||
public static let useOnionRequests = true
|
||||
public static let useTestnet = false
|
||||
@objc public static let useV2OpenGroups = true
|
||||
@objc public static let useV2FileServer = true
|
||||
}
|
||||
|
|
|
@ -7,11 +7,9 @@ public enum SNUserDefaults {
|
|||
case hasViewedSeed
|
||||
case hasSeenLinkPreviewSuggestion
|
||||
case isUsingFullAPNs
|
||||
case isMigratingToV2KeyPair
|
||||
}
|
||||
|
||||
public enum Date : Swift.String {
|
||||
case lastProfilePictureUpload
|
||||
case lastConfigurationSync
|
||||
case lastDisplayNameUpdate
|
||||
case lastProfilePictureUpdate
|
||||
|
@ -28,8 +26,6 @@ public enum SNUserDefaults {
|
|||
|
||||
public enum String : Swift.String {
|
||||
case deviceToken
|
||||
/// Just used for migration purposes.
|
||||
case displayName
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -9,6 +9,6 @@ public final class Configuration : NSObject {
|
|||
@objc public static func performMainSetup() {
|
||||
SNMessagingKit.configure(storage: Storage.shared)
|
||||
SNSnodeKit.configure(storage: Storage.shared)
|
||||
SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPI.maxFileSize) / FileServerAPI.fileSizeORMultiplier))
|
||||
SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -27,8 +27,6 @@ extension ConfigurationMessage {
|
|||
case .openGroup:
|
||||
if let v2OpenGroup = storage.getV2OpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert("\(v2OpenGroup.server)/\(v2OpenGroup.room)?public_key=\(v2OpenGroup.publicKey)")
|
||||
} else if let openGroup = storage.getOpenGroup(for: thread.uniqueId!) {
|
||||
openGroups.insert(openGroup.server)
|
||||
}
|
||||
default: break
|
||||
}
|
||||
|
|
|
@ -47,17 +47,10 @@ extension MessageSender {
|
|||
let (promise, seal) = Promise<Void>.pending()
|
||||
AttachmentUploadJob.upload(stream, using: { data in return OpenGroupAPIV2.upload(data, to: v2OpenGroup.room, on: v2OpenGroup.server) }, encrypt: false, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
|
||||
return promise
|
||||
} else if Features.useV2FileServer && storage.getOpenGroup(for: thread.uniqueId!) == nil {
|
||||
} else {
|
||||
let (promise, seal) = Promise<Void>.pending()
|
||||
AttachmentUploadJob.upload(stream, using: FileServerAPIV2.upload, encrypt: true, onSuccess: { seal.fulfill(()) }, onFailure: { seal.reject($0) })
|
||||
return promise
|
||||
} else { // Legacy
|
||||
let openGroup = storage.getOpenGroup(for: thread.uniqueId!)
|
||||
let server = openGroup?.server ?? FileServerAPI.server
|
||||
let maxRetryCount: UInt = (openGroup != nil) ? 24 : 8
|
||||
return attempt(maxRetryCount: maxRetryCount, recoveringOn: DispatchQueue.global(qos: .userInitiated)) {
|
||||
FileServerAPI.uploadAttachment(stream, with: stream.uniqueId!, to: server)
|
||||
}
|
||||
}
|
||||
}
|
||||
return when(resolved: attachmentUploadPromises).then(on: DispatchQueue.global(qos: .userInitiated)) { results -> Promise<Void> in
|
||||
|
|
|
@ -396,18 +396,6 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
|
|||
avatarUrl:(nullable NSString *)avatarURL
|
||||
success:(void (^)(void))successBlock
|
||||
failure:(ProfileManagerFailureBlock)failureBlock {
|
||||
OWSAssertDebug(successBlock);
|
||||
OWSAssertDebug(failureBlock);
|
||||
|
||||
NSDictionary *publicChats = [LKStorage.shared getAllUserOpenGroups];
|
||||
|
||||
NSSet *servers = [NSSet setWithArray:[publicChats.allValues map:^NSString *(SNOpenGroup *publicChat) { return publicChat.server; }]];
|
||||
|
||||
for (NSString *server in servers) {
|
||||
[[SNOpenGroupAPI setDisplayName:localProfileName on:server] retainUntilComplete];
|
||||
[[SNOpenGroupAPI setProfilePictureURL:avatarURL usingProfileKey:self.localProfileKey.keyData on:server] retainUntilComplete];
|
||||
}
|
||||
|
||||
successBlock();
|
||||
}
|
||||
|
||||
|
@ -805,14 +793,9 @@ typedef void (^ProfileManagerFailureBlock)(NSError *error);
|
|||
|
||||
NSString *profilePictureURL = userProfile.avatarUrlPath;
|
||||
|
||||
AnyPromise *promise;
|
||||
if ([profilePictureURL containsString:SNFileServerAPIV2.server] || [profilePictureURL containsString:SNFileServerAPIV2.oldServer]) {
|
||||
NSString *file = [profilePictureURL lastPathComponent];
|
||||
BOOL useOldServer = [profilePictureURL containsString:SNFileServerAPIV2.oldServer];
|
||||
promise = [SNFileServerAPIV2 download:file useOldServer:useOldServer];
|
||||
} else {
|
||||
promise = [SNFileServerAPI downloadAttachmentFrom:profilePictureURL];
|
||||
}
|
||||
NSString *file = [profilePictureURL lastPathComponent];
|
||||
BOOL useOldServer = [profilePictureURL containsString:SNFileServerAPIV2.oldServer];
|
||||
AnyPromise *promise = [SNFileServerAPIV2 download:file useOldServer:useOldServer];
|
||||
|
||||
[promise.then(^(NSData *data) {
|
||||
@synchronized(self.currentAvatarDownloads)
|
||||
|
|
Loading…
Reference in New Issue