diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 45c178a6f..d5544efd6 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -765,6 +765,26 @@ F5765D284BC6ECAC0C1D33F0 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E4A93ECA93B3DE800CC7D7F6 /* Pods_GlobalDependencies_FrameworkAndExtensionDependencies_SessionNotificationServiceExtension.framework */; }; FC3BD9881A30A790005B96BB /* Social.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC3BD9871A30A790005B96BB /* Social.framework */; }; FCB11D8C1A129A76002F93FB /* CoreMedia.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FCB11D8B1A129A76002F93FB /* CoreMedia.framework */; }; + FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */; }; + FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */; }; + FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */; }; + FD17D7A127F40D2500122BE0 /* GRDBStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */; }; + FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */; }; + FD17D7A427F40F8100122BE0 /* _002_YDBToGRDBMigration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */; }; + FD17D7A727F41AF000122BE0 /* SSKLegacyModels.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */; }; + FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7A927F41BF500122BE0 /* SnodeSet.swift */; }; + FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */; }; + FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */; }; + FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */; }; + FD17D7B627F51E7300122BE0 /* SettingType.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B527F51E7300122BE0 /* SettingType.swift */; }; + FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B727F51ECA00122BE0 /* Migration.swift */; }; + FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */; }; + FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */; }; + FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */; }; + FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */; }; + FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */; }; + FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */; }; + FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */; }; FD5D200F27AA2B6000FEA984 /* MessageRequestResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */; }; FD5D201127AA331F00FEA984 /* ConfigurationMessage+Convenience.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */; }; FD659AC027A7649600F12C02 /* MessageRequestsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */; }; @@ -1792,6 +1812,26 @@ F9BBF530D71905BA9007675F /* Pods-SessionShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-SessionShareExtension.debug.xcconfig"; path = "Pods/Target Support Files/Pods-SessionShareExtension/Pods-SessionShareExtension.debug.xcconfig"; sourceTree = ""; }; FC3BD9871A30A790005B96BB /* Social.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Social.framework; path = System/Library/Frameworks/Social.framework; sourceTree = SDKROOT; }; FCB11D8B1A129A76002F93FB /* CoreMedia.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreMedia.framework; path = System/Library/Frameworks/CoreMedia.framework; sourceTree = SDKROOT; }; + FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SMKLegacyModels.swift; sourceTree = ""; }; + FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _001_InitialSetupMigration.swift; sourceTree = ""; }; + FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _002_YDBToGRDBMigration.swift; sourceTree = ""; }; + FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKLegacyModels.swift; sourceTree = ""; }; + FD17D7A927F41BF500122BE0 /* SnodeSet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeSet.swift; sourceTree = ""; }; + FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SnodeReceivedMessageInfo.swift; sourceTree = ""; }; + FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Set+Utilities.swift"; sourceTree = ""; }; + FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SSKSetting.swift; sourceTree = ""; }; + FD17D7B527F51E7300122BE0 /* SettingType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingType.swift; sourceTree = ""; }; + FD17D7B727F51ECA00122BE0 /* Migration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Migration.swift; sourceTree = ""; }; + FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TargetMigrations.swift; sourceTree = ""; }; + FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "GRDB+Notifications.swift"; sourceTree = ""; }; + FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColumnExpressible.swift; sourceTree = ""; }; + FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypedTableDefinition.swift; sourceTree = ""; }; + FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Database+Utilities.swift"; sourceTree = ""; }; + FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ColumnDefinition+Utilities.swift"; sourceTree = ""; }; + FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DatabaseMigrator+Utilities.swift"; sourceTree = ""; }; + FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GRDBStorage.swift; sourceTree = ""; }; FD5D200E27AA2B6000FEA984 /* MessageRequestResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestResponse.swift; sourceTree = ""; }; FD5D201027AA331F00FEA984 /* ConfigurationMessage+Convenience.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ConfigurationMessage+Convenience.swift"; sourceTree = ""; }; FD659ABF27A7649600F12C02 /* MessageRequestsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageRequestsViewController.swift; sourceTree = ""; }; @@ -2243,6 +2283,9 @@ B8A582AB258C64E800AFD84C /* Database */ = { isa = PBXGroup; children = ( + FD17D7B427F51E6700122BE0 /* Types */, + FD17D7BB27F51F5C00122BE0 /* Utilities */, + FD28A4F527EAD44C00FF65E7 /* GRDBStorage.swift */, C33FDBAB255A581500E217F9 /* OWSFileSystem.h */, C33FDA8E255A57FD00E217F9 /* OWSFileSystem.m */, C3D9E41E25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift */, @@ -2338,6 +2381,7 @@ C33FDB51255A580D00E217F9 /* NSUserDefaults+OWS.h */, C33FDB77255A581000E217F9 /* NSUserDefaults+OWS.m */, C33FDB14255A580800E217F9 /* OWSMath.h */, + FD17D7AF27F4225C00122BE0 /* Set+Utilities.swift */, C33FDB6B255A580F00E217F9 /* SNUserDefaults.swift */, C33FDB3F255A580C00E217F9 /* String+SSK.swift */, C3C2AC2D2553CBEB00C340D1 /* String+Trimming.swift */, @@ -2654,6 +2698,8 @@ C32C5BCB256DC818003C73A2 /* Database */ = { isa = PBXGroup; children = ( + FD17D79A27F40ADA00122BE0 /* LegacyDatabase */, + FD17D79427F3E03300122BE0 /* Migrations */, B8F5F56425EC8453003BF8D4 /* Notification+Contacts.swift */, C33FDAEA255A580500E217F9 /* OWSBackupFragment.h */, C33FDB07255A580700E217F9 /* OWSBackupFragment.m */, @@ -3220,11 +3266,11 @@ isa = PBXGroup; children = ( C3C2A5B0255385C700C340D1 /* Meta */, + FD17D79D27F40CAA00122BE0 /* Database */, C3C2A5B9255385ED00C340D1 /* Configuration.swift */, C3C2A5BD255385EE00C340D1 /* Notification+OnionRequestAPI.swift */, C3C2A5BA255385ED00C340D1 /* OnionRequestAPI.swift */, C3C2A5BB255385ED00C340D1 /* OnionRequestAPI+Encryption.swift */, - C3C2A5B7255385EC00C340D1 /* Snode.swift */, C3C2A5BE255385EE00C340D1 /* SnodeAPI.swift */, C3C2A5B6255385EC00C340D1 /* SnodeMessage.swift */, C3C2A5B8255385EC00C340D1 /* Storage.swift */, @@ -3579,6 +3625,92 @@ path = Session; sourceTree = ""; }; + FD17D79427F3E03300122BE0 /* Migrations */ = { + isa = PBXGroup; + children = ( + FD17D79527F3E04600122BE0 /* _001_InitialSetupMigration.swift */, + FD17D79827F40AB800122BE0 /* _002_YDBToGRDBMigration.swift */, + ); + path = Migrations; + sourceTree = ""; + }; + FD17D79A27F40ADA00122BE0 /* LegacyDatabase */ = { + isa = PBXGroup; + children = ( + FD17D79B27F40B2E00122BE0 /* SMKLegacyModels.swift */, + ); + path = LegacyDatabase; + sourceTree = ""; + }; + FD17D79D27F40CAA00122BE0 /* Database */ = { + isa = PBXGroup; + children = ( + FD17D7A527F41ADE00122BE0 /* LegacyDatabase */, + FD17D79E27F40CC000122BE0 /* Migrations */, + FD17D7A827F41BE300122BE0 /* Models */, + FD17D7B127F51E2B00122BE0 /* Types */, + ); + path = Database; + sourceTree = ""; + }; + FD17D79E27F40CC000122BE0 /* Migrations */ = { + isa = PBXGroup; + children = ( + FD17D79F27F40CC800122BE0 /* _001_InitialSetupMigration.swift */, + FD17D7A327F40F8100122BE0 /* _002_YDBToGRDBMigration.swift */, + ); + path = Migrations; + sourceTree = ""; + }; + FD17D7A527F41ADE00122BE0 /* LegacyDatabase */ = { + isa = PBXGroup; + children = ( + FD17D7A627F41AF000122BE0 /* SSKLegacyModels.swift */, + ); + path = LegacyDatabase; + sourceTree = ""; + }; + FD17D7A827F41BE300122BE0 /* Models */ = { + isa = PBXGroup; + children = ( + C3C2A5B7255385EC00C340D1 /* Snode.swift */, + FD17D7A927F41BF500122BE0 /* SnodeSet.swift */, + FD17D7AD27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift */, + ); + path = Models; + sourceTree = ""; + }; + FD17D7B127F51E2B00122BE0 /* Types */ = { + isa = PBXGroup; + children = ( + FD17D7B227F51E5B00122BE0 /* SSKSetting.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD17D7B427F51E6700122BE0 /* Types */ = { + isa = PBXGroup; + children = ( + FD17D7BE27F51F8200122BE0 /* ColumnExpressible.swift */, + FD17D7B727F51ECA00122BE0 /* Migration.swift */, + FD17D7B527F51E7300122BE0 /* SettingType.swift */, + FD17D7B927F51F2100122BE0 /* TargetMigrations.swift */, + FD17D7C027F5200100122BE0 /* TypedTableDefinition.swift */, + ); + path = Types; + sourceTree = ""; + }; + FD17D7BB27F51F5C00122BE0 /* Utilities */ = { + isa = PBXGroup; + children = ( + FD17D7C427F5206300122BE0 /* ColumnDefinition+Utilities.swift */, + FD17D7C227F5204C00122BE0 /* Database+Utilities.swift */, + FD17D7C627F5207C00122BE0 /* DatabaseMigrator+Utilities.swift */, + FD17D7BC27F51F6900122BE0 /* GRDB+Notifications.swift */, + ); + path = Utilities; + sourceTree = ""; + }; FD659ABE27A7648200F12C02 /* Message Requests */ = { isa = PBXGroup; children = ( @@ -4576,16 +4708,22 @@ C3C2A5E02553860B00C340D1 /* Threading.swift in Sources */, C3C2A5BF255385EE00C340D1 /* SnodeMessage.swift in Sources */, C3C2A5C0255385EE00C340D1 /* Snode.swift in Sources */, + FD17D7A027F40CC800122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3C2A5C7255385EE00C340D1 /* SnodeAPI.swift in Sources */, C32C5CBF256DD282003C73A2 /* Storage+SnodeAPI.swift in Sources */, C3C2A5C6255385EE00C340D1 /* Notification+OnionRequestAPI.swift in Sources */, + FD17D7AA27F41BF500122BE0 /* SnodeSet.swift in Sources */, + FD17D7A427F40F8100122BE0 /* _002_YDBToGRDBMigration.swift in Sources */, C32C5CBE256DD282003C73A2 /* Storage+OnionRequests.swift in Sources */, C3C2A5DC2553860B00C340D1 /* Promise+Threading.swift in Sources */, C3C2A5C4255385EE00C340D1 /* OnionRequestAPI+Encryption.swift in Sources */, C3C2A5DE2553860B00C340D1 /* String+Trimming.swift in Sources */, C3C2A5DB2553860B00C340D1 /* Promise+Hashing.swift in Sources */, C3C2A5E42553860B00C340D1 /* Data+Utilities.swift in Sources */, + FD17D7A727F41AF000122BE0 /* SSKLegacyModels.swift in Sources */, C3C2A5C2255385EE00C340D1 /* Configuration.swift in Sources */, + FD17D7B327F51E5B00122BE0 /* SSKSetting.swift in Sources */, + FD17D7AE27F41C4300122BE0 /* SnodeReceivedMessageInfo.swift in Sources */, C3C2A5C3255385EE00C340D1 /* OnionRequestAPI.swift in Sources */, C3C2A5C1255385EE00C340D1 /* Storage.swift in Sources */, ); @@ -4597,6 +4735,7 @@ files = ( C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */, 7B1D74AC27BDE7510030B423 /* Promise+Timeout.swift in Sources */, + FD17D7B627F51E7300122BE0 /* SettingType.swift in Sources */, C3AABDDF2553ECF00042FF4C /* Array+Utilities.swift in Sources */, C32C5A47256DB8F0003C73A2 /* ECKeyPair+Hexadecimal.swift in Sources */, C3D9E41525676C320040E4F3 /* Storage.swift in Sources */, @@ -4604,6 +4743,7 @@ C32C5D83256DD5B6003C73A2 /* SSKKeychainStorage.swift in Sources */, C3D9E39B256763C20040E4F3 /* AppContext.m in Sources */, C3BBE0A82554D4DE0050F1E3 /* JSON.swift in Sources */, + FD17D7C127F5200100122BE0 /* TypedTableDefinition.swift in Sources */, C352A36D2557858E00338F3E /* NSTimer+Proxying.m in Sources */, C32C5A2D256DB849003C73A2 /* LKGroupUtilities.m in Sources */, C3C2ABD22553C6C900C340D1 /* Data+SecureRandom.swift in Sources */, @@ -4617,6 +4757,7 @@ C3A7211A2558BCA10043A11F /* DiffieHellman.swift in Sources */, C32C5FA1256DFED5003C73A2 /* NSArray+Functional.m in Sources */, C3A7225E2558C38D0043A11F /* Promise+Retaining.swift in Sources */, + FD17D7BD27F51F6900122BE0 /* GRDB+Notifications.swift in Sources */, C3D9E4D12567777D0040E4F3 /* OWSMediaUtils.swift in Sources */, C3BBE0AA2554D4DE0050F1E3 /* Dictionary+Description.swift in Sources */, C3D9E4DA256778410040E4F3 /* UIImage+OWS.m in Sources */, @@ -4624,21 +4765,28 @@ C3D9E35E25675F640040E4F3 /* OWSFileSystem.m in Sources */, C3C2AC2E2553CBEB00C340D1 /* String+Trimming.swift in Sources */, C32C5B48256DC211003C73A2 /* NSNotificationCenter+OWS.m in Sources */, + FD17D7C727F5207C00122BE0 /* DatabaseMigrator+Utilities.swift in Sources */, C3D9E3C925676AF30040E4F3 /* TSYapDatabaseObject.m in Sources */, C352A3A62557B60D00338F3E /* TSRequest.m in Sources */, FD705A92278D051200F16121 /* ReusableView.swift in Sources */, + FD17D7BA27F51F2100122BE0 /* TargetMigrations.swift in Sources */, + FD17D7C327F5204C00122BE0 /* Database+Utilities.swift in Sources */, B8AE75A425A6C6A6001A84D2 /* Data+Trimming.swift in Sources */, + FD17D7C527F5206300122BE0 /* ColumnDefinition+Utilities.swift in Sources */, B8856DE6256F15F2001CE70E /* String+SSK.swift in Sources */, C3471ED42555386B00297E91 /* AESGCM.swift in Sources */, + FD17D7B827F51ECA00122BE0 /* Migration.swift in Sources */, C32C5DDB256DD9FF003C73A2 /* ContentProxy.swift in Sources */, C3A71F892558BA9F0043A11F /* Mnemonic.swift in Sources */, B8F5F58325EC94A6003BF8D4 /* Collection+Subscripting.swift in Sources */, C33FDEF8255A656D00E217F9 /* Promise+Delaying.swift in Sources */, B8BC00C0257D90E30032E807 /* General.swift in Sources */, + FD17D7A127F40D2500122BE0 /* GRDBStorage.swift in Sources */, C32C5A24256DB7DB003C73A2 /* SNUserDefaults.swift in Sources */, C3D9E41F25676C870040E4F3 /* OWSPrimaryStorageProtocol.swift in Sources */, C3BBE0A72554D4DE0050F1E3 /* Promise+Retrying.swift in Sources */, B8856D7B256F14F4001CE70E /* UIView+OWS.m in Sources */, + FD17D7B027F4225C00122BE0 /* Set+Utilities.swift in Sources */, B88FA7FB26114EA70049422F /* Hex.swift in Sources */, C35D0DB525AE5F1200B6BF49 /* UIEdgeInsets.swift in Sources */, C3D9E4F4256778AF0040E4F3 /* NSData+Image.m in Sources */, @@ -4646,6 +4794,7 @@ C3BBE0A92554D4DE0050F1E3 /* HTTP.swift in Sources */, B8856D23256F116B001CE70E /* Weak.swift in Sources */, C32C5A48256DB8F0003C73A2 /* BuildConfiguration.swift in Sources */, + FD17D7BF27F51F8200122BE0 /* ColumnExpressible.swift in Sources */, B87EF18126377A1D00124B3C /* Features.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, @@ -4705,6 +4854,7 @@ C3A3A107256E1A5C004D228D /* OWSDisappearingMessagesFinder.m in Sources */, C32C59C3256DB41F003C73A2 /* TSGroupModel.m in Sources */, B8856ECE256F1E58001CE70E /* OWSPreferences.m in Sources */, + FD17D79927F40AB800122BE0 /* _002_YDBToGRDBMigration.swift in Sources */, C3C2A7842553AAF300C340D1 /* SNProto.swift in Sources */, C32C5DC0256DD743003C73A2 /* Poller.swift in Sources */, C3C2A7682553A3D900C340D1 /* VisibleMessage+Contact.swift in Sources */, @@ -4717,6 +4867,7 @@ B8566C63256F55930045A0B9 /* OWSLinkPreview+Conversion.swift in Sources */, C32C5B3F256DC1DF003C73A2 /* TSQuotedMessage+Conversion.swift in Sources */, B8EB20EE2640F28000773E52 /* VisibleMessage+OpenGroupInvitation.swift in Sources */, + FD17D7A227F40F0500122BE0 /* _001_InitialSetupMigration.swift in Sources */, C3C2A7712553A41E00C340D1 /* ControlMessage.swift in Sources */, C32C5D19256DD493003C73A2 /* OWSLinkPreview.swift in Sources */, C32C5CF0256DD3E4003C73A2 /* Storage+Shared.swift in Sources */, @@ -4786,6 +4937,7 @@ C32C5FD6256E0346003C73A2 /* Notification+Thread.swift in Sources */, C3BBE0C72554F1570050F1E3 /* FixedWidthInteger+BigEndian.swift in Sources */, C32C5C88256DD0D2003C73A2 /* Storage+Messaging.swift in Sources */, + FD17D79C27F40B2E00122BE0 /* SMKLegacyModels.swift in Sources */, C32C59C7256DB41F003C73A2 /* TSThread.m in Sources */, C300A5B22554AF9800555489 /* VisibleMessage+Profile.swift in Sources */, C32C5A75256DBBCF003C73A2 /* TSAttachmentPointer+Conversion.swift in Sources */, diff --git a/Session/Path/PathVC.swift b/Session/Path/PathVC.swift index 703f24e20..649501e6f 100644 --- a/Session/Path/PathVC.swift +++ b/Session/Path/PathVC.swift @@ -156,7 +156,7 @@ final class PathVC : BaseVC { return stackView } - private func getPathRow(snode: Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView { + private func getPathRow(snode: Legacy.Snode, location: LineView.Location, dotAnimationStartDelay: Double, dotAnimationRepeatInterval: Double, isGuardSnode: Bool) -> UIStackView { let country = IP2Country.isInitialized ? (IP2Country.shared.countryNamesCache[snode.ip] ?? "Resolving...") : "Resolving..." let title = isGuardSnode ? NSLocalizedString("vc_path_guard_node_row_title", comment: "") : NSLocalizedString("vc_path_service_node_row_title", comment: "") return getPathRow(title: title, subtitle: country, location: location, dotAnimationStartDelay: dotAnimationStartDelay, dotAnimationRepeatInterval: dotAnimationRepeatInterval) diff --git a/SessionMessagingKit/Configuration.swift b/SessionMessagingKit/Configuration.swift index 2af8d4c48..197827a2d 100644 --- a/SessionMessagingKit/Configuration.swift +++ b/SessionMessagingKit/Configuration.swift @@ -1,3 +1,5 @@ +import Foundation +import SessionUtilitiesKit @objc public final class SNMessagingKitConfiguration : NSObject { @@ -11,7 +13,20 @@ public final class SNMessagingKitConfiguration : NSObject { } public enum SNMessagingKit { // Just to make the external API nice - + public static func migrations() -> TargetMigrations { + return TargetMigrations( + identifier: .messagingKit, + migrations: [ + [ + _001_InitialSetupMigration.self + ], + [ + _002_YDBToGRDBMigration.self + ] + ] + ) + } + public static func configure(storage: SessionMessagingKitStorageProtocol) { SNMessagingKitConfiguration.shared = SNMessagingKitConfiguration(storage: storage) } diff --git a/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift new file mode 100644 index 000000000..79e14cd58 --- /dev/null +++ b/SessionMessagingKit/Database/LegacyDatabase/SMKLegacyModels.swift @@ -0,0 +1,6 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +enum Legacy { +} diff --git a/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift new file mode 100644 index 000000000..3971a1e96 --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -0,0 +1,49 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +// TODO: Remove/Move these +struct Place: Codable, FetchableRecord, PersistableRecord, ColumnExpressible { + static var databaseTableName: String { "place" } + + public enum Columns: String, CodingKey, ColumnExpression { + case id + case name + } + + let id: String + let name: String +} + +struct Setting: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + static var databaseTableName: String { "settings" } + + public enum Columns: String, CodingKey, ColumnExpression { + case key + case value + } + + let key: String + let value: Data +} + +enum _001_InitialSetupMigration: Migration { + static let identifier: String = "initialSetup" + + static func migrate(_ db: Database) throws { + try db.create(table: Setting.self) { t in + t.column(.key, .text) + .notNull() + .unique(onConflict: .abort) + .primaryKey() + t.column(.value, .blob).notNull() + } + + try db.create(table: Place.self) { t in + t.column(.id, .text).notNull().primaryKey() + t.column(.name, .text).notNull() + } + } +} diff --git a/SessionMessagingKit/Database/Migrations/_002_YDBToGRDBMigration.swift b/SessionMessagingKit/Database/Migrations/_002_YDBToGRDBMigration.swift new file mode 100644 index 000000000..3ccc6a5cf --- /dev/null +++ b/SessionMessagingKit/Database/Migrations/_002_YDBToGRDBMigration.swift @@ -0,0 +1,14 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _002_YDBToGRDBMigration: Migration { + static let identifier: String = "YDBToGRDBMigration" + + static func migrate(_ db: Database) throws { + + + } +} diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift index 623acf1ff..b7aaca193 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/Poller.swift @@ -5,7 +5,7 @@ import PromiseKit public final class Poller : NSObject { private let storage = OWSPrimaryStorage.shared() private var isPolling = false - private var usedSnodes = Set() + private var usedSnodes = Set() private var pollCount = 0 // MARK: Settings @@ -89,7 +89,7 @@ public final class Poller : NSObject { } } - private func poll(_ snode: Snode, seal longTermSeal: Resolver) -> Promise { + private func poll(_ snode: SessionSnodeKit.Legacy.Snode, seal longTermSeal: Resolver) -> Promise { guard isPolling else { return Promise { $0.fulfill(()) } } let userPublicKey = getUserHexEncodedPublicKey() return SnodeAPI.getRawMessages(from: snode, associatedWith: userPublicKey).then(on: Threading.pollerQueue) { [weak self] rawResponse -> Promise in diff --git a/SessionSnodeKit/Configuration.swift b/SessionSnodeKit/Configuration.swift index b880ae6e1..24a3074d5 100644 --- a/SessionSnodeKit/Configuration.swift +++ b/SessionSnodeKit/Configuration.swift @@ -1,3 +1,5 @@ +import Foundation +import SessionUtilitiesKit public struct SNSnodeKitConfiguration { public let storage: SessionSnodeKitStorageProtocol @@ -6,6 +8,19 @@ public struct SNSnodeKitConfiguration { } public enum SNSnodeKit { // Just to make the external API nice + public static func migrations() -> TargetMigrations { + return TargetMigrations( + identifier: .snodeKit, + migrations: [ + [ + _001_InitialSetupMigration.self + ], + [ + _002_YDBToGRDBMigration.self + ] + ] + ) + } public static func configure(storage: SessionSnodeKitStorageProtocol) { SNSnodeKitConfiguration.shared = SNSnodeKitConfiguration(storage: storage) diff --git a/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift b/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift new file mode 100644 index 000000000..52d873245 --- /dev/null +++ b/SessionSnodeKit/Database/LegacyDatabase/SSKLegacyModels.swift @@ -0,0 +1,80 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public enum Legacy { + // MARK: - Collections and Keys + + internal static let swarmCollectionPrefix = "LokiSwarmCollection-" + internal static let snodePoolCollection = "LokiSnodePoolCollection" + internal static let onionRequestPathCollection = "LokiOnionRequestPathCollection" + internal static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection" + internal static let lastMessageHashCollection = "LokiLastMessageHashCollection" // TODO: Remove this one? (make it a query??) + internal static let receivedMessagesCollection = "LokiReceivedMessagesCollection" + // TODO: - "lastSnodePoolRefreshDate" + + // MARK: - Types + + public typealias LegacyOnionRequestAPIPath = [Snode] + + @objc(Snode) + public final class Snode: NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility + public let address: String + public let port: UInt16 + public let publicKeySet: KeySet + + public var ip: String { + guard let range = address.range(of: "https://"), range.lowerBound == address.startIndex else { return address } + return String(address[range.upperBound.. Bool { + guard let other = other as? Snode else { return false } + return address == other.address && port == other.port + } + + override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) + return address.hashValue ^ port.hashValue + } + } +} diff --git a/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift b/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift new file mode 100644 index 000000000..37523b470 --- /dev/null +++ b/SessionSnodeKit/Database/Migrations/_001_InitialSetupMigration.swift @@ -0,0 +1,47 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _001_InitialSetupMigration: Migration { + static let identifier: String = "initialSetup" + + static func migrate(_ db: Database) throws { + try db.create(table: Snode.self) { t in + t.column(.address, .text).notNull() + t.column(.port, .integer).notNull() + t.column(.ed25519PublicKey, .text).notNull() + t.column(.x25519PublicKey, .text).notNull() + + t.primaryKey([.address, .port]) + } + + try db.create(table: SnodeSet.self) { t in + t.column(.key, .text).notNull() + t.column(.nodeIndex, .integer).notNull() + t.column(.address, .text).notNull() + t.column(.port, .integer).notNull() + + t.foreignKey( + [.address, .port], + references: Snode.self, + columns: [.address, .port], + onDelete: .cascade + ) + t.primaryKey([.key, .nodeIndex]) + } + + try db.create(table: SnodeReceivedMessageInfo.self) { t in + t.column(.key, .text) + .notNull() + .indexed() + t.column(.hash, .text).notNull() + t.column(.expirationDateMs, .integer) + .notNull() + .indexed() + + t.primaryKey([.key, .hash]) + } + } +} diff --git a/SessionSnodeKit/Database/Migrations/_002_YDBToGRDBMigration.swift b/SessionSnodeKit/Database/Migrations/_002_YDBToGRDBMigration.swift new file mode 100644 index 000000000..4a173265f --- /dev/null +++ b/SessionSnodeKit/Database/Migrations/_002_YDBToGRDBMigration.swift @@ -0,0 +1,138 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +enum _002_YDBToGRDBMigration: Migration { + static let identifier: String = "YDBToGRDBMigration" + + // TODO: Autorelease pool??? + static func migrate(_ db: Database) throws { + // MARK: - OnionRequestPath, Snode Pool & Swarm + + // Note: Want to exclude the Snode's we already added from the 'onionRequestPathResult' + var snodeResult: Set = [] + var snodeSetResult: [String: Set] = [:] + + Storage.read { transaction in + // Process the OnionRequestPaths + if + let path0Snode0 = transaction.object(forKey: "0-0", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode, + let path0Snode1 = transaction.object(forKey: "0-1", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode, + let path0Snode2 = transaction.object(forKey: "0-2", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode + { + snodeResult.insert(path0Snode0) + snodeResult.insert(path0Snode1) + snodeResult.insert(path0Snode2) + snodeSetResult["\(SnodeSet.onionRequestPathPrefix)0"] = [ path0Snode0, path0Snode1, path0Snode2 ] + + if + let path1Snode0 = transaction.object(forKey: "1-0", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode, + let path1Snode1 = transaction.object(forKey: "1-1", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode, + let path1Snode2 = transaction.object(forKey: "1-2", inCollection: Legacy.onionRequestPathCollection) as? Legacy.Snode + { + snodeResult.insert(path1Snode0) + snodeResult.insert(path1Snode1) + snodeResult.insert(path1Snode2) + snodeSetResult["\(SnodeSet.onionRequestPathPrefix)1"] = [ path1Snode0, path1Snode1, path1Snode2 ] + } + } + + // Process the SnodePool + transaction.enumerateKeysAndObjects(inCollection: Legacy.snodePoolCollection) { _, object, _ in + guard let snode = object as? Legacy.Snode else { return } + snodeResult.insert(snode) + } + + // Process the Swarms + var swarmCollections: Set = [] + + transaction.enumerateCollections { collectionName, _ in + if collectionName.starts(with: Legacy.swarmCollectionPrefix) { + swarmCollections.insert(collectionName.substring(from: Legacy.swarmCollectionPrefix.count)) + } + } + + for swarmCollection in swarmCollections { + let collection: String = "\(Legacy.swarmCollectionPrefix)\(swarmCollection)" + + transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in + guard let snode = object as? Legacy.Snode else { return } + snodeResult.insert(snode) + snodeSetResult[swarmCollection] = (snodeSetResult[swarmCollection] ?? Set()).inserting(snode) + } + } + } + + try snodeResult.forEach { legacySnode in + try Snode( + address: legacySnode.address, + port: legacySnode.port, + ed25519PublicKey: legacySnode.publicKeySet.ed25519Key, + x25519PublicKey: legacySnode.publicKeySet.x25519Key + ).insert(db) + } + + try snodeSetResult.forEach { key, legacySnodeSet in + try legacySnodeSet.enumerated().forEach { nodeIndex, legacySnode in + // Note: In this case the 'nodeIndex' is irrelivant + try SnodeSet( + key: key, + nodeIndex: UInt(nodeIndex), + address: legacySnode.address, + port: legacySnode.port + ).insert(db) + } + } + + // TODO: This +// public func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any) { +// (transaction as! YapDatabaseReadWriteTransaction).setObject(date, forKey: "lastSnodePoolRefreshDate", inCollection: Storage.lastSnodePoolRefreshDateCollection) +// } + + print("RAWR") + + // MARK: - Received Messages & Last Message Hash + + var lastMessageResults: [String: (hash: String, json: JSON)] = [:] + var receivedMessageResults: [String: Set] = [:] + + Storage.read { transaction in + // Extract the received message hashes + transaction.enumerateKeysAndObjects(inCollection: Legacy.receivedMessagesCollection) { key, object, _ in + guard let hashSet = object as? Set else { return } + receivedMessageResults[key] = hashSet + } + + // Retrieve the last message info + transaction.enumerateKeysAndObjects(inCollection: Legacy.lastMessageHashCollection) { key, object, _ in + guard let lastMessageJson = object as? JSON else { return } + guard let lastMessageHash: String = lastMessageJson["hash"] as? String else { return } + + // Note: We remove the value from 'receivedMessageResults' as we don't want to default it's + // expiration value to 0 + lastMessageResults[key] = (lastMessageHash, lastMessageJson) + receivedMessageResults[key] = receivedMessageResults[key]?.removing(lastMessageHash) + } + } + + try receivedMessageResults.forEach { key, hashes in + try hashes.forEach { hash in + try SnodeReceivedMessageInfo( + key: key, + hash: hash, + expirationDateMs: 0 + ).insert(db) + } + } + + try lastMessageResults.forEach { key, data in + try SnodeReceivedMessageInfo( + key: key, + hash: data.hash, + expirationDateMs: ((data.json["expirationDate"] as? Int64) ?? 0) + ).insert(db) + } + } +} diff --git a/SessionSnodeKit/Database/Models/Snode.swift b/SessionSnodeKit/Database/Models/Snode.swift new file mode 100644 index 000000000..7e4a2afc6 --- /dev/null +++ b/SessionSnodeKit/Database/Models/Snode.swift @@ -0,0 +1,21 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +public struct Snode: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible, Hashable { + public static var databaseTableName: String { "snode" } + + public enum Columns: String, CodingKey, ColumnExpression { + case address + case port + case ed25519PublicKey + case x25519PublicKey + } + + let address: String + let port: UInt16 + let ed25519PublicKey: String + let x25519PublicKey: String +} diff --git a/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift new file mode 100644 index 000000000..d6c80c16d --- /dev/null +++ b/SessionSnodeKit/Database/Models/SnodeReceivedMessageInfo.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +struct SnodeReceivedMessageInfo: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + static var databaseTableName: String { "snodeReceivedMessageInfo" } + + public enum Columns: String, CodingKey, ColumnExpression { + case key + case hash + case expirationDateMs + } + + let key: String + let hash: String + let expirationDateMs: Int64 +} diff --git a/SessionSnodeKit/Database/Models/SnodeSet.swift b/SessionSnodeKit/Database/Models/SnodeSet.swift new file mode 100644 index 000000000..4668d2e40 --- /dev/null +++ b/SessionSnodeKit/Database/Models/SnodeSet.swift @@ -0,0 +1,27 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SessionUtilitiesKit + +struct SnodeSet: Codable, FetchableRecord, PersistableRecord, TableRecord, ColumnExpressible { + static var databaseTableName: String { "snodeSet" } + static let nodes = hasMany(Snode.self) + static let onionRequestPathPrefix = "OnionRequestPath-" + + public enum Columns: String, CodingKey, ColumnExpression { + case key + case nodeIndex + case address + case port + } + + let key: String + let nodeIndex: UInt + let address: String + let port: UInt16 + + var nodes: QueryInterfaceRequest { + request(for: SnodeSet.nodes) + } +} diff --git a/SessionSnodeKit/Database/Types/SSKSetting.swift b/SessionSnodeKit/Database/Types/SSKSetting.swift new file mode 100644 index 000000000..0a8ab0dac --- /dev/null +++ b/SessionSnodeKit/Database/Types/SSKSetting.swift @@ -0,0 +1,3 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation diff --git a/SessionSnodeKit/OnionRequestAPI.swift b/SessionSnodeKit/OnionRequestAPI.swift index 7a03b16f0..b50bee214 100644 --- a/SessionSnodeKit/OnionRequestAPI.swift +++ b/SessionSnodeKit/OnionRequestAPI.swift @@ -8,9 +8,9 @@ public enum OnionRequestAPI { /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. private static var pathFailureCount: [Path:UInt] = [:] /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - private static var snodeFailureCount: [Snode:UInt] = [:] + private static var snodeFailureCount: [Legacy.Snode:UInt] = [:] /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var guardSnodes: Set = [] + public static var guardSnodes: Set = [] public static var paths: [Path] = [] // Not a set to ensure we consistently show the same path to the user // MARK: Settings @@ -29,7 +29,7 @@ public enum OnionRequestAPI { // MARK: Destination public enum Destination : CustomStringConvertible { - case snode(Snode) + case snode(Legacy.Snode) case server(host: String, target: String, x25519PublicKey: String, scheme: String?, port: UInt16?) public var description: String { @@ -67,14 +67,14 @@ public enum OnionRequestAPI { } // MARK: Path - public typealias Path = [Snode] + public typealias Path = [Legacy.Snode] // MARK: Onion Building Result - private typealias OnionBuildingResult = (guardSnode: Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data) + private typealias OnionBuildingResult = (guardSnode: Legacy.Snode, finalEncryptionResult: AESGCM.EncryptionResult, destinationSymmetricKey: Data) // MARK: Private API /// Tests the given snode. The returned promise errors out if the snode is faulty; the promise is fulfilled otherwise. - private static func testSnode(_ snode: Snode) -> Promise { + private static func testSnode(_ snode: Legacy.Snode) -> Promise { let (promise, seal) = Promise.pending() DispatchQueue.global(qos: .userInitiated).async { let url = "\(snode.address):\(snode.port)/get_stats/v1" @@ -96,17 +96,17 @@ public enum OnionRequestAPI { /// Finds `targetGuardSnodeCount` guard snodes to use for path building. The returned promise errors out with `Error.insufficientSnodes` /// if not enough (reliable) snodes are available. - private static func getGuardSnodes(reusing reusableGuardSnodes: [Snode]) -> Promise> { + private static func getGuardSnodes(reusing reusableGuardSnodes: [Legacy.Snode]) -> Promise> { if guardSnodes.count >= targetGuardSnodeCount { - return Promise> { $0.fulfill(guardSnodes) } + return Promise> { $0.fulfill(guardSnodes) } } else { SNLog("Populating guard snode cache.") var unusedSnodes = SnodeAPI.snodePool.subtracting(reusableGuardSnodes) // Sync on LokiAPI.workQueue let reusableGuardSnodeCount = UInt(reusableGuardSnodes.count) guard unusedSnodes.count >= (targetGuardSnodeCount - reusableGuardSnodeCount) else { return Promise(error: Error.insufficientSnodes) } - func getGuardSnode() -> Promise { + func getGuardSnode() -> Promise { // randomElement() uses the system's default random generator, which is cryptographically secure - guard let candidate = unusedSnodes.randomElement() else { return Promise { $0.reject(Error.insufficientSnodes) } } + guard let candidate = unusedSnodes.randomElement() else { return Promise { $0.reject(Error.insufficientSnodes) } } unusedSnodes.remove(candidate) // All used snodes should be unique SNLog("Testing guard snode: \(candidate).") // Loop until a reliable guard snode is found @@ -167,7 +167,7 @@ public enum OnionRequestAPI { } /// Returns a `Path` to be used for building an onion request. Builds new paths as needed. - private static func getPath(excluding snode: Snode?) -> Promise { + private static func getPath(excluding snode: Legacy.Snode?) -> Promise { guard pathSize >= 1 else { preconditionFailure("Can't build path of size zero.") } var paths = OnionRequestAPI.paths if paths.isEmpty { @@ -216,14 +216,14 @@ public enum OnionRequestAPI { } } - private static func dropGuardSnode(_ snode: Snode) { + private static func dropGuardSnode(_ snode: Legacy.Snode) { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif guardSnodes = guardSnodes.filter { $0 != snode } } - private static func drop(_ snode: Snode) throws { + private static func drop(_ snode: Legacy.Snode) throws { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif @@ -272,10 +272,10 @@ public enum OnionRequestAPI { /// Builds an onion around `payload` and returns the result. private static func buildOnion(around payload: JSON, targetedAt destination: Destination) -> Promise { - var guardSnode: Snode! + var guardSnode: Legacy.Snode! var targetSnodeSymmetricKey: Data! // Needed by invoke(_:on:with:) to decrypt the response sent back by the destination var encryptionResult: AESGCM.EncryptionResult! - var snodeToExclude: Snode? + var snodeToExclude: Legacy.Snode? if case .snode(let snode) = destination { snodeToExclude = snode } return getPath(excluding: snodeToExclude).then2 { path -> Promise in guardSnode = path.first! @@ -305,7 +305,7 @@ public enum OnionRequestAPI { // MARK: Public API /// Sends an onion request to `snode`. Builds new paths as needed. - public static func sendOnionRequest(to snode: Snode, invoking method: Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { + public static func sendOnionRequest(to snode: Legacy.Snode, invoking method: Legacy.Snode.Method, with parameters: JSON, associatedWith publicKey: String? = nil) -> Promise { let payload: JSON = [ "method" : method.rawValue, "params" : parameters ] return sendOnionRequest(with: payload, to: Destination.snode(snode)).recover2 { error -> Promise in guard case OnionRequestAPI.Error.httpRequestFailedAtDestination(let statusCode, let json, _) = error else { throw error } @@ -365,7 +365,7 @@ public enum OnionRequestAPI { public static func sendOnionRequest(with payload: JSON, to destination: Destination) -> Promise { let (promise, seal) = Promise.pending() - var guardSnode: Snode? + var guardSnode: Legacy.Snode? Threading.workQueue.async { // Avoid race conditions on `guardSnodes` and `paths` buildOnion(around: payload, targetedAt: destination).done2 { intermediate in guardSnode = intermediate.guardSnode diff --git a/SessionSnodeKit/Snode.swift b/SessionSnodeKit/Snode.swift deleted file mode 100644 index bb36f2586..000000000 --- a/SessionSnodeKit/Snode.swift +++ /dev/null @@ -1,65 +0,0 @@ -import Foundation - -public final class Snode : NSObject, NSCoding { // NSObject/NSCoding conformance is needed for YapDatabase compatibility - public let address: String - public let port: UInt16 - public let publicKeySet: KeySet - - public var ip: String { - address.removingPrefix("https://") - } - - // MARK: Nested Types - public enum Method : String { - case getSwarm = "get_snodes_for_pubkey" - case getMessages = "retrieve" - case sendMessage = "store" - case deleteMessage = "delete" - case oxenDaemonRPCCall = "oxend_request" - case getInfo = "info" - case clearAllData = "delete_all" - } - - public struct KeySet { - public let ed25519Key: String - public let x25519Key: String - } - - // MARK: Initialization - internal init(address: String, port: UInt16, publicKeySet: KeySet) { - self.address = address - self.port = port - self.publicKeySet = publicKeySet - } - - // MARK: Coding - public init?(coder: NSCoder) { - address = coder.decodeObject(forKey: "address") as! String - port = coder.decodeObject(forKey: "port") as! UInt16 - guard let idKey = coder.decodeObject(forKey: "idKey") as? String, - let encryptionKey = coder.decodeObject(forKey: "encryptionKey") as? String else { return nil } - publicKeySet = KeySet(ed25519Key: idKey, x25519Key: encryptionKey) - super.init() - } - - public func encode(with coder: NSCoder) { - coder.encode(address, forKey: "address") - coder.encode(port, forKey: "port") - coder.encode(publicKeySet.ed25519Key, forKey: "idKey") - coder.encode(publicKeySet.x25519Key, forKey: "encryptionKey") - } - - // MARK: Equality - override public func isEqual(_ other: Any?) -> Bool { - guard let other = other as? Snode else { return false } - return address == other.address && port == other.port - } - - // MARK: Hashing - override public var hash: Int { // Override NSObject.hash and not Hashable.hashValue or Hashable.hash(into:) - return address.hashValue ^ port.hashValue - } - - // MARK: Description - override public var description: String { return "\(address):\(port)" } -} diff --git a/SessionSnodeKit/SnodeAPI.swift b/SessionSnodeKit/SnodeAPI.swift index 7602242bf..6e802beb9 100644 --- a/SessionSnodeKit/SnodeAPI.swift +++ b/SessionSnodeKit/SnodeAPI.swift @@ -8,12 +8,12 @@ public final class SnodeAPI : NSObject { private static var hasLoadedSnodePool = false private static var loadedSwarms: Set = [] - private static var getSnodePoolPromise: Promise>? + private static var getSnodePoolPromise: Promise>? /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - internal static var snodeFailureCount: [Snode:UInt] = [:] + internal static var snodeFailureCount: [Legacy.Snode:UInt] = [:] /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - internal static var snodePool: Set = [] + internal static var snodePool: Set = [] /// The offset between the user's clock and the Service Node's clock. Used in cases where the /// user's clock is incorrect. @@ -21,7 +21,7 @@ public final class SnodeAPI : NSObject { /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. public static var clockOffset: Int64 = 0 /// - Note: Should only be accessed from `Threading.workQueue` to avoid race conditions. - public static var swarmCache: [String:Set] = [:] + public static var swarmCache: [String:Set] = [:] // MARK: Settings private static let maxRetryCount: UInt = 8 @@ -72,7 +72,7 @@ public final class SnodeAPI : NSObject { hasLoadedSnodePool = true } - private static func setSnodePool(to newValue: Set, using transaction: Any? = nil) { + private static func setSnodePool(to newValue: Set, using transaction: Any? = nil) { snodePool = newValue let storage = SNSnodeKitConfiguration.shared.storage if let transaction = transaction { @@ -84,7 +84,7 @@ public final class SnodeAPI : NSObject { } } - private static func dropSnodeFromSnodePool(_ snode: Snode) { + private static func dropSnodeFromSnodePool(_ snode: Legacy.Snode) { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif @@ -107,7 +107,7 @@ public final class SnodeAPI : NSObject { loadedSwarms.insert(publicKey) } - private static func setSwarm(to newValue: Set, for publicKey: String, persist: Bool = true) { + private static func setSwarm(to newValue: Set, for publicKey: String, persist: Bool = true) { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif @@ -118,7 +118,7 @@ public final class SnodeAPI : NSObject { } } - public static func dropSnodeFromSwarmIfNeeded(_ snode: Snode, publicKey: String) { + public static func dropSnodeFromSwarmIfNeeded(_ snode: Legacy.Snode, publicKey: String) { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif @@ -129,7 +129,7 @@ public final class SnodeAPI : NSObject { } // MARK: Internal API - internal static func invoke(_ method: Snode.Method, on snode: Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { + internal static func invoke(_ method: Legacy.Snode.Method, on snode: Legacy.Snode, associatedWith publicKey: String? = nil, parameters: JSON) -> RawResponsePromise { if Features.useOnionRequests { return OnionRequestAPI.sendOnionRequest(to: snode, invoking: method, with: parameters, associatedWith: publicKey).map2 { $0 as Any } } else { @@ -141,7 +141,7 @@ public final class SnodeAPI : NSObject { } } - private static func getNetworkTime(from snode: Snode) -> Promise { + private static func getNetworkTime(from snode: Legacy.Snode) -> Promise { return invoke(.getInfo, on: snode, parameters: [:]).map2 { rawResponse in guard let json = rawResponse as? JSON, let timestamp = json["timestamp"] as? UInt64 else { throw HTTP.Error.invalidJSON } @@ -149,12 +149,12 @@ public final class SnodeAPI : NSObject { } } - internal static func getRandomSnode() -> Promise { + internal static func getRandomSnode() -> Promise { // randomElement() uses the system's default random generator, which is cryptographically secure return getSnodePool().map2 { $0.randomElement()! } } - private static func getSnodePoolFromSeedNode() -> Promise> { + private static func getSnodePoolFromSeedNode() -> Promise> { let target = seedNodePool.randomElement()! let url = "\(target)/json_rpc" let parameters: JSON = [ @@ -168,10 +168,10 @@ public final class SnodeAPI : NSObject { ] ] SNLog("Populating snode pool using seed node: \(target).") - let (promise, seal) = Promise>.pending() + let (promise, seal) = Promise>.pending() Threading.workQueue.async { attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { - HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Set in + HTTP.execute(.post, url, parameters: parameters, useSeedNodeURLSession: true).map2 { json -> Set in guard let intermediate = json["result"] as? JSON, let rawSnodes = intermediate["service_node_states"] as? [JSON] else { throw Error.snodePoolUpdatingFailed } return Set(rawSnodes.compactMap { rawSnode in guard let address = rawSnode["public_ip"] as? String, let port = rawSnode["storage_port"] as? Int, @@ -179,7 +179,7 @@ public final class SnodeAPI : NSObject { SNLog("Failed to parse snode from: \(rawSnode).") return nil } - return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) + return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) }) } }.done2 { snodePool in @@ -193,15 +193,15 @@ public final class SnodeAPI : NSObject { return promise } - private static func getSnodePoolFromSnode() -> Promise> { + private static func getSnodePoolFromSnode() -> Promise> { var snodePool = SnodeAPI.snodePool - var snodes: Set = [] + var snodes: Set = [] (0..<3).forEach { _ in let snode = snodePool.randomElement()! snodePool.remove(snode) snodes.insert(snode) } - let snodePoolPromises: [Promise>] = snodes.map { snode in + let snodePoolPromises: [Promise>] = snodes.map { snode in return attempt(maxRetryCount: 4, recoveringOn: Threading.workQueue) { // Don't specify a limit in the request. Service nodes return a shuffled // list of nodes so if we specify a limit the 3 responses we get might have @@ -226,18 +226,18 @@ public final class SnodeAPI : NSObject { SNLog("Failed to parse snode from: \(rawSnode).") return nil } - return Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) + return Legacy.Snode(address: "https://\(address)", port: UInt16(port), publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) }) } } } - let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set in - var result: Set = results[0] + let promise = when(fulfilled: snodePoolPromises).map2 { results -> Set in + var result: Set = results[0] results.forEach { result = result.intersection($0) } if result.count > 24 { // We want the snodes to agree on at least this many snodes // Limit the snode pool size to 256 so that we don't go too long without // refreshing it - return (result.count > 256) ? Set([Snode](result)[0..<256]) : result + return (result.count > 256) ? Set([Legacy.Snode](result)[0..<256]) : result } else { throw Error.inconsistentSnodePools } @@ -251,7 +251,7 @@ public final class SnodeAPI : NSObject { AnyPromise.from(getSnodePool()) } - public static func getSnodePool() -> Promise> { + public static func getSnodePool() -> Promise> { loadSnodePoolIfNeeded() let now = Date() let hasSnodePoolExpired = given(Storage.shared.getLastSnodePoolRefreshDate()) { now.timeIntervalSince($0) > 2 * 60 * 60 } ?? true @@ -259,7 +259,7 @@ public final class SnodeAPI : NSObject { let hasInsufficientSnodes = (snodePool.count < minSnodePoolCount) if hasInsufficientSnodes || hasSnodePoolExpired { if let getSnodePoolPromise = getSnodePoolPromise { return getSnodePoolPromise } - let promise: Promise> + let promise: Promise> if snodePool.count < minSnodePoolCount { promise = getSnodePoolFromSeedNode() } else { @@ -268,15 +268,15 @@ public final class SnodeAPI : NSObject { } } getSnodePoolPromise = promise - promise.map2 { snodePool -> Set in + promise.map2 { snodePool -> Set in if snodePool.isEmpty { throw Error.snodePoolUpdatingFailed } else { return snodePool } } - promise.then2 { snodePool -> Promise> in - let (promise, seal) = Promise>.pending() + promise.then2 { snodePool -> Promise> in + let (promise, seal) = Promise>.pending() SNSnodeKitConfiguration.shared.storage.write(with: { transaction in Storage.shared.setLastSnodePoolRefreshDate(to: now, using: transaction) setSnodePool(to: snodePool, using: transaction) @@ -366,15 +366,15 @@ public final class SnodeAPI : NSObject { return promise } - public static func getTargetSnodes(for publicKey: String) -> Promise<[Snode]> { + public static func getTargetSnodes(for publicKey: String) -> Promise<[Legacy.Snode]> { // shuffled() uses the system's default random generator, which is cryptographically secure return getSwarm(for: publicKey).map2 { Array($0.shuffled().prefix(targetSwarmSnodeCount)) } } - public static func getSwarm(for publicKey: String) -> Promise> { + public static func getSwarm(for publicKey: String) -> Promise> { loadSwarmIfNeeded(for: publicKey) if let cachedSwarm = swarmCache[publicKey], cachedSwarm.count >= minSwarmSnodeCount { - return Promise> { $0.fulfill(cachedSwarm) } + return Promise> { $0.fulfill(cachedSwarm) } } else { SNLog("Getting swarm for: \((publicKey == SNSnodeKitConfiguration.shared.storage.getUserPublicKey()) ? "self" : publicKey).") let parameters: [String:Any] = [ "pubKey" : Features.useTestnet ? publicKey.removing05PrefixIfNeeded() : publicKey ] @@ -390,7 +390,7 @@ public final class SnodeAPI : NSObject { } } - public static func getRawMessages(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise { + public static func getRawMessages(from snode: Legacy.Snode, associatedWith publicKey: String) -> RawResponsePromise { let (promise, seal) = RawResponsePromise.pending() Threading.workQueue.async { getMessagesInternal(from: snode, associatedWith: publicKey).done2 { seal.fulfill($0) }.catch2 { seal.reject($0) } @@ -412,7 +412,7 @@ public final class SnodeAPI : NSObject { return promise } - private static func getMessagesInternal(from snode: Snode, associatedWith publicKey: String) -> RawResponsePromise { + private static func getMessagesInternal(from snode: Legacy.Snode, associatedWith publicKey: String) -> RawResponsePromise { let storage = SNSnodeKitConfiguration.shared.storage // NOTE: All authentication logic is currently commented out, the reason being that we can't currently support @@ -468,7 +468,7 @@ public final class SnodeAPI : NSObject { return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { getSwarm(for: publicKey).then2 { swarm -> Promise<[String:Bool]> in let snode = swarm.randomElement()! - let verificationData = (Snode.Method.deleteMessage.rawValue + serverHashes.joined(separator: "")).data(using: String.Encoding.utf8)! + let verificationData = (Legacy.Snode.Method.deleteMessage.rawValue + serverHashes.joined(separator: "")).data(using: String.Encoding.utf8)! guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed } let parameters: JSON = [ "pubkey" : userX25519PublicKey, @@ -515,7 +515,7 @@ public final class SnodeAPI : NSObject { let snode = swarm.randomElement()! return attempt(maxRetryCount: maxRetryCount, recoveringOn: Threading.workQueue) { getNetworkTime(from: snode).then2 { timestamp -> Promise<[String:Bool]> in - let verificationData = (Snode.Method.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)! + let verificationData = (Legacy.Snode.Method.clearAllData.rawValue + String(timestamp)).data(using: String.Encoding.utf8)! guard let signature = sodium.sign.signature(message: Bytes(verificationData), secretKey: userED25519KeyPair.secretKey) else { throw Error.signingFailed } let parameters: JSON = [ "pubkey" : userX25519PublicKey, @@ -558,7 +558,7 @@ public final class SnodeAPI : NSObject { // The parsing utilities below use a best attempt approach to parsing; they warn for parsing failures but don't throw exceptions. - private static func parseSnodes(from rawResponse: Any) -> Set { + private static func parseSnodes(from rawResponse: Any) -> Set { guard let json = rawResponse as? JSON, let rawSnodes = json["snodes"] as? [JSON] else { SNLog("Failed to parse snodes from: \(rawResponse).") return [] @@ -569,17 +569,17 @@ public final class SnodeAPI : NSObject { SNLog("Failed to parse snode from: \(rawSnode).") return nil } - return Snode(address: "https://\(address)", port: port, publicKeySet: Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) + return Legacy.Snode(address: "https://\(address)", port: port, publicKeySet: Legacy.Snode.KeySet(ed25519Key: ed25519PublicKey, x25519Key: x25519PublicKey)) }) } - public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Snode, associatedWith publicKey: String) -> [JSON] { + public static func parseRawMessagesResponse(_ rawResponse: Any, from snode: Legacy.Snode, associatedWith publicKey: String) -> [JSON] { guard let json = rawResponse as? JSON, let rawMessages = json["messages"] as? [JSON] else { return [] } updateLastMessageHashValueIfPossible(for: snode, associatedWith: publicKey, from: rawMessages) return removeDuplicates(from: rawMessages, associatedWith: publicKey) } - private static func updateLastMessageHashValueIfPossible(for snode: Snode, associatedWith publicKey: String, from rawMessages: [JSON]) { + private static func updateLastMessageHashValueIfPossible(for snode: Legacy.Snode, associatedWith publicKey: String, from rawMessages: [JSON]) { if let lastMessage = rawMessages.last, let lastHash = lastMessage["hash"] as? String, let expirationDate = lastMessage["expiration"] as? UInt64 { SNSnodeKitConfiguration.shared.storage.writeSync { transaction in SNSnodeKitConfiguration.shared.storage.setLastMessageHashInfo(for: snode, associatedWith: publicKey, @@ -614,7 +614,7 @@ public final class SnodeAPI : NSObject { // MARK: Error Handling /// - Note: Should only be invoked from `Threading.workQueue` to avoid race conditions. @discardableResult - internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Snode, associatedWith publicKey: String? = nil) -> Error? { + internal static func handleError(withStatusCode statusCode: UInt, json: JSON?, forSnode snode: Legacy.Snode, associatedWith publicKey: String? = nil) -> Error? { #if DEBUG dispatchPrecondition(condition: .onQueue(Threading.workQueue)) #endif diff --git a/SessionSnodeKit/Storage+SnodeAPI.swift b/SessionSnodeKit/Storage+SnodeAPI.swift index 24b36365f..71df7c0a2 100644 --- a/SessionSnodeKit/Storage+SnodeAPI.swift +++ b/SessionSnodeKit/Storage+SnodeAPI.swift @@ -7,18 +7,18 @@ extension Storage { private static let snodePoolCollection = "LokiSnodePoolCollection" private static let lastSnodePoolRefreshDateCollection = "LokiLastSnodePoolRefreshDateCollection" - public func getSnodePool() -> Set { - var result: Set = [] + public func getSnodePool() -> Set { + var result: Set = [] Storage.read { transaction in transaction.enumerateKeysAndObjects(inCollection: Storage.snodePoolCollection) { _, object, _ in - guard let snode = object as? Snode else { return } + guard let snode = object as? Legacy.Snode else { return } result.insert(snode) } } return result } - public func setSnodePool(to snodePool: Set, using transaction: Any) { + public func setSnodePool(to snodePool: Set, using transaction: Any) { clearSnodePool(in: transaction) snodePool.forEach { snode in (transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: Storage.snodePoolCollection) @@ -49,20 +49,21 @@ extension Storage { return "LokiSwarmCollection-\(publicKey)" } - public func getSwarm(for publicKey: String) -> Set { - var result: Set = [] + public func getSwarm(for publicKey: String) -> Set { + var result: Set = [] let collection = Storage.getSwarmCollection(for: publicKey) Storage.read { transaction in transaction.enumerateKeysAndObjects(inCollection: collection) { _, object, _ in - guard let snode = object as? Snode else { return } + guard let snode = object as? Legacy.Snode else { return } result.insert(snode) } } return result } - public func setSwarm(to swarm: Set, for publicKey: String, using transaction: Any) { + public func setSwarm(to swarm: Set, for publicKey: String, using transaction: Any) { clearSwarm(for: publicKey, in: transaction) + let tmp = getSnodePool() let collection = Storage.getSwarmCollection(for: publicKey) swarm.forEach { snode in (transaction as! YapDatabaseReadWriteTransaction).setObject(snode, forKey: snode.description, inCollection: collection) @@ -80,7 +81,7 @@ extension Storage { private static let lastMessageHashCollection = "LokiLastMessageHashCollection" - public func getLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String) -> JSON? { + public func getLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String) -> JSON? { let key = "\(snode.address):\(snode.port).\(publicKey)" var result: JSON? Storage.read { transaction in @@ -93,17 +94,17 @@ extension Storage { return result } - public func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? { + public func getLastMessageHash(for snode: Legacy.Snode, associatedWith publicKey: String) -> String? { return getLastMessageHashInfo(for: snode, associatedWith: publicKey)?["hash"] as? String } - public func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) { + public func setLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) { let key = "\(snode.address):\(snode.port).\(publicKey)" guard lastMessageHashInfo.count == 2 && lastMessageHashInfo["hash"] as? String != nil && lastMessageHashInfo["expirationDate"] as? NSNumber != nil else { return } (transaction as! YapDatabaseReadWriteTransaction).setObject(lastMessageHashInfo, forKey: key, inCollection: Storage.lastMessageHashCollection) } - public func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String) { + public func pruneLastMessageHashInfoIfExpired(for snode: Legacy.Snode, associatedWith publicKey: String) { guard let lastMessageHashInfo = getLastMessageHashInfo(for: snode, associatedWith: publicKey), (lastMessageHashInfo["hash"] as? String) != nil, let expirationDate = (lastMessageHashInfo["expirationDate"] as? NSNumber)?.uint64Value else { return } let now = NSDate.millisecondTimestamp() @@ -114,7 +115,7 @@ extension Storage { } } - public func removeLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, using transaction: Any) { + public func removeLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, using transaction: Any) { let key = "\(snode.address):\(snode.port).\(publicKey)" (transaction as! YapDatabaseReadWriteTransaction).removeObject(forKey: key, inCollection: Storage.lastMessageHashCollection) } diff --git a/SessionSnodeKit/Storage.swift b/SessionSnodeKit/Storage.swift index ce0a90f81..7ea5e0d3d 100644 --- a/SessionSnodeKit/Storage.swift +++ b/SessionSnodeKit/Storage.swift @@ -14,15 +14,15 @@ public protocol SessionSnodeKitStorageProtocol { func getUserED25519KeyPair() -> Box.KeyPair? func getOnionRequestPaths() -> [OnionRequestAPI.Path] func setOnionRequestPaths(to paths: [OnionRequestAPI.Path], using transaction: Any) - func getSnodePool() -> Set - func setSnodePool(to snodePool: Set, using transaction: Any) + func getSnodePool() -> Set + func setSnodePool(to snodePool: Set, using transaction: Any) func getLastSnodePoolRefreshDate() -> Date? func setLastSnodePoolRefreshDate(to date: Date, using transaction: Any) - func getSwarm(for publicKey: String) -> Set - func setSwarm(to swarm: Set, for publicKey: String, using transaction: Any) - func getLastMessageHash(for snode: Snode, associatedWith publicKey: String) -> String? - func setLastMessageHashInfo(for snode: Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) - func pruneLastMessageHashInfoIfExpired(for snode: Snode, associatedWith publicKey: String) + func getSwarm(for publicKey: String) -> Set + func setSwarm(to swarm: Set, for publicKey: String, using transaction: Any) + func getLastMessageHash(for snode: Legacy.Snode, associatedWith publicKey: String) -> String? + func setLastMessageHashInfo(for snode: Legacy.Snode, associatedWith publicKey: String, to lastMessageHashInfo: JSON, using transaction: Any) + func pruneLastMessageHashInfoIfExpired(for snode: Legacy.Snode, associatedWith publicKey: String) func getReceivedMessages(for publicKey: String) -> Set func setReceivedMessages(to receivedMessages: Set, for publicKey: String, using transaction: Any) } diff --git a/SessionUtilitiesKit/Database/GRDBStorage.swift b/SessionUtilitiesKit/Database/GRDBStorage.swift new file mode 100644 index 000000000..a628cb220 --- /dev/null +++ b/SessionUtilitiesKit/Database/GRDBStorage.swift @@ -0,0 +1,224 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB +import SignalCoreKit + +enum GRDBStorageError: Error { // TODO: Rename to `StorageError` + case invalidKeySpec +} + +// TODO: Protocol for storage (just need to have 'read' and 'write' methods and mock 'Database'? + +// TODO: Rename to `Storage` +public final class GRDBStorage { + public static var shared: GRDBStorage! // TODO: Figure out how/if we want to do this + + private static let dbFileName: String = "Session.sqlite" + private static let keychainService: String = "TSKeyChainService" + private static let dbCipherKeySpecKey: String = "GRDBDatabaseCipherKeySpec" + private static let kSQLCipherKeySpecLength: Int32 = 48 + + private static var sharedDatabaseDirectoryPath: String { "\(OWSFileSystem.appSharedDataDirectoryPath())/database" } + private static var databasePath: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)" } + private static var databasePathShm: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)-shm" } + private static var databasePathWal: String { "\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)-wal" } + + private let dbPool: DatabasePool + private let migrator: DatabaseMigrator + + // MARK: - Initialization + + public init?( + migrations: [TargetMigrations] + ) throws { + print("RAWR START \("\(GRDBStorage.sharedDatabaseDirectoryPath)/\(GRDBStorage.dbFileName)")") + GRDBStorage.deleteDatabaseFiles() // TODO: Remove this + try! GRDBStorage.deleteDbKeys() // TODO: Remove this + + // Create the database directory if needed and ensure it's protection level is set before attempting to + // create the database KeySpec or the database itself + OWSFileSystem.ensureDirectoryExists(GRDBStorage.sharedDatabaseDirectoryPath) + OWSFileSystem.protectFileOrFolder(atPath: GRDBStorage.sharedDatabaseDirectoryPath) + + // Generate the database KeySpec if needed (this MUST be done before we try to access the database + // as a different thread might attempt to access the database before the key is successfully created) + // + // Note: We reset the bytes immediately after generation to ensure the database key doesn't hang + // around in memory unintentionally + var tmpKeySpec: Data = GRDBStorage.getOrGenerateDatabaseKeySpec() + tmpKeySpec.resetBytes(in: 0.. Data { + return try CurrentAppContext().keychainStorage().data(forService: keychainService, key: dbCipherKeySpecKey) + } + + @discardableResult private static func getOrGenerateDatabaseKeySpec() -> Data { + do { + var keySpec: Data = try getDatabaseCipherKeySpec() + defer { keySpec.resetBytes(in: 0..(updates: (Database) throws -> T) throws -> T { + return try dbPool.write(updates) + } + + public func writeAsync(updates: @escaping (Database) throws -> T, completion: @escaping (Database, Result) -> Void) { + dbPool.asyncWrite(updates, completion: completion) + } + + public func read(_ value: (Database) throws -> T) throws -> T { + return try dbPool.read(value) + } +} diff --git a/SessionUtilitiesKit/Database/SSKKeychainStorage.swift b/SessionUtilitiesKit/Database/SSKKeychainStorage.swift index e9243901f..175725798 100644 --- a/SessionUtilitiesKit/Database/SSKKeychainStorage.swift +++ b/SessionUtilitiesKit/Database/SSKKeychainStorage.swift @@ -6,12 +6,18 @@ import Foundation import SAMKeychain public enum KeychainStorageError: Error { - case failure(description: String) + case failure(code: Int32?, description: String) + + public var code: Int32? { + switch self { + case .failure(let code, _): return code + } + } } // MARK: - -@objc public protocol SSKKeychainStorage: class { +@objc public protocol SSKKeychainStorage: AnyObject { @objc func string(forService service: String, key: String) throws -> String @@ -40,10 +46,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { var error: NSError? let result = SAMKeychain.password(forService: service, account: key, error: &error) if let error = error { - throw KeychainStorageError.failure(description: "\(logTag) error retrieving string: \(error)") + throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving string: \(error)") } guard let string = result else { - throw KeychainStorageError.failure(description: "\(logTag) could not retrieve string") + throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve string") } return string } @@ -55,10 +61,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { var error: NSError? let result = SAMKeychain.setPassword(string, forService: service, account: key, error: &error) if let error = error { - throw KeychainStorageError.failure(description: "\(logTag) error setting string: \(error)") + throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting string: \(error)") } guard result else { - throw KeychainStorageError.failure(description: "\(logTag) could not set string") + throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set string") } } @@ -66,10 +72,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { var error: NSError? let result = SAMKeychain.passwordData(forService: service, account: key, error: &error) if let error = error { - throw KeychainStorageError.failure(description: "\(logTag) error retrieving data: \(error)") + throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error retrieving data: \(error)") } guard let data = result else { - throw KeychainStorageError.failure(description: "\(logTag) could not retrieve data") + throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not retrieve data") } return data } @@ -81,10 +87,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { var error: NSError? let result = SAMKeychain.setPasswordData(data, forService: service, account: key, error: &error) if let error = error { - throw KeychainStorageError.failure(description: "\(logTag) error setting data: \(error)") + throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error setting data: \(error)") } guard result else { - throw KeychainStorageError.failure(description: "\(logTag) could not set data") + throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not set data") } } @@ -96,10 +102,10 @@ public class SSKDefaultKeychainStorage: NSObject, SSKKeychainStorage { if error.code == errSecItemNotFound { return } - throw KeychainStorageError.failure(description: "\(logTag) error removing data: \(error)") + throw KeychainStorageError.failure(code: Int32(error.code), description: "\(logTag) error removing data: \(error)") } guard result else { - throw KeychainStorageError.failure(description: "\(logTag) could not remove data") + throw KeychainStorageError.failure(code: nil, description: "\(logTag) could not remove data") } } } diff --git a/SessionUtilitiesKit/Database/Types/ColumnExpressible.swift b/SessionUtilitiesKit/Database/Types/ColumnExpressible.swift new file mode 100644 index 000000000..434af47e4 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/ColumnExpressible.swift @@ -0,0 +1,8 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public protocol ColumnExpressible { + associatedtype Columns: ColumnExpression +} diff --git a/SessionUtilitiesKit/Database/Types/Migration.swift b/SessionUtilitiesKit/Database/Types/Migration.swift new file mode 100644 index 000000000..493e00d06 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/Migration.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public protocol Migration { + static var identifier: String { get } + + static func migrate(_ db: Database) throws +} diff --git a/SessionUtilitiesKit/Database/Types/SettingType.swift b/SessionUtilitiesKit/Database/Types/SettingType.swift new file mode 100644 index 000000000..0a8ab0dac --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/SettingType.swift @@ -0,0 +1,3 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation diff --git a/SessionUtilitiesKit/Database/Types/TargetMigrations.swift b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift new file mode 100644 index 000000000..83cb310a5 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/TargetMigrations.swift @@ -0,0 +1,59 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public struct TargetMigrations: Comparable { + /// This identifier is used to determine the order each set of migrations should run in. + /// + /// All migrations within a specific set will run first, followed by all migrations for the same set index in + /// the next `Identifier` before moving on to the next `MigrationSet`. So given the migrations: + /// + /// `{a: [1], [2, 3]}, {b: [4, 5], [6]}` + /// + /// the migrations will run in the following order: + /// + /// `a1, b4, b5, a2, a3, b6` + public enum Identifier: String, CaseIterable, Comparable { + // WARNING: The string version of these cases are used as migration identifiers so + // changing them will result in the migrations running again + case snodeKit + case messagingKit + + public static func < (lhs: Self, rhs: Self) -> Bool { + let lhsIndex: Int = (Identifier.allCases.firstIndex(of: lhs) ?? Identifier.allCases.count) + let rhsIndex: Int = (Identifier.allCases.firstIndex(of: rhs) ?? Identifier.allCases.count) + + return (lhsIndex < rhsIndex) + } + } + + public typealias MigrationSet = [Migration.Type] + + let identifier: Identifier + let migrations: [MigrationSet] + + // MARK: - Initialization + + public init( + identifier: Identifier, + migrations: [MigrationSet] + ) { + self.identifier = identifier + self.migrations = migrations + } + + // MARK: - Equatable + + public static func == (lhs: TargetMigrations, rhs: TargetMigrations) -> Bool { + return ( + lhs.identifier == rhs.identifier && + lhs.migrations.count == rhs.migrations.count + ) + } + + // MARK: - Comparable + + public static func < (lhs: Self, rhs: Self) -> Bool { + return (lhs.identifier < rhs.identifier) + } +} diff --git a/SessionUtilitiesKit/Database/Types/TypedTableDefinition.swift b/SessionUtilitiesKit/Database/Types/TypedTableDefinition.swift new file mode 100644 index 000000000..8a5b86c92 --- /dev/null +++ b/SessionUtilitiesKit/Database/Types/TypedTableDefinition.swift @@ -0,0 +1,33 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +/// This is a convenience wrapper around the GRDB `TableDefinition` class which allows for shorthand +/// when creating tables +public class TypedTableDefinition where T: TableRecord, T: ColumnExpressible { + let definition: TableDefinition + + init(definition: TableDefinition) { + self.definition = definition + } + + @discardableResult public func column(_ key: T.Columns, _ type: Database.ColumnType? = nil) -> ColumnDefinition { + return definition.column(key.name, type) + } + + public func primaryKey(_ columns: [T.Columns], onConflict: Database.ConflictResolution? = nil) { + definition.primaryKey(columns.map { $0.name }, onConflict: onConflict) + } + + public func foreignKey(_ columns: [T.Columns], references table: Other.Type, columns destinationColumns: [Other.Columns]? = nil, onDelete: Database.ForeignKeyAction? = nil, onUpdate: Database.ForeignKeyAction? = nil, deferred: Bool = false) where Other: TableRecord, Other: ColumnExpressible { + return definition.foreignKey( + columns.map { $0.name }, + references: table.databaseTableName, + columns: destinationColumns?.map { $0.name }, + onDelete: onDelete, + onUpdate: onUpdate, + deferred: deferred + ) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/ColumnDefinition+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/ColumnDefinition+Utilities.swift new file mode 100644 index 000000000..1684441ff --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/ColumnDefinition+Utilities.swift @@ -0,0 +1,22 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension ColumnDefinition { + @discardableResult func references( + _ table: T.Type, + column: T.Columns? = nil, + onDelete deleteAction: Database.ForeignKeyAction? = nil, + onUpdate updateAction: Database.ForeignKeyAction? = nil, + deferred: Bool = false + ) -> Self where T: TableRecord, T: ColumnExpressible { + return references( + T.databaseTableName, + column: column?.name, + onDelete: deleteAction, + onUpdate: updateAction, + deferred: deferred + ) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift new file mode 100644 index 000000000..7ab658bb5 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/Database+Utilities.swift @@ -0,0 +1,18 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension Database { + func create( + table: T.Type, + options: TableOptions = [], + body: (TypedTableDefinition) throws -> Void + ) throws where T: TableRecord, T: ColumnExpressible { + try create(table: T.databaseTableName, options: options) { tableDefinition in + let typedDefinition: TypedTableDefinition = TypedTableDefinition(definition: tableDefinition) + + try body(typedDefinition) + } + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift new file mode 100644 index 000000000..ccc073f35 --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/DatabaseMigrator+Utilities.swift @@ -0,0 +1,10 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation +import GRDB + +public extension DatabaseMigrator { + mutating func registerMigration(_ identifier: TargetMigrations.Identifier, migration: Migration.Type, foreignKeyChecks: ForeignKeyChecks = .deferred) { + self.registerMigration("\(identifier).\(migration.identifier)", migrate: migration.migrate) + } +} diff --git a/SessionUtilitiesKit/Database/Utilities/GRDB+Notifications.swift b/SessionUtilitiesKit/Database/Utilities/GRDB+Notifications.swift new file mode 100644 index 000000000..fe1d4f95e --- /dev/null +++ b/SessionUtilitiesKit/Database/Utilities/GRDB+Notifications.swift @@ -0,0 +1,11 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Notification.Name { + static let resetStorage = Notification.Name("resetStorage") +} + +@objc public extension NSNotification { + @objc static let resetStorage = Notification.Name.resetStorage.rawValue as NSString +} diff --git a/SessionUtilitiesKit/General/Set+Utilities.swift b/SessionUtilitiesKit/General/Set+Utilities.swift new file mode 100644 index 000000000..a5880927d --- /dev/null +++ b/SessionUtilitiesKit/General/Set+Utilities.swift @@ -0,0 +1,19 @@ +// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public extension Set { + func inserting(_ value: Element) -> Set { + var updatedSet: Set = self + updatedSet.insert(value) + + return updatedSet + } + + func removing(_ value: Element) -> Set { + var updatedSet: Set = self + updatedSet.remove(value) + + return updatedSet + } +} diff --git a/SignalUtilitiesKit/Configuration.swift b/SignalUtilitiesKit/Configuration.swift index 11234855e..b32479eb4 100644 --- a/SignalUtilitiesKit/Configuration.swift +++ b/SignalUtilitiesKit/Configuration.swift @@ -3,12 +3,34 @@ import SessionSnodeKit extension OWSPrimaryStorage : OWSPrimaryStorageProtocol { } +var isSetup: Bool = false // TODO: Remove this + @objc(SNConfiguration) public final class Configuration : NSObject { + @objc public static func performMainSetup() { + // Need to do this first to ensure the legacy database exists + SNUtilitiesKit.configure( + owsPrimaryStorage: OWSPrimaryStorage.shared(), + maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier) + ) + + if !isSetup { + isSetup = true + + // TODO: Need to store this result somewhere? + // TODO: This function seems to get called multiple times + //DispatchQueue.main.once + let storage: GRDBStorage? = try? GRDBStorage( + migrations: [ + SNSnodeKit.migrations(), + SNMessagingKit.migrations() + ] + ) + } + SNMessagingKit.configure(storage: Storage.shared) SNSnodeKit.configure(storage: Storage.shared) - SNUtilitiesKit.configure(owsPrimaryStorage: OWSPrimaryStorage.shared(), maxFileSize: UInt(Double(FileServerAPIV2.maxFileSize) / FileServerAPIV2.fileSizeORMultiplier)) } }