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:
Morgan Pretty 2023-05-19 17:26:14 +10:00
parent 9799297e15
commit 4330a40f6f
51 changed files with 1026 additions and 284 deletions

View File

@ -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 */,

View File

@ -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
}

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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.";

View File

@ -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 }

View File

@ -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

View File

@ -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()
}

View File

@ -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

View File

@ -89,7 +89,7 @@ extension OpenGroupAPI.Message {
throw HTTPError.parsingFailed
}
case .none:
case .none, .group:
SNLog("Ignoring message with invalid sender.")
throw HTTPError.parsingFailed
}

View File

@ -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)

View File

@ -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()
}
}
)

View File

@ -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
}
}()

View File

@ -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
)
)
)

View File

@ -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:

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -3,7 +3,7 @@
import Foundation
extension PushNotificationAPI {
struct PushServerResponse: Codable {
struct LegacyPushServerResponse: Codable {
let code: Int
let message: String?
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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?
}
}

View File

@ -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
}
}
}

View File

@ -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?
}
}

View File

@ -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()
}
}

View File

@ -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
}
}
}
}

View File

@ -0,0 +1,9 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
extension PushNotificationAPI {
enum Service: String, Codable {
case apns
}
}

View File

@ -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
}

View File

@ -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)

View File

@ -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,

View File

@ -7,6 +7,8 @@ public enum StorageError: Error {
case databaseInvalid
case migrationFailed
case invalidKeySpec
case keySpecCreationFailed
case keySpecInaccessible
case decodingFailed
case failedToSave

View File

@ -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 }

View File

@ -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(