Started working on integrating the updated push APIs
Updated the PushNotificationAPI to be more consistent with the SnodeAPI and OpenGroupAPI structures Updated the logic so if the database key can't be retrieved the app will no longer throw a fatalError (now just fail to initialise Storage and rely on the App/Extensions to properly handle this case) Fixed a couple of bugs where the share extension wouldn't populate correctly
This commit is contained in:
parent
9799297e15
commit
4330a40f6f
|
@ -758,6 +758,15 @@
|
|||
FDBB25E12983909300F1508E /* ConfigConvoInfoVolatileSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */; };
|
||||
FDBB25E32988B13800F1508E /* _004_AddJobPriority.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */; };
|
||||
FDBB25E72988BBBE00F1508E /* UIContextualAction+Theming.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */; };
|
||||
FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */; };
|
||||
FDC13D492A16EC20007267C7 /* Service.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D482A16EC20007267C7 /* Service.swift */; };
|
||||
FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */; };
|
||||
FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */; };
|
||||
FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */; };
|
||||
FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */; };
|
||||
FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */; };
|
||||
FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */; };
|
||||
FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */; };
|
||||
FDC2908727D7047F005DAE71 /* RoomSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908627D7047F005DAE71 /* RoomSpec.swift */; };
|
||||
FDC2908927D70656005DAE71 /* RoomPollInfoSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */; };
|
||||
FDC2908B27D707F3005DAE71 /* SendMessageRequestSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */; };
|
||||
|
@ -778,7 +787,7 @@
|
|||
FDC4381527B329CE00C60D73 /* NonceGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381427B329CE00C60D73 /* NonceGenerator.swift */; };
|
||||
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381627B32EC700C60D73 /* Personalization.swift */; };
|
||||
FDC4382027B36ADC00C60D73 /* SOGSEndpoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */; };
|
||||
FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */; };
|
||||
FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */; };
|
||||
FDC4383827B3863200C60D73 /* VersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* VersionResponse.swift */; };
|
||||
FDC4385D27B4C18900C60D73 /* Room.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385C27B4C18900C60D73 /* Room.swift */; };
|
||||
FDC4385F27B4C4A200C60D73 /* PinnedMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4385E27B4C4A200C60D73 /* PinnedMessage.swift */; };
|
||||
|
@ -1890,6 +1899,15 @@
|
|||
FDBB25E02983909300F1508E /* ConfigConvoInfoVolatileSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfigConvoInfoVolatileSpec.swift; sourceTree = "<group>"; };
|
||||
FDBB25E22988B13800F1508E /* _004_AddJobPriority.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _004_AddJobPriority.swift; sourceTree = "<group>"; };
|
||||
FDBB25E62988BBBD00F1508E /* UIContextualAction+Theming.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIContextualAction+Theming.swift"; sourceTree = "<group>"; };
|
||||
FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeRequest.swift; sourceTree = "<group>"; };
|
||||
FDC13D482A16EC20007267C7 /* Service.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Service.swift; sourceTree = "<group>"; };
|
||||
FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscribeResponse.swift; sourceTree = "<group>"; };
|
||||
FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIEndpoint.swift; sourceTree = "<group>"; };
|
||||
FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushNotificationAPIRequest.swift; sourceTree = "<group>"; };
|
||||
FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyGroupRequest.swift; sourceTree = "<group>"; };
|
||||
FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = "<group>"; };
|
||||
FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = "<group>"; };
|
||||
FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = "<group>"; };
|
||||
FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = "<group>"; };
|
||||
FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = "<group>"; };
|
||||
FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = "<group>"; };
|
||||
|
@ -1909,7 +1927,7 @@
|
|||
FDC4381427B329CE00C60D73 /* NonceGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonceGenerator.swift; sourceTree = "<group>"; };
|
||||
FDC4381627B32EC700C60D73 /* Personalization.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Personalization.swift; sourceTree = "<group>"; };
|
||||
FDC4381F27B36ADC00C60D73 /* SOGSEndpoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SOGSEndpoint.swift; sourceTree = "<group>"; };
|
||||
FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PushServerResponse.swift; sourceTree = "<group>"; };
|
||||
FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyPushServerResponse.swift; sourceTree = "<group>"; };
|
||||
FDC4383727B3863200C60D73 /* VersionResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VersionResponse.swift; sourceTree = "<group>"; };
|
||||
FDC4383D27B4708600C60D73 /* Atomic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Atomic.swift; sourceTree = "<group>"; };
|
||||
FDC4385C27B4C18900C60D73 /* Room.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Room.swift; sourceTree = "<group>"; };
|
||||
|
@ -3151,6 +3169,7 @@
|
|||
C379DC6825672B5E0002D4EB /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDC13D4E2A16EE41007267C7 /* Types */,
|
||||
FDC4382D27B383A600C60D73 /* Models */,
|
||||
FDF0B7502807BA56004C14C5 /* NotificationsProtocol.swift */,
|
||||
C33FDBDE255A581900E217F9 /* PushNotificationAPI.swift */,
|
||||
|
@ -4140,6 +4159,15 @@
|
|||
path = Configs;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FDC13D4E2A16EE41007267C7 /* Types */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDC13D482A16EC20007267C7 /* Service.swift */,
|
||||
FDC13D4F2A16EE50007267C7 /* PushNotificationAPIEndpoint.swift */,
|
||||
);
|
||||
path = Types;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
FDC2909227D710A9005DAE71 /* Types */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
|
@ -4192,7 +4220,14 @@
|
|||
FDC4382D27B383A600C60D73 /* Models */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
FDC4382E27B383AF00C60D73 /* PushServerResponse.swift */,
|
||||
FDC13D512A16F22E007267C7 /* PushNotificationAPIRequest.swift */,
|
||||
FDC13D462A16E4CA007267C7 /* SubscribeRequest.swift */,
|
||||
FDC13D4A2A16ECBA007267C7 /* SubscribeResponse.swift */,
|
||||
FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */,
|
||||
FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */,
|
||||
FDC13D532A16FF29007267C7 /* LegacyGroupRequest.swift */,
|
||||
FDC4382E27B383AF00C60D73 /* LegacyPushServerResponse.swift */,
|
||||
FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */,
|
||||
);
|
||||
path = Models;
|
||||
sourceTree = "<group>";
|
||||
|
@ -5703,6 +5738,7 @@
|
|||
FD245C672850665E00B966DD /* AttachmentDownloadJob.swift in Sources */,
|
||||
C300A5D32554B05A00555489 /* TypingIndicator.swift in Sources */,
|
||||
7B521E0A29BFF84400C3C36A /* GroupLeavingJob.swift in Sources */,
|
||||
FDC13D582A17207D007267C7 /* UnsubscribeResponse.swift in Sources */,
|
||||
FD09799927FFC1A300936362 /* Attachment.swift in Sources */,
|
||||
FD245C5F2850662200B966DD /* OWSWindowManager.m in Sources */,
|
||||
C3471ECB2555356A00297E91 /* MessageSender+Encryption.swift in Sources */,
|
||||
|
@ -5749,6 +5785,7 @@
|
|||
FD37EA0F28AB3330003AE748 /* _006_FixHiddenModAdminSupport.swift in Sources */,
|
||||
FD2B4AFD294688D000AB4848 /* SessionUtil+Contacts.swift in Sources */,
|
||||
7B81682328A4C1210069F315 /* UpdateTypes.swift in Sources */,
|
||||
FDC13D472A16E4CA007267C7 /* SubscribeRequest.swift in Sources */,
|
||||
FD2B4AFF2946C93200AB4848 /* ConfigurationSyncJob.swift in Sources */,
|
||||
FDC438A627BB113A00C60D73 /* UserUnbanRequest.swift in Sources */,
|
||||
FD5C72FB284F0EA10029977D /* MessageReceiver+DataExtractionNotification.swift in Sources */,
|
||||
|
@ -5757,6 +5794,7 @@
|
|||
C379DCF4256735770002D4EB /* VisibleMessage+Attachment.swift in Sources */,
|
||||
FDB4BBC72838B91E00B7C95D /* LinkPreviewError.swift in Sources */,
|
||||
FD09798327FD1A1500936362 /* ClosedGroup.swift in Sources */,
|
||||
FDC13D542A16FF29007267C7 /* LegacyGroupRequest.swift in Sources */,
|
||||
B8B320B7258C30D70020074B /* HTMLMetadata.swift in Sources */,
|
||||
FD8ECF8B2935DB4B00C0D1BB /* SharedConfigMessage.swift in Sources */,
|
||||
FD09798727FD1B7800936362 /* GroupMember.swift in Sources */,
|
||||
|
@ -5766,6 +5804,7 @@
|
|||
FD17D79927F40AB800122BE0 /* _003_YDBToGRDBMigration.swift in Sources */,
|
||||
FDF0B7512807BA56004C14C5 /* NotificationsProtocol.swift in Sources */,
|
||||
B8DE1FB426C22F2F0079C9CE /* WebRTCSession.swift in Sources */,
|
||||
FDC13D5A2A1721C5007267C7 /* LegacyNotifyRequest.swift in Sources */,
|
||||
FDC6D6F32860607300B04575 /* Environment.swift in Sources */,
|
||||
C3A3A171256E1D25004D228D /* SSKReachabilityManager.swift in Sources */,
|
||||
FD245C59285065FC00B966DD /* ControlMessage.swift in Sources */,
|
||||
|
@ -5794,16 +5833,18 @@
|
|||
FD6A7A692818BE7300035AC1 /* RetrieveDefaultOpenGroupRoomsJob.swift in Sources */,
|
||||
FD09798D27FD1D8900936362 /* DisappearingMessageConfiguration.swift in Sources */,
|
||||
FDF0B75A2807F3A3004C14C5 /* MessageSenderError.swift in Sources */,
|
||||
FDC4382F27B383AF00C60D73 /* PushServerResponse.swift in Sources */,
|
||||
FDC4382F27B383AF00C60D73 /* LegacyPushServerResponse.swift in Sources */,
|
||||
FDC4386327B4D94E00C60D73 /* SOGSMessage.swift in Sources */,
|
||||
FD245C692850666800B966DD /* ExpirationTimerUpdate.swift in Sources */,
|
||||
FD42F9A8285064B800A0C77D /* PushNotificationAPI.swift in Sources */,
|
||||
FD245C6C2850669200B966DD /* MessageReceiveJob.swift in Sources */,
|
||||
FD43EE9D297A5190009C87C5 /* SessionUtil+UserGroups.swift in Sources */,
|
||||
FD83B9CC27D179BC005E1583 /* FSEndpoint.swift in Sources */,
|
||||
FDC13D4B2A16ECBA007267C7 /* SubscribeResponse.swift in Sources */,
|
||||
FD7115F228C6CB3900B47552 /* _010_AddThreadIdToFTS.swift in Sources */,
|
||||
FD716E6428502DDD00C96BF4 /* CallManagerProtocol.swift in Sources */,
|
||||
FDC438C727BB6DF000C60D73 /* DirectMessage.swift in Sources */,
|
||||
FDC13D502A16EE50007267C7 /* PushNotificationAPIEndpoint.swift in Sources */,
|
||||
FD432434299C6985008A0213 /* PendingReadReceipt.swift in Sources */,
|
||||
FDC4381727B32EC700C60D73 /* Personalization.swift in Sources */,
|
||||
FD245C51285065CC00B966DD /* MessageReceiver.swift in Sources */,
|
||||
|
@ -5831,7 +5872,9 @@
|
|||
FD09796E27FA6D0000936362 /* Contact.swift in Sources */,
|
||||
C38D5E8D2575011E00B6A65C /* MessageSender+ClosedGroups.swift in Sources */,
|
||||
FD5C72F7284F0E560029977D /* MessageReceiver+ReadReceipts.swift in Sources */,
|
||||
FDC13D492A16EC20007267C7 /* Service.swift in Sources */,
|
||||
FD778B6429B189FF001BAC6B /* _014_GenerateInitialUserConfigDumps.swift in Sources */,
|
||||
FDC13D562A171FE4007267C7 /* UnsubscribeRequest.swift in Sources */,
|
||||
C32C598A256D0664003C73A2 /* SNProtoEnvelope+Conversion.swift in Sources */,
|
||||
FDC438CB27BB7DB100C60D73 /* UpdateMessageRequest.swift in Sources */,
|
||||
FD8ECF7F2934298100C0D1BB /* ConfigDump.swift in Sources */,
|
||||
|
@ -5854,6 +5897,7 @@
|
|||
FD245C682850666300B966DD /* Message+Destination.swift in Sources */,
|
||||
FDF8488029405994007DCAE5 /* HTTPQueryParam+OpenGroup.swift in Sources */,
|
||||
FD09798527FD1A6500936362 /* ClosedGroupKeyPair.swift in Sources */,
|
||||
FDC13D522A16F22E007267C7 /* PushNotificationAPIRequest.swift in Sources */,
|
||||
FD245C632850664600B966DD /* Configuration.swift in Sources */,
|
||||
C32C5DBF256DD743003C73A2 /* ClosedGroupPoller.swift in Sources */,
|
||||
C352A35B2557824E00338F3E /* AttachmentUploadJob.swift in Sources */,
|
||||
|
|
|
@ -74,7 +74,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: .finishLaunching, error: error)
|
||||
self?.showDatabaseSetupFailureModal(calledFrom: .finishLaunching, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -145,7 +145,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: .enterForeground, error: error)
|
||||
self?.showDatabaseSetupFailureModal(calledFrom: .enterForeground, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -330,15 +330,20 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
}
|
||||
}
|
||||
|
||||
private func showFailedMigrationAlert(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) {
|
||||
private func showDatabaseSetupFailureModal(calledFrom lifecycleMethod: LifecycleMethod, error: Error?) {
|
||||
let alert = UIAlertController(
|
||||
title: "Session",
|
||||
message: "DATABASE_MIGRATION_FAILED".localized(),
|
||||
message: {
|
||||
switch (error as? StorageError) {
|
||||
case .databaseInvalid: return "DATABASE_SETUP_FAILED".localized()
|
||||
default: return "DATABASE_MIGRATION_FAILED".localized()
|
||||
}
|
||||
}(),
|
||||
preferredStyle: .alert
|
||||
)
|
||||
alert.addAction(UIAlertAction(title: "HELP_REPORT_BUG_ACTION_TITLE".localized(), style: .default) { _ in
|
||||
HelpViewModel.shareLogs(viewControllerToDismiss: alert) { [weak self] in
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||
self?.showDatabaseSetupFailureModal(calledFrom: lifecycleMethod, error: error)
|
||||
}
|
||||
})
|
||||
alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { _ in
|
||||
|
@ -359,7 +364,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
|||
},
|
||||
migrationsCompletion: { [weak self] result, needsConfigSync in
|
||||
if case .failure(let error) = result {
|
||||
self?.showFailedMigrationAlert(calledFrom: lifecycleMethod, error: error)
|
||||
self?.showDatabaseSetupFailureModal(calledFrom: lifecycleMethod, error: error)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "متاسفانه خطایی رخ داده است";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "لطفا بعدا دوباره تلاش کنید";
|
||||
"LOADING_CONVERSATIONS" = "درحال بارگزاری پیام ها...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "هنگام بهینهسازی پایگاه داده خطایی روی داد\n\nشما میتوانید گزارشهای برنامه خود را صادر کنید تا بتوانید برای عیبیابی به اشتراک بگذارید یا میتوانید دستگاه خود را بازیابی کنید\n\nهشدار: بازیابی دستگاه شما منجر به از دست رفتن دادههای قدیمیتر از دو هفته میشود.";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "مشکلی پیش آمد. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "به نظر می رسد کلمات کافی وارد نکرده اید. لطفاً عبارت بازیابی خود را بررسی کنید و دوباره امتحان کنید.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oups, une erreur est survenue";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Veuillez réessayer plus tard";
|
||||
"LOADING_CONVERSATIONS" = "Chargement des conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "Une erreur est survenue pendant l'optimisation de la base de données\n\nVous pouvez exporter votre journal d'application pour le partager et aider à régler le problème ou vous pouvez restaurer votre appareil\n\nAttention : restaurer votre appareil résultera en une perte des données des deux dernières semaines";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Quelque chose s'est mal passé. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Il semble que vous n'avez pas saisi tous les mots. Vérifiez votre phrase de récupération et réessayez s'il vous plaît.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -414,6 +414,7 @@
|
|||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_TITLE" = "Oops, an error occurred";
|
||||
"DEFAULT_OPEN_GROUP_LOAD_ERROR_SUBTITLE" = "Please try again later";
|
||||
"LOADING_CONVERSATIONS" = "Loading Conversations...";
|
||||
"DATABASE_SETUP_FAILED" = "An error occurred when opening the database, please restart to try again\n\nIf you contineu to see this error you can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"DATABASE_MIGRATION_FAILED" = "An error occurred when optimising the database\n\nYou can export your application logs to be able to share for troubleshooting or you can restore your device\n\nWarning: Restoring your device will result in loss of any data older than two weeks";
|
||||
"RECOVERY_PHASE_ERROR_GENERIC" = "Something went wrong. Please check your recovery phrase and try again.";
|
||||
"RECOVERY_PHASE_ERROR_LENGTH" = "Looks like you didn't enter enough words. Please check your recovery phrase and try again.";
|
||||
|
@ -641,3 +642,4 @@
|
|||
"CONVERSATION_EMPTY_STATE_READ_ONLY" = "There are no messages in %@.";
|
||||
"CONVERSATION_EMPTY_STATE_NOTE_TO_SELF" = "You have no messages in %@.";
|
||||
"USER_CONFIG_OUTDATED_WARNING" = "Some of your devices are using outdated versions. Syncing may be unreliable until they are updated.";
|
||||
"database_inaccessible_error" = "There is an issue opening the database. Please restart the app and try again.";
|
||||
|
|
|
@ -155,15 +155,15 @@ extension SyncPushTokensJob {
|
|||
.setFailureType(to: Error.self)
|
||||
.flatMap { pushTokenAsData -> AnyPublisher<Bool, Error> in
|
||||
guard isUsingFullAPNs else {
|
||||
return PushNotificationAPI.unregister(pushTokenAsData)
|
||||
return PushNotificationAPI
|
||||
.unsubscribe(token: pushTokenAsData)
|
||||
.map { _ in true }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return PushNotificationAPI
|
||||
.register(
|
||||
with: pushTokenAsData,
|
||||
publicKey: getUserHexEncodedPublicKey(),
|
||||
.subscribe(
|
||||
token: pushTokenAsData,
|
||||
isForcedUpdate: isForcedUpdate
|
||||
)
|
||||
.map { _ in true }
|
||||
|
|
|
@ -225,8 +225,9 @@ final class NukeDataModal: Modal {
|
|||
let maybeDeviceToken: String? = UserDefaults.standard[.deviceToken]
|
||||
|
||||
if isUsingFullAPNs, let deviceToken: String = maybeDeviceToken {
|
||||
let data: Data = Data(hex: deviceToken)
|
||||
PushNotificationAPI.unregister(data).sinkUntilComplete()
|
||||
PushNotificationAPI
|
||||
.unsubscribe(token: Data(hex: deviceToken))
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
|
||||
// Clear the app badge and notifications
|
||||
|
|
|
@ -144,10 +144,9 @@ public extension ClosedGroup {
|
|||
ClosedGroupPoller.shared.stopPolling(for: threadId)
|
||||
|
||||
PushNotificationAPI
|
||||
.performOperation(
|
||||
.unsubscribe,
|
||||
for: threadId,
|
||||
publicKey: userPublicKey
|
||||
.unsubscribeFromLegacyGroup(
|
||||
legacyGroupId: threadId,
|
||||
currentUserPublicKey: userPublicKey
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import Combine
|
|||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
// FIXME: Remove this once legacy notifications and legacy groups are deprecated
|
||||
public enum NotifyPushServerJob: JobExecutor {
|
||||
public static var maxFailureCount: Int = 20
|
||||
public static var requiresThreadId: Bool = false
|
||||
|
@ -26,7 +27,7 @@ public enum NotifyPushServerJob: JobExecutor {
|
|||
}
|
||||
|
||||
PushNotificationAPI
|
||||
.notify(
|
||||
.legacyNotify(
|
||||
recipient: details.message.recipient,
|
||||
with: details.message.data,
|
||||
maxRetryCount: 4
|
||||
|
|
|
@ -89,7 +89,7 @@ extension OpenGroupAPI.Message {
|
|||
throw HTTPError.parsingFailed
|
||||
}
|
||||
|
||||
case .none:
|
||||
case .none, .group:
|
||||
SNLog("Ignoring message with invalid sender.")
|
||||
throw HTTPError.parsingFailed
|
||||
}
|
||||
|
|
|
@ -992,6 +992,8 @@ public final class OpenGroupManager {
|
|||
.filter(possibleKeys.contains(GroupMember.Columns.profileId))
|
||||
.filter(targetRoles.contains(GroupMember.Columns.role))
|
||||
.isNotEmpty(db)
|
||||
|
||||
case .group: return false
|
||||
}
|
||||
}
|
||||
.defaulting(to: false)
|
||||
|
|
|
@ -192,8 +192,13 @@ extension MessageReceiver {
|
|||
// Start polling
|
||||
ClosedGroupPoller.shared.startIfNeeded(for: groupPublicKey)
|
||||
|
||||
// Notify the PN server
|
||||
let _ = PushNotificationAPI.performOperation(.subscribe, for: groupPublicKey, publicKey: getUserHexEncodedPublicKey(db))
|
||||
// Subscribe for push notifications
|
||||
PushNotificationAPI
|
||||
.subscribeToLegacyGroup(
|
||||
legacyGroupId: groupPublicKey,
|
||||
currentUserPublicKey: getUserHexEncodedPublicKey(db)
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
|
||||
/// Extracts and adds the new encryption key pair to our list of key pairs if there is one for our public key, AND the message was
|
||||
|
@ -479,11 +484,12 @@ extension MessageReceiver {
|
|||
.keyPairs
|
||||
.deleteAll(db)
|
||||
|
||||
let _ = PushNotificationAPI.performOperation(
|
||||
.unsubscribe,
|
||||
for: threadId,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
PushNotificationAPI
|
||||
.unsubscribeFromLegacyGroup(
|
||||
legacyGroupId: threadId,
|
||||
currentUserPublicKey: userPublicKey
|
||||
)
|
||||
.sinkUntilComplete()
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
|
@ -71,7 +71,7 @@ extension MessageReceiver {
|
|||
|
||||
return try? OpenGroup.fetchOne(db, id: threadId)
|
||||
}()
|
||||
let variant: Interaction.Variant = {
|
||||
let variant: Interaction.Variant = try {
|
||||
guard
|
||||
let senderSessionId: SessionId = SessionId(from: sender),
|
||||
let openGroup: OpenGroup = maybeOpenGroup
|
||||
|
@ -106,6 +106,10 @@ extension MessageReceiver {
|
|||
.standardOutgoing :
|
||||
.standardIncoming
|
||||
)
|
||||
|
||||
case .group:
|
||||
SNLog("Ignoring message with invalid sender.")
|
||||
throw HTTPError.parsingFailed
|
||||
}
|
||||
}()
|
||||
|
||||
|
|
|
@ -117,11 +117,10 @@ extension MessageSender {
|
|||
memberSendData
|
||||
.map { MessageSender.sendImmediate(preparedSendData: $0) }
|
||||
.appending(
|
||||
// Notify the PN server
|
||||
PushNotificationAPI.performOperation(
|
||||
.subscribe,
|
||||
for: groupPublicKey,
|
||||
publicKey: userPublicKey
|
||||
// Subscribe for push notifications (if enabled)
|
||||
PushNotificationAPI.subscribeToLegacyGroup(
|
||||
legacyGroupId: groupPublicKey,
|
||||
currentUserPublicKey: userPublicKey
|
||||
)
|
||||
)
|
||||
)
|
||||
|
|
|
@ -64,6 +64,11 @@ public enum MessageReceiver {
|
|||
userEd25519KeyPair: userEd25519KeyPair,
|
||||
using: dependencies
|
||||
)
|
||||
|
||||
case .group:
|
||||
// TODO: Need to decide how we will handle updated group messages
|
||||
SNLog("Ignoring message with invalid sender.")
|
||||
throw HTTPError.parsingFailed
|
||||
}
|
||||
|
||||
case .closedGroupMessage:
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct LegacyGroupRequest: Codable {
|
||||
let pubKey: String
|
||||
let closedGroupPublicKey: String
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct LegacyNotifyRequest: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case data
|
||||
case sendTo = "send_to"
|
||||
}
|
||||
|
||||
let data: String
|
||||
let sendTo: String
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct PushServerResponse: Codable {
|
||||
struct LegacyPushServerResponse: Codable {
|
||||
let code: Int
|
||||
let message: String?
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public struct PushNotificationAPIRequest<T: Encodable>: Encodable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case method
|
||||
case body = "params"
|
||||
}
|
||||
|
||||
internal let endpoint: PushNotificationAPI.Endpoint
|
||||
internal let body: T
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
public init(
|
||||
endpoint: PushNotificationAPI.Endpoint,
|
||||
body: T
|
||||
) {
|
||||
self.endpoint = endpoint
|
||||
self.body = body
|
||||
}
|
||||
|
||||
// MARK: - Codable
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
try container.encode(endpoint.rawValue, forKey: .method)
|
||||
try container.encode(body, forKey: .body)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,153 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionSnodeKit
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct SubscribeRequest: Encodable {
|
||||
struct ServiceInfo: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case token
|
||||
}
|
||||
|
||||
private let token: String
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(token: String) {
|
||||
self.token = token
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pubkey
|
||||
case ed25519PublicKey = "session_ed25519"
|
||||
case subkey = "subkey_tag"
|
||||
case namespaces
|
||||
case includeMessageData = "data"
|
||||
case timestamp = "sig_ts"
|
||||
case signatureBase64 = "signature"
|
||||
case service
|
||||
case serviceInfo = "service_info"
|
||||
case notificationsEncryptionKey = "enc_key"
|
||||
}
|
||||
|
||||
/// The 33-byte account being subscribed to; typically a session ID.
|
||||
private let pubkey: String
|
||||
|
||||
/// List of integer namespace (-32768 through 32767). These must be sorted in ascending order.
|
||||
private let namespaces: [SnodeAPI.Namespace]
|
||||
|
||||
/// If provided and true then notifications will include the body of the message (as long as it isn't too large); if false then the body will
|
||||
/// not be included in notifications.
|
||||
private let includeMessageData: Bool
|
||||
|
||||
/// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the
|
||||
/// future may have different input requirements.
|
||||
private let serviceInfo: ServiceInfo
|
||||
|
||||
/// 32-byte encryption key; notification payloads sent to the device will be encrypted with XChaCha20-Poly1305 using this key. Though
|
||||
/// it is permitted for this to change, it is recommended that the device generate this once and persist it.
|
||||
private let notificationsEncryptionKey: Data
|
||||
|
||||
/// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth
|
||||
private let subkey: String?
|
||||
|
||||
/// The signature unix timestamp (seconds, not ms)
|
||||
private let timestamp: TimeInterval
|
||||
|
||||
/// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session
|
||||
/// ID. When not 05, this field should not be provided.
|
||||
private let ed25519PublicKey: [UInt8]
|
||||
|
||||
/// Secret key used to generate the signature (**Not** sent with the request)
|
||||
private let ed25519SecretKey: [UInt8]
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
pubkey: String,
|
||||
namespaces: [SnodeAPI.Namespace],
|
||||
includeMessageData: Bool,
|
||||
serviceInfo: ServiceInfo,
|
||||
notificationsEncryptionKey: Data,
|
||||
subkey: String?,
|
||||
timestamp: TimeInterval,
|
||||
ed25519PublicKey: [UInt8],
|
||||
ed25519SecretKey: [UInt8]
|
||||
) {
|
||||
self.pubkey = pubkey
|
||||
self.namespaces = namespaces
|
||||
self.includeMessageData = includeMessageData
|
||||
self.serviceInfo = serviceInfo
|
||||
self.notificationsEncryptionKey = notificationsEncryptionKey
|
||||
self.subkey = subkey
|
||||
self.timestamp = timestamp
|
||||
self.ed25519PublicKey = ed25519PublicKey
|
||||
self.ed25519SecretKey = ed25519SecretKey
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Generate the signature for the request for encoding
|
||||
let signatureBase64: String = try generateSignature().toBase64()
|
||||
try container.encode(pubkey, forKey: .pubkey)
|
||||
try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey)
|
||||
try container.encodeIfPresent(subkey, forKey: .subkey)
|
||||
try container.encode(namespaces.map { $0.rawValue}.sorted(), forKey: .namespaces)
|
||||
try container.encode(includeMessageData, forKey: .includeMessageData)
|
||||
try container.encode(Int64(timestamp), forKey: .timestamp) // Server expects rounded seconds
|
||||
try container.encode(signatureBase64, forKey: .signatureBase64)
|
||||
try container.encode(Service.apns, forKey: .service)
|
||||
try container.encode(serviceInfo, forKey: .serviceInfo)
|
||||
try container.encode(notificationsEncryptionKey.toHexString(), forKey: .notificationsEncryptionKey)
|
||||
}
|
||||
|
||||
// MARK: - Abstract Methods
|
||||
|
||||
func generateSignature() throws -> [UInt8] {
|
||||
/// The signature data collected and stored here is used by the PN server to subscribe to the swarms
|
||||
/// for the given account; the specific rules are governed by the storage server, but in general:
|
||||
///
|
||||
/// A signature must have been produced (via the timestamp) within the past 14 days. It is
|
||||
/// recommended that clients generate a new signature whenever they re-subscribe, and that
|
||||
/// re-subscriptions happen more frequently than once every 14 days.
|
||||
///
|
||||
/// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using
|
||||
/// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value:
|
||||
/// `"MONITOR" || HEX(ACCOUNT) || SIG_TS || DATA01 || NS[0] || "," || ... || "," || NS[n]`
|
||||
///
|
||||
/// Where `SIG_TS` is the `sig_ts` value as a base-10 string; `DATA01` is either "0" or "1" depending
|
||||
/// on whether the subscription wants message data included; and the trailing `NS[i]` values are a
|
||||
/// comma-delimited list of namespaces that should be subscribed to, in the same sorted order as
|
||||
/// the `namespaces` parameter.
|
||||
let verificationBytes: [UInt8] = "MONITOR".bytes
|
||||
.appending(contentsOf: pubkey.bytes)
|
||||
.appending(contentsOf: "\(Int64(timestamp))".bytes) // Server expects rounded seconds
|
||||
.appending(contentsOf: (includeMessageData ? "1" : "0").bytes)
|
||||
.appending(
|
||||
contentsOf: namespaces
|
||||
.map { $0.rawValue } // Intentionally not using `verificationString` here
|
||||
.sorted()
|
||||
.map { "\($0)" }
|
||||
.joined(separator: ",")
|
||||
.bytes
|
||||
)
|
||||
|
||||
// TODO: Need to add handling for subkey auth
|
||||
guard
|
||||
let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature(
|
||||
message: verificationBytes,
|
||||
secretKey: ed25519SecretKey
|
||||
)
|
||||
else {
|
||||
throw SnodeAPIError.signingFailed
|
||||
}
|
||||
|
||||
return signatureBytes
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct SubscribeResponse: Codable {
|
||||
/// Flag indicating the success of the registration
|
||||
let success: Bool?
|
||||
|
||||
/// Value is `true` upon an initial registration
|
||||
let added: Bool?
|
||||
|
||||
/// Value is `true` upon a renewal/update registration
|
||||
let updated: Bool?
|
||||
|
||||
/// This will be one of the errors found here:
|
||||
/// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21
|
||||
///
|
||||
/// Values at the time of writing are:
|
||||
/// OK = 0 // Great Success!
|
||||
/// BAD_INPUT = 1 // Unparseable, invalid values, missing required arguments, etc. (details in the string)
|
||||
/// SERVICE_NOT_AVAILABLE = 2 // The requested service name isn't currently available
|
||||
/// SERVICE_TIMEOUT = 3 // The backend service did not response
|
||||
/// ERROR = 4 // There was some other error processing the subscription (details in the string)
|
||||
/// INTERNAL_ERROR = 5 // An internal program error occured processing the request
|
||||
let error: Int?
|
||||
|
||||
/// Includes additional information about the error
|
||||
let message: String?
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
import SessionSnodeKit
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct UnsubscribeRequest: Encodable {
|
||||
struct ServiceInfo: Codable {
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case token
|
||||
}
|
||||
|
||||
private let token: String
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(token: String) {
|
||||
self.token = token
|
||||
}
|
||||
}
|
||||
|
||||
private enum CodingKeys: String, CodingKey {
|
||||
case pubkey
|
||||
case ed25519PublicKey = "session_ed25519"
|
||||
case subkey = "subkey_tag"
|
||||
case timestamp = "sig_ts"
|
||||
case signatureBase64 = "signature"
|
||||
case service
|
||||
case serviceInfo = "service_info"
|
||||
}
|
||||
|
||||
/// The 33-byte account being subscribed to; typically a session ID.
|
||||
private let pubkey: String
|
||||
|
||||
/// Dict of service-specific data; typically this includes just a "token" field with a device-specific token, but different services in the
|
||||
/// future may have different input requirements.
|
||||
private let serviceInfo: ServiceInfo
|
||||
|
||||
/// 32-byte swarm authentication subkey; omitted (or null) when not using subkey auth
|
||||
private let subkey: String?
|
||||
|
||||
/// The signature unix timestamp (seconds, not ms)
|
||||
private let timestamp: TimeInterval
|
||||
|
||||
/// When the pubkey value starts with 05 (i.e. a session ID) this is the underlying ed25519 32-byte pubkey associated with the session
|
||||
/// ID. When not 05, this field should not be provided.
|
||||
private let ed25519PublicKey: [UInt8]
|
||||
|
||||
/// Secret key used to generate the signature (**Not** sent with the request)
|
||||
private let ed25519SecretKey: [UInt8]
|
||||
|
||||
// MARK: - Initialization
|
||||
|
||||
init(
|
||||
pubkey: String,
|
||||
serviceInfo: ServiceInfo,
|
||||
subkey: String?,
|
||||
timestamp: TimeInterval,
|
||||
ed25519PublicKey: [UInt8],
|
||||
ed25519SecretKey: [UInt8]
|
||||
) {
|
||||
self.pubkey = pubkey
|
||||
self.serviceInfo = serviceInfo
|
||||
self.subkey = subkey
|
||||
self.timestamp = timestamp
|
||||
self.ed25519PublicKey = ed25519PublicKey
|
||||
self.ed25519SecretKey = ed25519SecretKey
|
||||
}
|
||||
|
||||
// MARK: - Coding
|
||||
|
||||
public func encode(to encoder: Encoder) throws {
|
||||
var container: KeyedEncodingContainer<CodingKeys> = encoder.container(keyedBy: CodingKeys.self)
|
||||
|
||||
// Generate the signature for the request for encoding
|
||||
let signatureBase64: String = try generateSignature().toBase64()
|
||||
try container.encode(pubkey, forKey: .pubkey)
|
||||
try container.encode(ed25519PublicKey.toHexString(), forKey: .ed25519PublicKey)
|
||||
try container.encodeIfPresent(subkey, forKey: .subkey)
|
||||
try container.encode(timestamp, forKey: .timestamp)
|
||||
try container.encode(signatureBase64, forKey: .signatureBase64)
|
||||
try container.encode(Service.apns, forKey: .service)
|
||||
try container.encode(serviceInfo, forKey: .serviceInfo)
|
||||
}
|
||||
|
||||
// MARK: - Abstract Methods
|
||||
|
||||
func generateSignature() throws -> [UInt8] {
|
||||
/// A signature is signed using the account's Ed25519 private key (or Ed25519 subkey, if using
|
||||
/// subkey authentication with a `subkey_tag`, for future closed group subscriptions), and signs the value:
|
||||
/// `"UNSUBSCRIBE" || HEX(ACCOUNT) || SIG_TS`
|
||||
///
|
||||
/// Where `SIG_TS` is the `sig_ts` value as a base-10 string and must be within 24 hours of the current time.
|
||||
let verificationBytes: [UInt8] = "UNSUBSCRIBE".bytes
|
||||
.appending(contentsOf: pubkey.bytes)
|
||||
.appending(contentsOf: "\(timestamp)".data(using: .ascii)?.bytes)
|
||||
|
||||
// TODO: Need to add handling for subkey auth
|
||||
guard
|
||||
let signatureBytes: [UInt8] = sodium.wrappedValue.sign.signature(
|
||||
message: verificationBytes,
|
||||
secretKey: ed25519SecretKey
|
||||
)
|
||||
else {
|
||||
throw SnodeAPIError.signingFailed
|
||||
}
|
||||
|
||||
return signatureBytes
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
struct UnsubscribeResponse: Codable {
|
||||
/// Flag indicating the success of the registration
|
||||
let success: Bool?
|
||||
|
||||
/// Value is `true` upon an initial registration
|
||||
let added: Bool?
|
||||
|
||||
/// Value is `true` upon a renewal/update registration
|
||||
let updated: Bool?
|
||||
|
||||
/// This will be one of the errors found here:
|
||||
/// https://github.com/jagerman/session-push-notification-server/blob/spns-v2/spns/hive/subscription.hpp#L21
|
||||
///
|
||||
/// Values at the time of writing are:
|
||||
/// OK = 0 // Great Success!
|
||||
/// BAD_INPUT = 1 // Unparseable, invalid values, missing required arguments, etc. (details in the string)
|
||||
/// SERVICE_NOT_AVAILABLE = 2 // The requested service name isn't currently available
|
||||
/// SERVICE_TIMEOUT = 3 // The backend service did not response
|
||||
/// ERROR = 4 // There was some other error processing the subscription (details in the string)
|
||||
/// INTERNAL_ERROR = 5 // An internal program error occured processing the request
|
||||
let error: Int?
|
||||
|
||||
/// Includes additional information about the error
|
||||
let message: String?
|
||||
}
|
||||
}
|
|
@ -3,134 +3,31 @@
|
|||
import Foundation
|
||||
import Combine
|
||||
import GRDB
|
||||
import Sodium
|
||||
import SessionSnodeKit
|
||||
import SessionUtilitiesKit
|
||||
|
||||
public enum PushNotificationAPI {
|
||||
struct RegistrationRequestBody: Codable {
|
||||
let token: String
|
||||
let pubKey: String?
|
||||
}
|
||||
|
||||
struct NotifyRequestBody: Codable {
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case data
|
||||
case sendTo = "send_to"
|
||||
}
|
||||
|
||||
let data: String
|
||||
let sendTo: String
|
||||
}
|
||||
|
||||
struct ClosedGroupRequestBody: Codable {
|
||||
let closedGroupPublicKey: String
|
||||
let pubKey: String
|
||||
}
|
||||
|
||||
// MARK: - Settings
|
||||
|
||||
public static let server = "https://live.apns.getsession.org"
|
||||
public static let serverPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
|
||||
internal static let sodium: Atomic<Sodium> = Atomic(Sodium())
|
||||
private static let keychainService: String = "PNKeyChainService"
|
||||
private static let encryptionKeyKey: String = "PNEncryptionKeyKey"
|
||||
private static let encryptionKeyLength: Int = 32
|
||||
private static let maxRetryCount: Int = 4
|
||||
private static let tokenExpirationInterval: TimeInterval = 12 * 60 * 60
|
||||
|
||||
public enum ClosedGroupOperation: Int {
|
||||
case subscribe, unsubscribe
|
||||
|
||||
public var endpoint: String {
|
||||
switch self {
|
||||
case .subscribe: return "subscribe_closed_group"
|
||||
case .unsubscribe: return "unsubscribe_closed_group"
|
||||
}
|
||||
}
|
||||
}
|
||||
private static let tokenExpirationInterval: TimeInterval = (12 * 60 * 60)
|
||||
|
||||
// MARK: - Registration
|
||||
public static let server = "https://push.getsession.org"
|
||||
public static let serverPublicKey = "d7557fe563e2610de876c0ac7341b62f3c82d5eea4b62c702392ea4368f51b3b"
|
||||
public static let legacyServer = "https://live.apns.getsession.org"
|
||||
public static let legacyServerPublicKey = "642a6585919742e5a2d4dc51244964fbcd8bcab2b75612407de58b810740d049"
|
||||
|
||||
// MARK: - Requests
|
||||
|
||||
public static func unregister(_ token: Data) -> AnyPublisher<Void, Error> {
|
||||
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: token.toHexString(), pubKey: nil)
|
||||
|
||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||
return Fail(error: HTTPError.invalidJSON)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// Unsubscribe from all closed groups (including ones the user is no longer a member of,
|
||||
// just in case)
|
||||
Storage.shared
|
||||
.readPublisher { db -> (String, Set<String>) in
|
||||
(
|
||||
getUserHexEncodedPublicKey(db),
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.asRequest(of: String.self)
|
||||
.fetchSet(db)
|
||||
)
|
||||
}
|
||||
.flatMap { userPublicKey, closedGroupPublicKeys in
|
||||
Publishers
|
||||
.MergeMany(
|
||||
closedGroupPublicKeys
|
||||
.map { closedGroupPublicKey -> AnyPublisher<Void, Error> in
|
||||
PushNotificationAPI
|
||||
.performOperation(
|
||||
.unsubscribe,
|
||||
for: closedGroupPublicKey,
|
||||
publicKey: userPublicKey
|
||||
)
|
||||
}
|
||||
)
|
||||
.collect()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.sinkUntilComplete()
|
||||
|
||||
// Unregister for normal push notifications
|
||||
let url = URL(string: "\(server)/unregister")!
|
||||
var request: URLRequest = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
|
||||
request.httpBody = body
|
||||
|
||||
return OnionRequestAPI
|
||||
.sendOnionRequest(request, to: server, with: serverPublicKey)
|
||||
.map { _, data -> Void in
|
||||
guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else {
|
||||
return SNLog("Couldn't unregister from push notifications.")
|
||||
}
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't unregister from push notifications due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
|
||||
return ()
|
||||
}
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't unregister from push notifications.")
|
||||
}
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func register(
|
||||
with token: Data,
|
||||
publicKey: String,
|
||||
isForcedUpdate: Bool
|
||||
public static func subscribe(
|
||||
token: Data,
|
||||
isForcedUpdate: Bool,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
let hexEncodedToken: String = token.toHexString()
|
||||
let requestBody: RegistrationRequestBody = RegistrationRequestBody(token: hexEncodedToken, pubKey: publicKey)
|
||||
|
||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||
return Fail(error: HTTPError.invalidJSON)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let oldToken: String? = UserDefaults.standard[.deviceToken]
|
||||
let lastUploadTime: Double = UserDefaults.standard[.lastDeviceTokenUpload]
|
||||
let now: TimeInterval = Date().timeIntervalSince1970
|
||||
|
@ -142,153 +39,402 @@ public enum PushNotificationAPI {
|
|||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let url = URL(string: "\(server)/register")!
|
||||
var request: URLRequest = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
|
||||
request.httpBody = body
|
||||
guard let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey() else {
|
||||
SNLog("Unable to retrieve PN encryption key.")
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return Publishers
|
||||
.MergeMany(
|
||||
[
|
||||
OnionRequestAPI
|
||||
.sendOnionRequest(request, to: server, with: serverPublicKey)
|
||||
.map { _, data -> Void in
|
||||
guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else {
|
||||
return SNLog("Couldn't register device token.")
|
||||
// TODO: Need to generate requests for each updated group as well
|
||||
return Storage.shared
|
||||
.readPublisher { db -> (SubscribeRequest, String, Set<String>) in
|
||||
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
|
||||
throw SnodeAPIError.noKeyPair
|
||||
}
|
||||
|
||||
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db)
|
||||
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
|
||||
.defaulting(to: Preferences.NotificationPreviewType.defaultPreviewType)
|
||||
|
||||
let request: SubscribeRequest = SubscribeRequest(
|
||||
pubkey: currentUserPublicKey,
|
||||
namespaces: [.default],
|
||||
includeMessageData: (previewType == .nameAndPreview), // TODO: Test resubscribing when changing the type
|
||||
serviceInfo: SubscribeRequest.ServiceInfo(
|
||||
token: hexEncodedToken
|
||||
),
|
||||
notificationsEncryptionKey: notificationsEncryptionKey,
|
||||
subkey: nil,
|
||||
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
|
||||
ed25519PublicKey: userED25519KeyPair.publicKey,
|
||||
ed25519SecretKey: userED25519KeyPair.secretKey
|
||||
)
|
||||
|
||||
return (
|
||||
request,
|
||||
currentUserPublicKey,
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
|
||||
.joining(
|
||||
required: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
|
||||
)
|
||||
.asRequest(of: String.self)
|
||||
.fetchSet(db)
|
||||
)
|
||||
}
|
||||
.flatMap { request, currentUserPublicKey, legacyGroupIds -> AnyPublisher<Void, Error> in
|
||||
Publishers
|
||||
.MergeMany(
|
||||
[
|
||||
PushNotificationAPI
|
||||
.send(
|
||||
request: PushNotificationAPIRequest(
|
||||
endpoint: .subscribe,
|
||||
body: request
|
||||
)
|
||||
)
|
||||
.decoded(as: SubscribeResponse.self, using: dependencies)
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveOutput: { _, response in
|
||||
guard response.success == true else {
|
||||
return SNLog("Couldn't subscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
|
||||
}
|
||||
|
||||
UserDefaults.standard[.deviceToken] = hexEncodedToken
|
||||
UserDefaults.standard[.lastDeviceTokenUpload] = now
|
||||
UserDefaults.standard[.isUsingFullAPNs] = true
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't subscribe for push notifications.")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
].appending(
|
||||
// FIXME: Remove this once legacy groups are deprecated
|
||||
contentsOf: legacyGroupIds
|
||||
.map { legacyGroupId in
|
||||
PushNotificationAPI.subscribeToLegacyGroup(
|
||||
legacyGroupId: legacyGroupId,
|
||||
currentUserPublicKey: currentUserPublicKey,
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
)
|
||||
)
|
||||
.collect()
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func unsubscribe(
|
||||
token: Data,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
let hexEncodedToken: String = token.toHexString()
|
||||
|
||||
// FIXME: Remove this once legacy groups are deprecated
|
||||
/// Unsubscribe from all legacy groups (including ones the user is no longer a member of, just in case)
|
||||
Storage.shared
|
||||
.readPublisher { db -> (String, Set<String>) in
|
||||
(
|
||||
getUserHexEncodedPublicKey(db),
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%"))
|
||||
.asRequest(of: String.self)
|
||||
.fetchSet(db)
|
||||
)
|
||||
}
|
||||
.flatMap { currentUserPublicKey, legacyGroupIds in
|
||||
Publishers
|
||||
.MergeMany(
|
||||
legacyGroupIds
|
||||
.map { legacyGroupId -> AnyPublisher<Void, Error> in
|
||||
PushNotificationAPI
|
||||
.unsubscribeFromLegacyGroup(
|
||||
legacyGroupId: legacyGroupId,
|
||||
currentUserPublicKey: currentUserPublicKey,
|
||||
using: dependencies
|
||||
)
|
||||
}
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't register device token due to error: \(response.message ?? "nil").")
|
||||
)
|
||||
.collect()
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.subscribe(on: DispatchQueue.global(qos: .userInitiated))
|
||||
.sinkUntilComplete()
|
||||
|
||||
// TODO: Need to generate requests for each updated group as well
|
||||
return Storage.shared
|
||||
.readPublisher { db -> UnsubscribeRequest in
|
||||
guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else {
|
||||
throw SnodeAPIError.noKeyPair
|
||||
}
|
||||
|
||||
return UnsubscribeRequest(
|
||||
pubkey: getUserHexEncodedPublicKey(db),
|
||||
serviceInfo: UnsubscribeRequest.ServiceInfo(
|
||||
token: hexEncodedToken
|
||||
),
|
||||
subkey: nil,
|
||||
timestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000), // Seconds
|
||||
ed25519PublicKey: userED25519KeyPair.publicKey,
|
||||
ed25519SecretKey: userED25519KeyPair.secretKey
|
||||
)
|
||||
}
|
||||
.flatMap { request -> AnyPublisher<Void, Error> in
|
||||
PushNotificationAPI
|
||||
.send(
|
||||
request: PushNotificationAPIRequest(
|
||||
endpoint: .unsubscribe,
|
||||
body: request
|
||||
)
|
||||
)
|
||||
.decoded(as: UnsubscribeResponse.self, using: dependencies)
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveOutput: { _, response in
|
||||
guard response.success == true else {
|
||||
return SNLog("Couldn't unsubscribe for push notifications due to error (\(response.error ?? -1)): \(response.message ?? "nil").")
|
||||
}
|
||||
|
||||
UserDefaults.standard[.deviceToken] = hexEncodedToken
|
||||
UserDefaults.standard[.lastDeviceTokenUpload] = now
|
||||
UserDefaults.standard[.isUsingFullAPNs] = true
|
||||
return ()
|
||||
}
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't register device token.")
|
||||
}
|
||||
UserDefaults.standard[.deviceToken] = nil
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't unsubscribe for push notifications.")
|
||||
}
|
||||
)
|
||||
.eraseToAnyPublisher()
|
||||
].appending(
|
||||
contentsOf: Storage.shared
|
||||
.read { db -> [String] in
|
||||
try ClosedGroup
|
||||
.select(.threadId)
|
||||
.joining(
|
||||
required: ClosedGroup.members
|
||||
.filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db))
|
||||
)
|
||||
.asRequest(of: String.self)
|
||||
.fetchAll(db)
|
||||
}
|
||||
.defaulting(to: [])
|
||||
.map { closedGroupPublicKey -> AnyPublisher<Void, Error> in
|
||||
PushNotificationAPI
|
||||
.performOperation(
|
||||
.subscribe,
|
||||
for: closedGroupPublicKey,
|
||||
publicKey: publicKey
|
||||
)
|
||||
}
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Legacy Notifications
|
||||
|
||||
// FIXME: Remove this once legacy notifications and legacy groups are deprecated
|
||||
public static func legacyNotify(
|
||||
recipient: String,
|
||||
with message: String,
|
||||
maxRetryCount: Int? = nil,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
return PushNotificationAPI
|
||||
.send(
|
||||
request: PushNotificationAPIRequest(
|
||||
endpoint: .legacyNotify,
|
||||
body: LegacyNotifyRequest(
|
||||
data: message,
|
||||
sendTo: recipient
|
||||
)
|
||||
)
|
||||
)
|
||||
.collect()
|
||||
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
|
||||
.retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveOutput: { _, response in
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't send push notification due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't send push notification.")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
public static func performOperation(
|
||||
_ operation: ClosedGroupOperation,
|
||||
for closedGroupPublicKey: String,
|
||||
publicKey: String
|
||||
|
||||
// MARK: - Legacy Groups
|
||||
|
||||
// FIXME: Remove this once legacy groups are deprecated
|
||||
public static func subscribeToLegacyGroup(
|
||||
legacyGroupId: String,
|
||||
currentUserPublicKey: String,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs]
|
||||
let requestBody: ClosedGroupRequestBody = ClosedGroupRequestBody(
|
||||
closedGroupPublicKey: closedGroupPublicKey,
|
||||
pubKey: publicKey
|
||||
)
|
||||
|
||||
guard isUsingFullAPNs else {
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||
return Fail(error: HTTPError.invalidJSON)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let url = URL(string: "\(server)/\(operation.endpoint)")!
|
||||
var request: URLRequest = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
|
||||
request.httpBody = body
|
||||
|
||||
return OnionRequestAPI
|
||||
.sendOnionRequest(request, to: server, with: serverPublicKey)
|
||||
.map { _, data in
|
||||
guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else {
|
||||
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
||||
}
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey) due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
|
||||
return ()
|
||||
}
|
||||
return PushNotificationAPI
|
||||
.send(
|
||||
request: PushNotificationAPIRequest(
|
||||
endpoint: .legacyGroupSubscribe,
|
||||
body: LegacyGroupRequest(
|
||||
pubKey: currentUserPublicKey,
|
||||
closedGroupPublicKey: legacyGroupId
|
||||
)
|
||||
)
|
||||
)
|
||||
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveOutput: { _, response in
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't subscribe for legacy group: \(legacyGroupId) due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure:
|
||||
SNLog("Couldn't subscribe/unsubscribe for closed group: \(closedGroupPublicKey).")
|
||||
case .failure: SNLog("Couldn't subscribe for legacy group: \(legacyGroupId).")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Notify
|
||||
|
||||
public static func notify(
|
||||
recipient: String,
|
||||
with message: String,
|
||||
maxRetryCount: Int? = nil
|
||||
// FIXME: Remove this once legacy groups are deprecated
|
||||
public static func unsubscribeFromLegacyGroup(
|
||||
legacyGroupId: String,
|
||||
currentUserPublicKey: String,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<Void, Error> {
|
||||
let requestBody: NotifyRequestBody = NotifyRequestBody(data: message, sendTo: recipient)
|
||||
let isUsingFullAPNs = UserDefaults.standard[.isUsingFullAPNs]
|
||||
|
||||
guard let body: Data = try? JSONEncoder().encode(requestBody) else {
|
||||
// TODO: Need to validate if this is actually desired behaviour - would this check prevent the app from unsubscribing if the user switches off fast mode??? (this is what the app is currently doing)
|
||||
// TODO: This flag seems like it might actually be buggy... should double check it
|
||||
guard isUsingFullAPNs else {
|
||||
return Just(())
|
||||
.setFailureType(to: Error.self)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return PushNotificationAPI
|
||||
.send(
|
||||
request: PushNotificationAPIRequest(
|
||||
endpoint: .legacyGroupUnsubscribe,
|
||||
body: LegacyGroupRequest(
|
||||
pubKey: currentUserPublicKey,
|
||||
closedGroupPublicKey: legacyGroupId
|
||||
)
|
||||
)
|
||||
)
|
||||
.decoded(as: LegacyPushServerResponse.self, using: dependencies)
|
||||
.retry(maxRetryCount)
|
||||
.handleEvents(
|
||||
receiveOutput: { _, response in
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't unsubscribe for legacy group: \(legacyGroupId) due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
},
|
||||
receiveCompletion: { result in
|
||||
switch result {
|
||||
case .finished: break
|
||||
case .failure: SNLog("Couldn't unsubscribe for legacy group: \(legacyGroupId).")
|
||||
}
|
||||
}
|
||||
)
|
||||
.map { _ in () }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
// MARK: - Security
|
||||
|
||||
@discardableResult private static func getOrGenerateEncryptionKey() throws -> Data {
|
||||
// TODO: May want to work this differently (will break after a phone restart if the device hasn't been unlocked yet)
|
||||
do {
|
||||
var encryptionKey: Data = try SSKDefaultKeychainStorage.shared.data(
|
||||
forService: keychainService,
|
||||
key: encryptionKeyKey
|
||||
)
|
||||
defer { encryptionKey.resetBytes(in: 0..<encryptionKey.count) }
|
||||
|
||||
guard encryptionKey.count == encryptionKeyLength else { throw StorageError.invalidKeySpec }
|
||||
|
||||
return encryptionKey
|
||||
}
|
||||
catch {
|
||||
switch (error, (error as? KeychainStorageError)?.code) {
|
||||
case (StorageError.invalidKeySpec, _), (_, errSecItemNotFound):
|
||||
// No keySpec was found so we need to generate a new one
|
||||
do {
|
||||
var keySpec: Data = try Randomness.generateRandomBytes(numberBytes: encryptionKeyLength)
|
||||
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
|
||||
|
||||
try SSKDefaultKeychainStorage.shared.set(
|
||||
data: keySpec,
|
||||
service: keychainService,
|
||||
key: encryptionKeyKey
|
||||
)
|
||||
return keySpec
|
||||
}
|
||||
catch {
|
||||
SNLog("Setting keychain value failed with error: \(error.localizedDescription)")
|
||||
throw StorageError.keySpecCreationFailed
|
||||
}
|
||||
|
||||
default:
|
||||
// Because we use kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly, the keychain will be inaccessible
|
||||
// after device restart until device is unlocked for the first time. If the app receives a push
|
||||
// notification, we won't be able to access the keychain to process that notification, so we should
|
||||
// just terminate by throwing an uncaught exception
|
||||
if CurrentAppContext().isMainApp || CurrentAppContext().isInBackground() {
|
||||
let appState: UIApplication.State = CurrentAppContext().reportedApplicationState
|
||||
SNLog("CipherKeySpec inaccessible. New install or no unlock since device restart?, ApplicationState: \(NSStringForUIApplicationState(appState))")
|
||||
throw StorageError.keySpecInaccessible
|
||||
}
|
||||
|
||||
SNLog("CipherKeySpec inaccessible; not main app.")
|
||||
throw StorageError.keySpecInaccessible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
private static func send<T: Encodable>(
|
||||
request: PushNotificationAPIRequest<T>,
|
||||
using dependencies: SSKDependencies = SSKDependencies()
|
||||
) -> AnyPublisher<(ResponseInfoType, Data?), Error> {
|
||||
guard
|
||||
let url: URL = URL(string: "\(request.endpoint.server)/\(request.endpoint.rawValue)"),
|
||||
let payload: Data = try? JSONEncoder().encode(request.body)
|
||||
else {
|
||||
return Fail(error: HTTPError.invalidJSON)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
let url = URL(string: "\(server)/notify")!
|
||||
var request: URLRequest = URLRequest(url: url)
|
||||
request.httpMethod = "POST"
|
||||
request.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
|
||||
request.httpBody = body
|
||||
guard Features.useOnionRequests else {
|
||||
return HTTP
|
||||
.execute(
|
||||
.post,
|
||||
"\(request.endpoint.server)/\(request.endpoint.rawValue)",
|
||||
body: payload
|
||||
)
|
||||
.map { response in (HTTP.ResponseInfo(code: -1, headers: [:]), response) }
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
|
||||
return OnionRequestAPI
|
||||
.sendOnionRequest(request, to: server, with: serverPublicKey)
|
||||
.map { _, data -> Void in
|
||||
guard let response: PushServerResponse = try? data?.decoded(as: PushServerResponse.self) else {
|
||||
return SNLog("Couldn't send push notification.")
|
||||
}
|
||||
guard response.code != 0 else {
|
||||
return SNLog("Couldn't send push notification due to error: \(response.message ?? "nil").")
|
||||
}
|
||||
|
||||
return ()
|
||||
}
|
||||
.retry(maxRetryCount ?? PushNotificationAPI.maxRetryCount)
|
||||
var urlRequest: URLRequest = URLRequest(url: url)
|
||||
urlRequest.httpMethod = "POST"
|
||||
urlRequest.allHTTPHeaderFields = [ HTTPHeader.contentType: "application/json" ]
|
||||
urlRequest.httpBody = payload
|
||||
|
||||
return dependencies.onionApi
|
||||
.sendOnionRequest(urlRequest, to: request.endpoint.server, with: request.endpoint.serverPublicKey)
|
||||
.eraseToAnyPublisher()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension PushNotificationAPI {
|
||||
enum Endpoint: String {
|
||||
case subscribe = "subscribe"
|
||||
case unsubscribe = "unsubscribe"
|
||||
|
||||
// MARK: - Legacy Endpoints
|
||||
|
||||
case legacyNotify = "notify"
|
||||
case legacyRegister = "register"
|
||||
case legacyUnregister = "unregister"
|
||||
case legacyGroupSubscribe = "subscribe_closed_group"
|
||||
case legacyGroupUnsubscribe = "unsubscribe_closed_group"
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
var server: String {
|
||||
switch self {
|
||||
case .legacyNotify, .legacyRegister, .legacyUnregister,
|
||||
.legacyGroupSubscribe, .legacyGroupUnsubscribe:
|
||||
return PushNotificationAPI.legacyServer
|
||||
|
||||
default: return PushNotificationAPI.server
|
||||
}
|
||||
}
|
||||
|
||||
var serverPublicKey: String {
|
||||
switch self {
|
||||
case .legacyNotify, .legacyRegister, .legacyUnregister,
|
||||
.legacyGroupSubscribe, .legacyGroupUnsubscribe:
|
||||
return PushNotificationAPI.legacyServerPublicKey
|
||||
|
||||
default: return PushNotificationAPI.serverPublicKey
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
|
||||
|
||||
import Foundation
|
||||
|
||||
extension PushNotificationAPI {
|
||||
enum Service: String, Codable {
|
||||
case apns
|
||||
}
|
||||
}
|
|
@ -8,7 +8,7 @@ import SessionUIKit
|
|||
import SignalCoreKit
|
||||
|
||||
final class ShareNavController: UINavigationController, ShareViewDelegate {
|
||||
private var areVersionMigrationsComplete = false
|
||||
private static let areVersionMigrationsComplete: Atomic<Bool> = Atomic(false)
|
||||
public static var attachmentPrepPublisher: AnyPublisher<[SignalAttachment], Error>?
|
||||
|
||||
// MARK: - Error
|
||||
|
@ -24,6 +24,8 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
|
|||
|
||||
override func loadView() {
|
||||
super.loadView()
|
||||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
|
||||
// This should be the first thing we do (Note: If you leave the share context and return to it
|
||||
// the context will already exist, trying to override it results in the share context crashing
|
||||
|
@ -72,6 +74,12 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
|
|||
name: .OWSApplicationDidEnterBackground,
|
||||
object: nil
|
||||
)
|
||||
|
||||
/// **Note:** If the user opens, dismisses and re-opens the share extension it'll actually use the same instance which
|
||||
/// results in the `AppSetup` not actually running (and the UI not actually being loaded correctly) - in order to avoid this
|
||||
/// we call `checkIsAppReady` explicitly here assuming that either the `AppSetup` _hasn't_ complete or won't ever
|
||||
/// get run
|
||||
checkIsAppReady()
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
@ -88,7 +96,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
|
|||
|
||||
Logger.debug("")
|
||||
|
||||
areVersionMigrationsComplete = true
|
||||
ShareNavController.areVersionMigrationsComplete.mutate { $0 = true }
|
||||
|
||||
// If we need a config sync then trigger it now
|
||||
if needsConfigSync {
|
||||
|
@ -105,10 +113,15 @@ final class ShareNavController: UINavigationController, ShareViewDelegate {
|
|||
AssertIsOnMainThread()
|
||||
|
||||
// App isn't ready until storage is ready AND all version migrations are complete.
|
||||
guard areVersionMigrationsComplete else { return }
|
||||
guard Storage.shared.isValid else { return }
|
||||
guard ShareNavController.areVersionMigrationsComplete.wrappedValue else { return }
|
||||
guard Storage.shared.isValid else {
|
||||
// If the database is invalid then the UI will handle it
|
||||
showLockScreenOrMainContent()
|
||||
return
|
||||
}
|
||||
guard !AppReadiness.isAppReady() else {
|
||||
// Only mark the app as ready once.
|
||||
showLockScreenOrMainContent()
|
||||
return
|
||||
}
|
||||
|
||||
|
|
|
@ -31,6 +31,18 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
return titleLabel
|
||||
}()
|
||||
|
||||
private lazy var databaseErrorLabel: UILabel = {
|
||||
let result: UILabel = UILabel()
|
||||
result.font = .systemFont(ofSize: Values.mediumFontSize)
|
||||
result.text = "database_inaccessible_error".localized()
|
||||
result.textAlignment = .center
|
||||
result.themeTextColor = .textPrimary
|
||||
result.numberOfLines = 0
|
||||
result.isHidden = true
|
||||
|
||||
return result
|
||||
}()
|
||||
|
||||
private lazy var tableView: UITableView = {
|
||||
let tableView: UITableView = UITableView()
|
||||
|
@ -53,6 +65,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
view.themeBackgroundColor = .backgroundPrimary
|
||||
view.addSubview(tableView)
|
||||
view.addSubview(databaseErrorLabel)
|
||||
|
||||
setupLayout()
|
||||
|
||||
|
@ -99,6 +112,10 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
|
||||
private func setupLayout() {
|
||||
tableView.pin(to: view)
|
||||
|
||||
databaseErrorLabel.pin(.top, to: .top, of: view, withInset: Values.massiveSpacing)
|
||||
databaseErrorLabel.pin(.leading, to: .leading, of: view, withInset: Values.veryLargeSpacing)
|
||||
databaseErrorLabel.pin(.trailing, to: .trailing, of: view, withInset: -Values.veryLargeSpacing)
|
||||
}
|
||||
|
||||
// MARK: - Updating
|
||||
|
@ -107,7 +124,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView
|
|||
// Start observing for data changes
|
||||
dataChangeObservable = Storage.shared.start(
|
||||
viewModel.observableViewData,
|
||||
onError: { _ in },
|
||||
onError: { [weak self] _ in self?.databaseErrorLabel.isHidden = Storage.shared.isValid },
|
||||
onChange: { [weak self] viewData in
|
||||
// The defaul scheduler emits changes on the main thread
|
||||
self?.handleUpdates(viewData)
|
||||
|
|
|
@ -56,20 +56,25 @@ open class Storage {
|
|||
return
|
||||
}
|
||||
|
||||
// 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 = Storage.getOrGenerateDatabaseKeySpec()
|
||||
tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count)
|
||||
/// 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)
|
||||
///
|
||||
/// We reset the bytes immediately after generation to ensure the database key doesn't hang around in memory unintentionally
|
||||
///
|
||||
/// **Note:** If we fail to get/generate the keySpec then don't bother continuing to setup the Database as it'll just be invalid,
|
||||
/// in this case the App/Extensions will have logic that checks the `isValid` flag of the database
|
||||
do {
|
||||
var tmpKeySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
|
||||
tmpKeySpec.resetBytes(in: 0..<tmpKeySpec.count)
|
||||
}
|
||||
catch { return }
|
||||
|
||||
// Configure the database and create the DatabasePool for interacting with the database
|
||||
var config = Configuration()
|
||||
config.maximumReaderCount = 10 // Increase the max read connection limit - Default is 5
|
||||
config.observesSuspensionNotifications = true // Minimise `0xDEAD10CC` exceptions
|
||||
config.prepareDatabase { db in
|
||||
var keySpec: Data = Storage.getOrGenerateDatabaseKeySpec()
|
||||
var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec()
|
||||
defer { keySpec.resetBytes(in: 0..<keySpec.count) } // Reset content immediately after use
|
||||
|
||||
// Use a raw key spec, where the 96 hexadecimal digits are provided
|
||||
|
@ -252,7 +257,7 @@ open class Storage {
|
|||
return try SSKDefaultKeychainStorage.shared.data(forService: keychainService, key: dbCipherKeySpecKey)
|
||||
}
|
||||
|
||||
@discardableResult private static func getOrGenerateDatabaseKeySpec() -> Data {
|
||||
@discardableResult private static func getOrGenerateDatabaseKeySpec() throws -> Data {
|
||||
do {
|
||||
var keySpec: Data = try getDatabaseCipherKeySpec()
|
||||
defer { keySpec.resetBytes(in: 0..<keySpec.count) }
|
||||
|
@ -283,8 +288,9 @@ open class Storage {
|
|||
return keySpec
|
||||
}
|
||||
catch {
|
||||
SNLog("Setting keychain value failed with error: \(error.localizedDescription)")
|
||||
Thread.sleep(forTimeInterval: 15) // Sleep to allow any background behaviours to complete
|
||||
fatalError("Setting keychain value failed with error: \(error.localizedDescription)")
|
||||
throw StorageError.keySpecCreationFailed
|
||||
}
|
||||
|
||||
default:
|
||||
|
@ -294,15 +300,18 @@ open class Storage {
|
|||
// just terminate by throwing an uncaught exception
|
||||
if CurrentAppContext().isMainApp || CurrentAppContext().isInBackground() {
|
||||
let appState: UIApplication.State = CurrentAppContext().reportedApplicationState
|
||||
SNLog("CipherKeySpec inaccessible. New install or no unlock since device restart?, ApplicationState: \(NSStringForUIApplicationState(appState))")
|
||||
|
||||
// In this case we should have already detected the situation earlier and exited gracefully (in the
|
||||
// app delegate) using isDatabasePasswordAccessible, but we want to stop the app running here anyway
|
||||
// In this case we should have already detected the situation earlier and exited
|
||||
// gracefully (in the app delegate) using isDatabasePasswordAccessible, but we
|
||||
// want to stop the app running here anyway
|
||||
Thread.sleep(forTimeInterval: 5) // Sleep to allow any background behaviours to complete
|
||||
fatalError("CipherKeySpec inaccessible. New install or no unlock since device restart?, ApplicationState: \(NSStringForUIApplicationState(appState))")
|
||||
throw StorageError.keySpecInaccessible
|
||||
}
|
||||
|
||||
SNLog("CipherKeySpec inaccessible; not main app.")
|
||||
Thread.sleep(forTimeInterval: 5) // Sleep to allow any background behaviours to complete
|
||||
fatalError("CipherKeySpec inaccessible; not main app.")
|
||||
throw StorageError.keySpecInaccessible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -422,7 +431,10 @@ open class Storage {
|
|||
onError: @escaping (Error) -> Void,
|
||||
onChange: @escaping (Reducer.Value) -> Void
|
||||
) -> DatabaseCancellable {
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else { return AnyDatabaseCancellable(cancel: {}) }
|
||||
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
|
||||
onError(StorageError.databaseInvalid)
|
||||
return AnyDatabaseCancellable(cancel: {})
|
||||
}
|
||||
|
||||
return observation.start(
|
||||
in: dbWriter,
|
||||
|
|
|
@ -7,6 +7,8 @@ public enum StorageError: Error {
|
|||
case databaseInvalid
|
||||
case migrationFailed
|
||||
case invalidKeySpec
|
||||
case keySpecCreationFailed
|
||||
case keySpecInaccessible
|
||||
case decodingFailed
|
||||
|
||||
case failedToSave
|
||||
|
|
|
@ -10,6 +10,7 @@ public struct SessionId {
|
|||
case standard = "05" // Used for identified users, open groups, etc.
|
||||
case blinded = "15" // Used for authentication and participants in open groups with blinding enabled
|
||||
case unblinded = "00" // Used for authentication in open groups with blinding disabled
|
||||
case group = "03" // Used for update group conversations
|
||||
|
||||
public init?(from stringValue: String?) {
|
||||
guard let stringValue: String = stringValue else { return nil }
|
||||
|
|
|
@ -63,6 +63,14 @@ public enum AppSetup {
|
|||
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
|
||||
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> ()
|
||||
) {
|
||||
// If the database can't be initialised into a valid state then error
|
||||
guard Storage.shared.isValid else {
|
||||
DispatchQueue.main.async {
|
||||
migrationsCompletion(Result.failure(StorageError.databaseInvalid), false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
|
||||
|
||||
Storage.shared.perform(
|
||||
|
|
Loading…
Reference in New Issue