Merge remote-tracking branch 'upstream/dev' into feature/groups-rebuild

# Conflicts:
#	Session.xcodeproj/project.pbxproj
#	Session/Conversations/ConversationVC.swift
#	Session/Meta/AppDelegate.swift
#	SessionMessagingKit/Database/Models/Interaction.swift
#	SessionMessagingKit/Database/Models/SessionThread.swift
#	SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift
#	SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+Contacts.swift
#	SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+ConvoInfoVolatile.swift
#	SessionMessagingKit/SessionUtil/Config Handling/SessionUtil+UserProfile.swift
#	SessionMessagingKit/SessionUtil/Database/QueryInterfaceRequest+Utilities.swift
#	SessionNotificationServiceExtension/NotificationServiceExtension.swift
#	SessionUtilitiesKit/Database/Storage.swift
#	SessionUtilitiesKit/Utilities/Bencode.swift
#	SignalUtilitiesKit/Utilities/AppSetup.swift
This commit is contained in:
Morgan Pretty 2023-09-08 17:39:16 +10:00
commit 65057fba21
42 changed files with 766 additions and 431 deletions

View File

@ -753,6 +753,7 @@
FD97B2402A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */; };
FD97B2422A3FEBF30027DD57 /* UnreadMarkerCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */; };
FD9AECA72AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA62AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift */; };
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9AECA42AAA9609009B3406 /* NotificationError.swift */; };
FD9B30F3293EA0BF008DEE3E /* BatchResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */; };
FD9BDE002A5D22B7005F1EBC /* libSessionUtil.a in Frameworks */ = {isa = PBXBuildFile; fileRef = FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */; };
FD9BDE012A5D24EA005F1EBC /* SessionUIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C331FF1B2558F9D300070591 /* SessionUIKit.framework */; };
@ -1947,6 +1948,7 @@
FD97B23F2A3FEB050027DD57 /* ARC4RandomNumberGenerator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ARC4RandomNumberGenerator.swift; sourceTree = "<group>"; };
FD97B2412A3FEBF30027DD57 /* UnreadMarkerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadMarkerCell.swift; sourceTree = "<group>"; };
FD9AECA62AAAF5B0009B3406 /* Crypto+SessionUtilitiesKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Crypto+SessionUtilitiesKit.swift"; sourceTree = "<group>"; };
FD9AECA42AAA9609009B3406 /* NotificationError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationError.swift; sourceTree = "<group>"; };
FD9B30F2293EA0BF008DEE3E /* BatchResponseSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BatchResponseSpec.swift; sourceTree = "<group>"; };
FD9BDDF82A5D2294005F1EBC /* libSessionUtil.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libSessionUtil.a; sourceTree = BUILT_PRODUCTS_DIR; };
FD9DD2702A72516D00ECB68E /* TestExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestExtensions.swift; sourceTree = "<group>"; };
@ -2561,6 +2563,7 @@
isa = PBXGroup;
children = (
C31C219B255BC92200EC2D66 /* Meta */,
FD9AECA42AAA9609009B3406 /* NotificationError.swift */,
7BDCFC07242186E700641C39 /* NotificationServiceExtensionContext.swift */,
7BC01A3D241F40AB00BC7C55 /* NotificationServiceExtension.swift */,
7B1D74A927BCC16E0030B423 /* NSENotificationPresenter.swift */,
@ -5745,6 +5748,7 @@
7BDCFC08242186E700641C39 /* NotificationServiceExtensionContext.swift in Sources */,
7BC01A3E241F40AB00BC7C55 /* NotificationServiceExtension.swift in Sources */,
7B1D74AA27BCC16E0030B423 /* NSENotificationPresenter.swift in Sources */,
FD9AECA52AAA9609009B3406 /* NotificationError.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -6810,7 +6814,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6834,7 +6838,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6882,7 +6886,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -6911,7 +6915,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = NO;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.ShareExtension";
PRODUCT_NAME = "$(TARGET_NAME)";
@ -6947,7 +6951,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = "$(inherited)";
@ -6970,7 +6974,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7021,7 +7025,7 @@
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CODE_SIGN_STYLE = Automatic;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = SUQ8J2PCT7;
ENABLE_NS_ASSERTIONS = NO;
@ -7049,7 +7053,7 @@
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
MTL_ENABLE_DEBUG_INFO = NO;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger.NotificationServiceExtension";
@ -7981,7 +7985,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -8019,7 +8023,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
OTHER_LDFLAGS = "$(inherited)";
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
@ -8052,7 +8056,7 @@
CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements;
CODE_SIGN_IDENTITY = "iPhone Developer";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
CURRENT_PROJECT_VERSION = 422;
CURRENT_PROJECT_VERSION = 423;
DEVELOPMENT_TEAM = SUQ8J2PCT7;
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
@ -8090,7 +8094,7 @@
"$(SRCROOT)",
);
LLVM_LTO = NO;
MARKETING_VERSION = 2.4.0;
MARKETING_VERSION = 2.4.1;
OTHER_LDFLAGS = "$(inherited)";
PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger";
PRODUCT_NAME = Session;

View File

@ -93,7 +93,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
public func reportOutgoingCall(_ call: SessionCall) {
AssertIsOnMainThread()
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
call.stateDidChange = {
if call.hasStartedConnecting {
@ -123,7 +124,8 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
completion(error)
return
}
UserDefaults.sharedLokiProject?.set(true, forKey: "isCallOngoing")
UserDefaults.sharedLokiProject?[.isCallOngoing] = true
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
completion(nil)
}
}
@ -141,7 +143,9 @@ public final class SessionCallManager: NSObject, CallManagerProtocol {
func handleCallEnded() {
WebRTCSession.current = nil
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
if CurrentAppContext().isInBackground() {
(UIApplication.shared.delegate as? AppDelegate)?.stopPollers()
DDLog.flushLog()

View File

@ -545,7 +545,11 @@ extension ConversationVC:
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread
.filter(id: threadId)
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
.updateAllAndConfig(
db,
SessionThread.Columns.shouldBeVisible.set(to: true),
using: dependencies
)
}
// Insert the interaction and associated it with the optimistically inserted message so
@ -1346,7 +1350,11 @@ extension ConversationVC:
if self?.viewModel.threadData.threadShouldBeVisible == false {
_ = try SessionThread
.filter(id: cellViewModel.threadId)
.updateAllAndConfig(db, SessionThread.Columns.shouldBeVisible.set(to: true))
.updateAllAndConfig(
db,
SessionThread.Columns.shouldBeVisible.set(to: true),
using: dependencies
)
}
let pendingReaction: Reaction? = {
@ -2498,7 +2506,8 @@ extension ConversationVC {
db,
Contact.Columns.isApproved.set(to: true),
Contact.Columns.didApproveMe
.set(to: contact.didApproveMe || !isNewThread)
.set(to: contact.didApproveMe || !isNewThread),
using: dependencies
)
}
.subscribe(on: DispatchQueue.global(qos: .userInitiated))

View File

@ -2025,25 +2025,22 @@ final class ConversationVC: BaseVC, SessionUtilRespondingViewController, Convers
.sorted()
.filter({ $0.section == messagesSection })
.compactMap({ indexPath -> (frame: CGRect, cellViewModel: MessageViewModel)? in
let cellViewModel: MessageViewModel = self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
guard let cell: UITableViewCell = tableView.cellForRow(at: indexPath) else { return nil }
// Deal with disappearing messages control message
if let cell: InfoMessageCell = tableView.cellForRow(at: indexPath) as? InfoMessageCell, cellViewModel.variant == .infoDisappearingMessagesUpdate {
return (
view.convert(cell.frame, from: tableView),
cellViewModel
)
switch cell {
case is VisibleMessageCell, is CallMessageCell, is InfoMessageCell:
return (
view.convert(cell.frame, from: tableView),
self.viewModel.interactionData[indexPath.section].elements[indexPath.row]
)
case is TypingIndicatorCell, is DateHeaderCell, is UnreadMarkerCell:
return nil
default:
SNLog("[ConversationVC] Warning: Processing unhandled cell type when marking as read, this could result in intermittent failures")
return nil
}
// Deal with visible messages
if let cell: VisibleMessageCell = tableView.cellForRow(at: indexPath) as? VisibleMessageCell {
return (
view.convert(cell.frame, from: tableView),
cellViewModel
)
}
return nil
})
// Exclude messages that are partially off the bottom of the screen
.filter({ $0.frame.maxY <= tableVisualBottom })

View File

@ -824,7 +824,7 @@ public class ConversationViewModel: OWSAudioPlayerDelegate {
dependencies[singleton: .storage].writeAsync { db in
try Contact
.filter(id: threadId)
.updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false))
.updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false), using: dependencies)
}
}

View File

@ -119,8 +119,7 @@ final class QuoteView: UIView {
// Content view
let contentView = UIView()
addSubview(contentView)
contentView.pin([ UIView.HorizontalEdge.left, UIView.VerticalEdge.top, UIView.VerticalEdge.bottom ], to: self)
contentView.rightAnchor.constraint(lessThanOrEqualTo: self.rightAnchor).isActive = true
contentView.pin(to: self)
if let attachment: Attachment = attachment {
let isAudio: Bool = MIMETypeUtil.isAudio(attachment.contentType)

View File

@ -157,7 +157,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
.updateAllAndConfig(
db,
Profile.Columns.nickname
.set(to: (updatedNickname.isEmpty ? nil : editedDisplayName))
.set(to: (updatedNickname.isEmpty ? nil : editedDisplayName)),
using: dependencies
)
}
}
@ -816,7 +817,8 @@ class ThreadSettingsViewModel: SessionTableViewModel<ThreadSettingsViewModel.Nav
.filter(id: threadId)
.updateAllAndConfig(
db,
Contact.Columns.isBlocked.set(to: isBlocked)
Contact.Columns.isBlocked.set(to: isBlocked),
using: dependencies
)
},
completion: { [weak self] db, _ in

View File

@ -383,6 +383,7 @@ class ImagePickerGridController: UICollectionViewController, PhotoLibraryDelegat
func photoLibraryDidChange(_ photoLibrary: PhotoLibrary) {
photoCollectionContents = photoCollection.contents()
collectionView?.reloadData()
}
// MARK: - PhotoCollectionPicker Presentation

View File

@ -469,8 +469,17 @@ class MediaPageViewController: UIPageViewController, UIPageViewControllerDataSou
// If the screen wasn't presented or it was presented from a location which isn't the
// MediaTileViewController then just pop/dismiss the screen
let parentNavController: UINavigationController? = {
switch self.presentingViewController {
case let topBannerController as TopBannerController:
return topBannerController.children.first as? UINavigationController
default: return self.presentingViewController as? UINavigationController
}
}()
guard
let presentingNavController: UINavigationController = (self.presentingViewController as? UINavigationController),
let presentingNavController: UINavigationController = parentNavController,
!(presentingNavController.viewControllers.last is AllMediaViewController)
else {
guard self.navigationController?.viewControllers.count == 1 else {

View File

@ -99,7 +99,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
)
if Environment.shared?.callManager.wrappedValue?.currentCall == nil {
UserDefaults.sharedLokiProject?.set(false, forKey: "isCallOngoing")
UserDefaults.sharedLokiProject?[.isCallOngoing] = false
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = nil
}
// No point continuing if we are running tests
@ -441,7 +442,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
// Don't offer the 'Restore' option if it was a 'startupFailed' error as a restore is unlikely to
// resolve it (most likely the database is locked or the key was somehow lost - safer to get them
// to restart and manually reinstall/restore)
case .databaseError(StorageError.startupFailed): break
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED): break
// Offer the 'Restore' option if it was a migration error
case .databaseError:
@ -695,41 +696,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
/// On application startup the `Storage.read` can be slightly slow while GRDB spins up it's database
/// read pools (up to a few seconds), since this read is blocking we want to dispatch it to run async to ensure
/// we don't block user interaction while it's running
DispatchQueue.global(qos: .default).async {
DispatchQueue.global(qos: .default).async(using: dependencies) {
let unreadCount: Int = dependencies[singleton: .storage]
.read { db in
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
Interaction.Columns.hasMention == true
)
.joining(
required: Interaction.thread
.aliased(thread)
.joining(optional: SessionThread.contact)
.filter(
// Ignore muted threads
SessionThread.Columns.mutedUntilTimestamp == nil ||
SessionThread.Columns.mutedUntilTimestamp < Date().timeIntervalSince1970
)
.filter(
// Ignore message request threads
SessionThread.Columns.variant != SessionThread.Variant.contact ||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
)
)
.fetchCount(db)
}
.read(using: dependencies) { db in try Interaction.fetchUnreadCount(db) }
.defaulting(to: 0)
DispatchQueue.main.async {
CurrentAppContext().setMainAppBadgeNumber(unreadCount)
DispatchQueue.main.async(using: dependencies) {
UIApplication.shared.applicationIconBadgeNumber = unreadCount
}
}
}
@ -944,7 +917,9 @@ private enum StartupError: Error {
var name: String {
switch self {
case .databaseError(StorageError.startupFailed): return "Database startup failed"
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
return "Database startup failed"
case .databaseError(StorageError.migrationNoLongerSupported): return "Unsupported version"
case .failedToRestore: return "Failed to restore"
case .databaseError: return "Database error"
@ -954,9 +929,12 @@ private enum StartupError: Error {
var message: String {
switch self {
case .databaseError(StorageError.startupFailed): return "DATABASE_STARTUP_FAILED".localized()
case .databaseError(StorageError.startupFailed), .databaseError(DatabaseError.SQLITE_LOCKED):
return "DATABASE_STARTUP_FAILED".localized()
case .databaseError(StorageError.migrationNoLongerSupported):
return "DATABASE_UNSUPPORTED_MIGRATION".localized()
case .failedToRestore: return "DATABASE_RESTORE_FAILED".localized()
case .databaseError: return "DATABASE_MIGRATION_FAILED".localized()
case .startupTimeout: return "APP_STARTUP_TIMEOUT".localized()

View File

@ -179,11 +179,6 @@ final class MainAppContext: NSObject, AppContext {
UIApplication.shared.isIdleTimerDisabled = shouldBeBlocking
}
func setMainAppBadgeNumber(_ value: Int) {
UIApplication.shared.applicationIconBadgeNumber = value
UserDefaults.sharedLokiProject?.setValue(value, forKey: "currentBadgeNumber")
}
func frontmostViewController() -> UIViewController? {
UIApplication.shared.frontmostViewControllerIgnoringAlerts
}

View File

@ -247,7 +247,7 @@ class BlockedContactsViewModel: SessionTableViewModel<NoNav, BlockedContactsView
dependencies[singleton: .storage].write { db in
_ = try Contact
.filter(ids: contactIds)
.updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false))
.updateAllAndConfig(db, Contact.Columns.isBlocked.set(to: false), using: dependencies)
}
self?.selectedContactIdsSubject.send([])

View File

@ -279,7 +279,7 @@ final class NukeDataModal: Modal {
// Clear the app badge and notifications
AppEnvironment.shared.notificationPresenter.clearAllNotifications()
CurrentAppContext().setMainAppBadgeNumber(0)
UIApplication.shared.applicationIconBadgeNumber = 0
// Clear out the user defaults
UserDefaults.removeAll()

View File

@ -167,7 +167,7 @@ public extension UIContextualAction {
db,
threadId: threadViewModel.threadId,
threadVariant: threadViewModel.threadVariant,
groupLeaveType: .forced,
groupLeaveType: .silent,
calledFromConfigHandling: false
)
}
@ -216,7 +216,8 @@ public extension UIContextualAction {
.updateAllAndConfig(
db,
SessionThread.Columns.pinnedPriority
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0))
.set(to: (threadViewModel.threadPinnedPriority == 0 ? 1 : 0)),
using: dependencies
)
}
}
@ -319,7 +320,7 @@ public extension UIContextualAction {
.save(db)
try Contact
.filter(id: threadViewModel.threadId)
.updateAllAndConfig(db, contactChanges)
.updateAllAndConfig(db, contactChanges, using: dependencies)
// Blocked message requests should be deleted
if threadIsMessageRequest {

View File

@ -139,7 +139,7 @@ public extension BlindedIdLookup {
if isCheckingForOutbox && !contact.isApproved {
try Contact
.filter(id: contact.id)
.updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true))
.updateAllAndConfig(db, Contact.Columns.isApproved.set(to: true), using: dependencies)
}
break

View File

@ -193,7 +193,9 @@ public extension ClosedGroup {
.filter(id: group.id)
.updateAllAndConfig(
db,
ClosedGroup.Columns.invited.set(to: false)
ClosedGroup.Columns.invited.set(to: false),
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
}

View File

@ -509,6 +509,46 @@ public extension Interaction {
// MARK: - GRDB Interactions
public extension Interaction {
struct ReadInfo: Decodable, FetchableRecord {
let id: Int64
let variant: Interaction.Variant
let timestampMs: Int64
let wasRead: Bool
}
static func fetchUnreadCount(
_ db: Database,
using dependencies: Dependencies = Dependencies()
) throws -> Int {
let userPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let thread: TypedTableAlias<SessionThread> = TypedTableAlias()
return try Interaction
.filter(Interaction.Columns.wasRead == false)
.filter(Interaction.Variant.variantsToIncrementUnreadCount.contains(Interaction.Columns.variant))
.filter(
// Only count mentions if 'onlyNotifyForMentions' is set
thread[.onlyNotifyForMentions] == false ||
Interaction.Columns.hasMention == true
)
.joining(
required: Interaction.thread
.aliased(thread)
.joining(optional: SessionThread.contact)
.filter(
// Ignore muted threads
SessionThread.Columns.mutedUntilTimestamp == nil ||
SessionThread.Columns.mutedUntilTimestamp < dependencies.dateNow.timeIntervalSince1970
)
.filter(
// Ignore message request threads
SessionThread.Columns.variant != SessionThread.Variant.contact ||
!SessionThread.isMessageRequest(userPublicKey: userPublicKey)
)
)
.fetchCount(db)
}
/// This will update the `wasRead` state the the interaction
///
/// - Parameters
@ -527,93 +567,16 @@ public extension Interaction {
) throws {
guard let interactionId: Int64 = interactionId else { return }
struct InteractionReadInfo: Decodable, FetchableRecord {
let id: Int64
let variant: Interaction.Variant
let timestampMs: Int64
let wasRead: Bool
}
// Once all of the below is done schedule the jobs
func scheduleJobs(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
interactionInfo: [InteractionReadInfo],
lastReadTimestampMs: Int64,
using dependencies: Dependencies
) throws {
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
// messages `expiresStartedAtMs` values in local database, and create seperate
// jobs updating message expiration
dependencies[singleton: .jobRunner].upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interactionIds: interactionInfo.map { $0.id },
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()),
threadId: threadId,
using: dependencies
),
canStartJob: true,
using: dependencies
)
// Update the last read timestamp if needed
try SessionUtil.syncThreadLastReadIfNeeded(
db,
threadId: threadId,
threadVariant: threadVariant,
lastReadTimestampMs: lastReadTimestampMs,
using: dependencies
)
// Clear out any notifications for the interactions we mark as read
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
identifiers: interactionInfo
.map { interactionInfo in
Interaction.notificationIdentifier(
for: interactionInfo.id,
threadId: threadId,
shouldGroupMessagesForThread: false
)
}
.appending(Interaction.notificationIdentifier(
for: 0,
threadId: threadId,
shouldGroupMessagesForThread: true
))
)
// If we want to send read receipts and it's a contact thread then try to add the
// 'SendReadReceiptsJob' for and unread messages that weren't outgoing
if trySendReadReceipt && threadVariant == .contact {
dependencies[singleton: .jobRunner].upsert(
db,
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
db,
threadId: threadId,
interactionIds: interactionInfo
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
.map { $0.id },
using: dependencies
),
canStartJob: true,
using: dependencies
)
}
}
// Since there is no guarantee on the order messages are inserted into the database
// fetch the timestamp for the interaction and set everything before that as read
let maybeInteractionInfo: InteractionReadInfo? = try Interaction
let maybeInteractionInfo: Interaction.ReadInfo? = try Interaction
.select(.id, .variant, .timestampMs, .wasRead)
.filter(id: interactionId)
.asRequest(of: InteractionReadInfo.self)
.asRequest(of: Interaction.ReadInfo.self)
.fetchOne(db)
// If we aren't including older interactions then update and save the current one
guard includingOlder, let interactionInfo: InteractionReadInfo = maybeInteractionInfo else {
guard includingOlder, let interactionInfo: Interaction.ReadInfo = maybeInteractionInfo else {
// Only mark as read and trigger the subsequent jobs if the interaction is
// actually not read (no point updating and triggering db changes otherwise)
guard
@ -630,12 +593,12 @@ public extension Interaction {
.filter(id: interactionId)
.updateAll(db, Columns.wasRead.set(to: true))
try scheduleJobs(
try Interaction.scheduleReadJobs(
db,
threadId: threadId,
threadVariant: threadVariant,
interactionInfo: [
InteractionReadInfo(
Interaction.ReadInfo(
id: interactionId,
variant: variant,
timestampMs: 0,
@ -643,6 +606,8 @@ public extension Interaction {
)
],
lastReadTimestampMs: timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false,
using: dependencies
)
return
@ -652,21 +617,23 @@ public extension Interaction {
.filter(Interaction.Columns.threadId == threadId)
.filter(Interaction.Columns.timestampMs <= interactionInfo.timestampMs)
.filter(Interaction.Columns.wasRead == false)
let interactionInfoToMarkAsRead: [InteractionReadInfo] = try interactionQuery
let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
.select(.id, .variant, .timestampMs, .wasRead)
.asRequest(of: InteractionReadInfo.self)
.asRequest(of: Interaction.ReadInfo.self)
.fetchAll(db)
// If there are no other interactions to mark as read then just schedule the jobs
// for this interaction (need to ensure the disapeparing messages run for sync'ed
// outgoing messages which will always have 'wasRead' as false)
guard !interactionInfoToMarkAsRead.isEmpty else {
try scheduleJobs(
try Interaction.scheduleReadJobs(
db,
threadId: threadId,
threadVariant: threadVariant,
interactionInfo: [interactionInfo],
lastReadTimestampMs: interactionInfo.timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false,
using: dependencies
)
return
@ -676,12 +643,14 @@ public extension Interaction {
try interactionQuery.updateAll(db, Columns.wasRead.set(to: true))
// Retrieve the interaction ids we want to update
try scheduleJobs(
try Interaction.scheduleReadJobs(
db,
threadId: threadId,
threadVariant: threadVariant,
interactionInfo: interactionInfoToMarkAsRead,
lastReadTimestampMs: interactionInfo.timestampMs,
trySendReadReceipt: trySendReadReceipt,
calledFromConfigHandling: false,
using: dependencies
)
}
@ -749,6 +718,81 @@ public extension Interaction {
.asSet()
.subtracting(timestampsUpdated)
}
static func scheduleReadJobs(
_ db: Database,
threadId: String,
threadVariant: SessionThread.Variant,
interactionInfo: [Interaction.ReadInfo],
lastReadTimestampMs: Int64,
trySendReadReceipt: Bool,
calledFromConfigHandling: Bool,
using dependencies: Dependencies
) throws {
guard !interactionInfo.isEmpty else { return }
// Update the last read timestamp if needed
if !calledFromConfigHandling {
try SessionUtil.syncThreadLastReadIfNeeded(
db,
threadId: threadId,
threadVariant: threadVariant,
lastReadTimestampMs: lastReadTimestampMs,
using: dependencies
)
}
// Add the 'DisappearingMessagesJob' if needed - this will update any expiring
// messages `expiresStartedAtMs` values in local database, and create seperate
// jobs updating message expiration
dependencies[singleton: .jobRunner].upsert(
db,
job: DisappearingMessagesJob.updateNextRunIfNeeded(
db,
interactionIds: interactionInfo.map { $0.id },
startedAtMs: TimeInterval(SnodeAPI.currentOffsetTimestampMs()),
threadId: threadId,
using: dependencies
),
canStartJob: true,
using: dependencies
)
// Clear out any notifications for the interactions we mark as read
Environment.shared?.notificationsManager.wrappedValue?.cancelNotifications(
identifiers: interactionInfo
.map { interactionInfo in
Interaction.notificationIdentifier(
for: interactionInfo.id,
threadId: threadId,
shouldGroupMessagesForThread: false
)
}
.appending(Interaction.notificationIdentifier(
for: 0,
threadId: threadId,
shouldGroupMessagesForThread: true
))
)
/// If we want to send read receipts and it's a contact thread then try to add the `SendReadReceiptsJob` for and unread
/// messages that weren't outgoing
if trySendReadReceipt && threadVariant == .contact {
dependencies[singleton: .jobRunner].upsert(
db,
job: SendReadReceiptsJob.createOrUpdateIfNeeded(
db,
threadId: threadId,
interactionIds: interactionInfo
.filter { !$0.wasRead && $0.variant != .standardOutgoing }
.map { $0.id },
using: dependencies
),
canStartJob: true,
using: dependencies
)
}
}
}
// MARK: - Search Queries

View File

@ -310,28 +310,42 @@ public extension SessionThread {
using dependencies: Dependencies = Dependencies()
) throws {
let currentUserPublicKey: String = getUserHexEncodedPublicKey(db, using: dependencies)
let remainingThreadIds: [String] = threadIds.filter { $0 != currentUserPublicKey }
let remainingThreadIds: Set<String> = threadIds.asSet().removing(currentUserPublicKey)
switch (threadVariant, groupLeaveType) {
case (.contact, _):
case (.contact, .standard), (.contact, .silent):
// Clear any interactions for the deleted thread
_ = try Interaction
.filter(threadIds.contains(Interaction.Columns.threadId))
.deleteAll(db)
// We need to custom handle the 'Note to Self' conversation (it should just be
// hidden rather than deleted
// hidden locally rather than deleted)
if threadIds.contains(currentUserPublicKey) {
_ = try Interaction
.filter(Interaction.Columns.threadId == currentUserPublicKey)
.deleteAll(db)
_ = try SessionThread
.filter(id: currentUserPublicKey)
.updateAllAndConfig(
db,
SessionThread.Columns.pinnedPriority.set(to: 0),
SessionThread.Columns.shouldBeVisible.set(to: false),
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
return
}
// Update any other threads to be hidden (don't want to actually delete the thread
// record in case it's settings get changed while it's not visible)
_ = try SessionThread
.filter(ids: remainingThreadIds)
.updateAllAndConfig(
db,
SessionThread.Columns.pinnedPriority.set(to: SessionUtil.hiddenPriority),
SessionThread.Columns.shouldBeVisible.set(to: false),
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
case (.contact, .forced):
// If this wasn't called from config handling then we need to hide the conversation
if !calledFromConfigHandling {
try SessionUtil

View File

@ -220,6 +220,7 @@ public final class OpenGroupManager {
db,
OpenGroup.Columns.isActive.set(to: true),
OpenGroup.Columns.sequenceNumber.set(to: 0),
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
}
@ -362,7 +363,12 @@ public final class OpenGroupManager {
// If it's a session-run room then just set it to inactive
_ = try? OpenGroup
.filter(id: openGroupId)
.updateAllAndConfig(db, OpenGroup.Columns.isActive.set(to: false))
.updateAllAndConfig(
db,
OpenGroup.Columns.isActive.set(to: false),
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
}
// Remove the thread and associated data
@ -450,7 +456,7 @@ public final class OpenGroupManager {
try OpenGroup
.filter(id: openGroup.id)
.updateAllAndConfig(db, changes)
.updateAllAndConfig(db, changes, using: dependencies)
// Update the admin/moderator group members
if let roomDetails: OpenGroupAPI.Room = pollInfo.details {

View File

@ -15,6 +15,7 @@ extension MessageSender {
members: [GroupMember],
preparedNotificationsSubscription: HTTP.PreparedRequest<PushNotificationAPI.SubscribeResponse>?
)
public static func createGroup(
name: String,
displayPicture: SignalAttachment?,
@ -182,5 +183,8 @@ extension MessageSender {
// Update the group
_ = try ClosedGroup
.filter(id: groupIdentityPublicKey)
.updateAllAndConfig(db, ClosedGroup.Columns.name.set(to: name))
.updateAllAndConfig(db, ClosedGroup.Columns.name.set(to: name), using: dependencies)
}
}
}
}

View File

@ -41,7 +41,7 @@ extension PushNotificationAPI.NotificationMetadata {
hash: try container.decode(String.self, forKey: .hash),
namespace: try container.decode(Int.self, forKey: .namespace),
dataLength: try container.decode(Int.self, forKey: .dataLength),
dataTooLong: ((try? container.decode(Bool.self, forKey: .dataTooLong)) ?? false)
dataTooLong: ((try? container.decode(Int.self, forKey: .dataTooLong) != 0) ?? false)
)
}
}

View File

@ -405,8 +405,11 @@ public enum PushNotificationAPI {
return (envelope, .legacySuccess)
}
guard let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String else {
return (nil, .failureNoContent)
}
guard
let base64EncodedEncString: String = notificationContent.userInfo["enc_payload"] as? String,
let encData: Data = Data(base64Encoded: base64EncodedEncString),
let notificationsEncryptionKey: Data = try? getOrGenerateEncryptionKey(using: dependencies),
encData.count > dependencies[singleton: .crypto].size(.aeadXChaCha20NonceBytes)
@ -434,7 +437,7 @@ public enum PushNotificationAPI {
// If the metadata says that the message was too large then we should show the generic
// notification (this is a valid case)
guard !notification.info.dataTooLong else { return (nil, .success) }
guard !notification.info.dataTooLong else { return (nil, .successTooLong) }
// Check that the body we were given is valid
guard

View File

@ -5,7 +5,9 @@ import Foundation
public extension PushNotificationAPI {
enum ProcessResult {
case success
case successTooLong
case failure
case failureNoContent
case legacySuccess
case legacyFailure
case legacyForceSilent

View File

@ -140,7 +140,7 @@ internal extension SessionUtil {
.fetchOne(db)
let threadExists: Bool = (threadInfo != nil)
let updatedShouldBeVisible: Bool = SessionUtil.shouldBeVisible(priority: data.priority)
/// If we are hiding the conversation then kick the user from it if it's currently open
if !updatedShouldBeVisible {
SessionUtil.kickFromConversationUIIfNeeded(removedThreadIds: [sessionId])

View File

@ -77,16 +77,30 @@ internal extension SessionUtil {
}
// Mark all older interactions as read
try Interaction
.filter(
Interaction.Columns.threadId == threadId &&
Interaction.Columns.timestampMs <= lastReadTimestampMs &&
Interaction.Columns.wasRead == false
)
let interactionQuery = Interaction
.filter(Interaction.Columns.threadId == threadId)
.filter(Interaction.Columns.timestampMs <= lastReadTimestampMs)
.filter(Interaction.Columns.wasRead == false)
let interactionInfoToMarkAsRead: [Interaction.ReadInfo] = try interactionQuery
.select(.id, .variant, .timestampMs, .wasRead)
.asRequest(of: Interaction.ReadInfo.self)
.fetchAll(db)
try interactionQuery
.updateAll( // Handling a config update so don't use `updateAllAndConfig`
db,
Interaction.Columns.wasRead.set(to: true)
)
try Interaction.scheduleReadJobs(
db,
threadId: threadId,
threadVariant: threadInfo.variant,
interactionInfo: interactionInfoToMarkAsRead,
lastReadTimestampMs: lastReadTimestampMs,
trySendReadReceipt: false, // Interactions already read, no need to send
calledFromConfigHandling: true,
using: dependencies
)
// Update old disappearing after read messages to start
DisappearingMessagesJob.updateNextRunIfNeeded(
db,

View File

@ -112,6 +112,7 @@ internal extension SessionUtil {
// MARK: - Outgoing Changes
internal extension SessionUtil {
}
// MARK: - MemberData

View File

@ -112,7 +112,7 @@ internal extension SessionUtil {
db,
threadId: userPublicKey,
threadVariant: .contact,
groupLeaveType: .forced,
groupLeaveType: .silent,
calledFromConfigHandling: true,
using: dependencies
)

View File

@ -53,15 +53,17 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
func updateAllAndConfig(
_ db: Database,
_ assignments: ConfigColumnAssignment...,
calledFromConfig: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> Int {
return try updateAllAndConfig(db, assignments, using: dependencies)
return try updateAllAndConfig(db, assignments, calledFromConfig: calledFromConfig, using: dependencies)
}
@discardableResult
func updateAllAndConfig(
_ db: Database,
_ assignments: [ConfigColumnAssignment],
calledFromConfig: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> Int {
let targetAssignments: [ColumnAssignment] = assignments.map { $0.assignment }
@ -71,7 +73,12 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
return try self.updateAll(db, targetAssignments)
}
return try self.updateAndFetchAllAndUpdateConfig(db, assignments, using: dependencies).count
return try self.updateAndFetchAllAndUpdateConfig(
db,
assignments,
calledFromConfig: calledFromConfig,
using: dependencies
).count
}
// MARK: -- updateAndFetchAll
@ -80,22 +87,32 @@ public extension QueryInterfaceRequest where RowDecoder: FetchableRecord & Table
func updateAndFetchAllAndUpdateConfig(
_ db: Database,
_ assignments: ConfigColumnAssignment...,
calledFromConfig: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> [RowDecoder] {
return try updateAndFetchAllAndUpdateConfig(db, assignments, using: dependencies)
return try updateAndFetchAllAndUpdateConfig(
db,
assignments,
calledFromConfig: calledFromConfig,
using: dependencies
)
}
@discardableResult
func updateAndFetchAllAndUpdateConfig(
_ db: Database,
_ assignments: [ConfigColumnAssignment],
calledFromConfig: Bool = false,
using dependencies: Dependencies = Dependencies()
) throws -> [RowDecoder] {
// First perform the actual updates
let updatedData: [RowDecoder] = try self.updateAndFetchAll(db, assignments.map { $0.assignment })
// Then check if any of the changes could affect the config
guard SessionUtil.assignmentsRequireConfigUpdate(assignments) else { return updatedData }
guard
!calledFromConfig &&
SessionUtil.assignmentsRequireConfigUpdate(assignments)
else { return updatedData }
defer {
// If we changed a column that requires a config update then we may as well automatically

View File

@ -598,7 +598,12 @@ public struct ProfileManager {
else {
try Profile
.filter(id: publicKey)
.updateAllAndConfig(db, profileChanges)
.updateAllAndConfig(
db,
profileChanges,
calledFromConfig: calledFromConfigHandling,
using: dependencies
)
}
}

View File

@ -56,11 +56,9 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
notificationContent.sound = thread.notificationSound
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
.notificationSound(isQuiet: false)
// Badge Number
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
.map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0))
// Title & body
let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType]
@ -157,16 +155,11 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol {
let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = userInfo
notificationContent.sound = thread.notificationSound
.defaulting(
to: db[.defaultNotificationSound]
.defaulting(to: Preferences.Sound.defaultNotificationSound)
)
.defaulting(to: db[.defaultNotificationSound] ?? Preferences.Sound.defaultNotificationSound)
.notificationSound(isQuiet: false)
// Badge Number
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
.map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0))
notificationContent.title = "Session"
notificationContent.body = ""

View File

@ -0,0 +1,18 @@
// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved.
import Foundation
import SessionMessagingKit
enum NotificationError: LocalizedError {
case processing(PushNotificationAPI.ProcessResult)
case messageProcessing
case messageHandling(MessageReceiverError)
public var errorDescription: String? {
switch self {
case .processing(let result): return "Failed to process notification (\(result))"
case .messageProcessing: return "Failed to process message"
case .messageHandling(let error): return "Failed to handle message (\(error))"
}
}
}

View File

@ -15,11 +15,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
private var didPerformSetup = false
private var contentHandler: ((UNNotificationContent) -> Void)?
private var request: UNNotificationRequest?
private var openGroupPollCancellable: AnyCancellable?
public static let isFromRemoteKey = "remote"
public static let threadIdKey = "Signal.AppNotificationsUserInfoKey.threadId"
public static let threadVariantRaw = "Signal.AppNotificationsUserInfoKey.threadVariantRaw"
public static let threadNotificationCounter = "Session.AppNotificationsUserInfoKey.threadNotificationCounter"
private static let callPreOfferLargeNotificationSupressionDuration: TimeInterval = 30
// MARK: Did receive a remote push notification request
@ -27,16 +29,16 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
self.contentHandler = contentHandler
self.request = request
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return self.completeSilenty()
}
// Called via the OS so create a default 'Dependencies' instance
let dependencies: Dependencies = Dependencies()
guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else {
return self.completeSilenty(using: dependencies)
}
// Abort if the main app is running
guard !(UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else {
return self.completeSilenty()
return self.completeSilenty(using: dependencies)
}
/// Create the context if we don't have it (needed before _any_ interaction with the database)
@ -46,6 +48,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let isCallOngoing: Bool = (UserDefaults.sharedLokiProject?[.isCallOngoing])
.defaulting(to: false)
let lastCallPreOffer: Date? = UserDefaults.sharedLokiProject?[.lastCallPreOffer]
// Perform main setup
Storage.resumeDatabaseAccess(using: dependencies)
@ -55,14 +58,13 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
AppReadiness.runNowOrWhenAppDidBecomeReady {
let openGroupPollingPublishers: [AnyPublisher<Void, Error>] = self.pollForOpenGroups(using: dependencies)
defer {
Publishers
self.openGroupPollCancellable = Publishers
.MergeMany(openGroupPollingPublishers)
.subscribe(on: DispatchQueue.global(qos: .background))
.subscribe(on: DispatchQueue.main)
.sinkUntilComplete(
receiveCompletion: { _ in
self.completeSilenty()
}
.sink(
receiveCompletion: { [weak self] _ in self?.completeSilenty(using: dependencies) },
receiveValue: { _ in }
)
}
@ -78,9 +80,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// If we got an explicit failure, or we got a success but no content then show
// the fallback notification
case .success, .legacySuccess, .failure, .legacyFailure:
return self.handleFailure(for: notificationContent)
case .legacyForceSilent: return
return self.handleFailure(for: notificationContent, error: .processing(result))
case .successTooLong:
/// If the notification is too long and there is an ongoing call or a recent call pre-offer then we assume the notification
/// is a call `ICE_CANDIDATES` message and just complete silently (because the fallback would be annoying), if not
/// then we do want to show the fallback notification
guard
isCallOngoing ||
(lastCallPreOffer ?? Date.distantPast).timeIntervalSinceNow < NotificationServiceExtension.callPreOfferLargeNotificationSupressionDuration
else { return self.handleFailure(for: notificationContent, error: .processing(result)) }
NSLog("[NotificationServiceExtension] Suppressing large notification too close to a call.")
return
case .legacyForceSilent, .failureNoContent: return
}
}
@ -90,10 +104,26 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
dependencies[singleton: .storage].write { db in
do {
guard let processedMessage: ProcessedMessage = try Message.processRawReceivedMessageAsNotification(db, envelope: envelope) else {
self.handleFailure(for: notificationContent)
self.handleFailure(for: notificationContent, error: .messageProcessing)
return
}
/// Due to the way the `CallMessage` and `SharedConfigMessage` work we need to custom
/// handle their behaviours, for all other message types we want to just use standard messages
switch processedMessage.messageInfo.message {
case is CallMessage, is SharedConfigMessage: break
default:
try MessageReceiver.handle(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: processedMessage.messageInfo.message,
serverExpirationTimestamp: processedMessage.messageInfo.serverExpirationTimestamp,
associatedWithProto: processedMessage.proto
)
return
}
// Throw if the message is outdated and shouldn't be processed
try MessageReceiver.throwIfMessageOutdated(
db,
@ -103,47 +133,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
)
switch processedMessage.messageInfo.message {
case let visibleMessage as VisibleMessage:
let interactionId: Int64 = try MessageReceiver.handleVisibleMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: visibleMessage,
associatedWithProto: processedMessage.proto
)
// Remove the notifications if there is an outgoing messages from a linked device
if
let interaction: Interaction = try? Interaction.fetchOne(db, id: interactionId),
interaction.variant == .standardOutgoing
{
let semaphore = DispatchSemaphore(value: 0)
let center = UNUserNotificationCenter.current()
center.getDeliveredNotifications { notifications in
let matchingNotifications = notifications.filter({ $0.request.content.userInfo[NotificationServiceExtension.threadIdKey] as? String == interaction.threadId })
center.removeDeliveredNotifications(withIdentifiers: matchingNotifications.map({ $0.request.identifier }))
// Hack: removeDeliveredNotifications seems to be async,need to wait for some time before the delivered notifications can be removed.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { semaphore.signal() }
}
semaphore.wait()
}
case let unsendRequest as UnsendRequest:
try MessageReceiver.handleUnsendRequest(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: unsendRequest
)
case let closedGroupControlMessage as ClosedGroupControlMessage:
try MessageReceiver.handleLegacyClosedGroupControlMessage(
db,
threadId: processedMessage.threadId,
threadVariant: processedMessage.threadVariant,
message: closedGroupControlMessage
)
case let callMessage as CallMessage:
try MessageReceiver.handleCallMessage(
db,
@ -152,7 +141,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
message: callMessage
)
guard case .preOffer = callMessage.kind else { return self.completeSilenty() }
guard case .preOffer = callMessage.kind else {
return self.completeSilenty(using: dependencies)
}
if !db[.areCallsEnabled] {
if
@ -190,7 +181,8 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
break
}
self.handleSuccessForIncomingCall(db, for: callMessage)
try MessageReceiver.insertCallInfoMessage(db, for: callMessage)
self.handleSuccessForIncomingCall(db, for: callMessage, using: dependencies)
case let sharedConfigMessage as SharedConfigMessage:
try SessionUtil.handleConfigMessages(
@ -213,8 +205,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
catch {
if let error = error as? MessageReceiverError, error.isRetryable {
switch error {
case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage: self.completeSilenty()
default: self.handleFailure(for: notificationContent)
case .invalidGroupPublicKey, .noGroupKeyPair, .outdatedMessage:
self.completeSilenty(using: dependencies)
default: self.handleFailure(for: notificationContent, error: .messageHandling(error))
}
}
}
@ -243,6 +237,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
Cryptography.seedRandom()
AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: true,
appSpecificBlock: {
Environment.shared?.notificationsManager.mutate {
$0 = NSENotificationPresenter()
@ -253,7 +248,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// Only 'NSLog' works in the extension - viewable via Console.app
case .failure(let error):
NSLog("[NotificationServiceExtension] Failed to complete migrations: \(error)")
self?.completeSilenty()
self?.completeSilenty(using: dependencies)
case .success:
// We should never receive a non-voip notification on an app that doesn't support
@ -263,7 +258,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// and don't disturb the user. Messages will be processed when they open the app.
guard dependencies[singleton: .storage][.isReadyForAppExtensions] else {
NSLog("[NotificationServiceExtension] Not ready for extensions")
self?.completeSilenty()
self?.completeSilenty(using: dependencies)
return
}
@ -309,7 +304,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// App isn't ready until storage is ready AND all version migrations are complete.
guard dependencies[singleton: .storage].isValid && migrationsCompleted else {
NSLog("[NotificationServiceExtension] Storage invalid")
self.completeSilenty()
self.completeSilenty(using: dependencies)
return
}
@ -322,19 +317,33 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
// MARK: Handle completion
override public func serviceExtensionTimeWillExpire() {
// Called via the OS so create a default 'Dependencies' instance
let dependencies: Dependencies = Dependencies()
// Called just before the extension will be terminated by the system.
// Use this as an opportunity to deliver your "best attempt" at modified content, otherwise the original push payload will be used.
completeSilenty()
NSLog("[NotificationServiceExtension] Execution time expired")
openGroupPollCancellable?.cancel()
completeSilenty(using: dependencies)
}
private func completeSilenty() {
private func completeSilenty(using dependencies: Dependencies) {
NSLog("[NotificationServiceExtension] Complete silently")
let silentContent: UNMutableNotificationContent = UNMutableNotificationContent()
silentContent.badge = dependencies[singleton: .storage]
.read { db in try Interaction.fetchUnreadCount(db, using: dependencies) }
.map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0))
Storage.suspendDatabaseAccess()
self.contentHandler!(.init())
self.contentHandler!(silentContent)
}
private func handleSuccessForIncomingCall(_ db: Database, for callMessage: CallMessage) {
private func handleSuccessForIncomingCall(
_ db: Database,
for callMessage: CallMessage,
using dependencies: Dependencies
) {
if #available(iOSApplicationExtension 14.5, *), Preferences.isCallKitSupported {
guard let caller: String = callMessage.sender, let timestamp = callMessage.sentTimestamp else { return }
@ -347,11 +356,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
CXProvider.reportNewIncomingVoIPPushPayload(payload) { error in
if let error = error {
self.handleFailureForVoIP(db, for: callMessage)
SNLog("Failed to notify main app of call message: \(error)")
NSLog("[NotificationServiceExtension] Failed to notify main app of call message: \(error)")
}
else {
self.completeSilenty()
SNLog("Successfully notified main app of call message.")
NSLog("[NotificationServiceExtension] Successfully notified main app of call message.")
UserDefaults.sharedLokiProject?[.lastCallPreOffer] = Date()
self.completeSilenty(using: dependencies)
}
}
}
@ -364,11 +374,9 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
let notificationContent = UNMutableNotificationContent()
notificationContent.userInfo = [ NotificationServiceExtension.isFromRemoteKey : true ]
notificationContent.title = "Session"
// Badge Number
let newBadgeNumber = CurrentAppContext().appUserDefaults().integer(forKey: "currentBadgeNumber") + 1
notificationContent.badge = NSNumber(value: newBadgeNumber)
CurrentAppContext().appUserDefaults().set(newBadgeNumber, forKey: "currentBadgeNumber")
notificationContent.badge = (try? Interaction.fetchUnreadCount(db))
.map { NSNumber(value: $0) }
.defaulting(to: NSNumber(value: 0))
if let sender: String = callMessage.sender {
let senderDisplayName: String = Profile.displayName(db, id: sender, threadVariant: .contact)
@ -384,20 +392,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension
UNUserNotificationCenter.current().add(request) { error in
if let error = error {
SNLog("Failed to add notification request due to error:\(error)")
NSLog("[NotificationServiceExtension] Failed to add notification request due to error: \(error)")
}
semaphore.signal()
}
semaphore.wait()
SNLog("Add remote notification request")
NSLog("[NotificationServiceExtension] Add remote notification request")
}
private func handleFailure(for content: UNMutableNotificationContent) {
private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) {
NSLog("[NotificationServiceExtension] Show generic failure message due to error: \(error)")
Storage.suspendDatabaseAccess()
content.body = "You've got a new message"
content.title = "Session"
let userInfo: [String:Any] = [ NotificationServiceExtension.isFromRemoteKey : true ]
content.body = "APN_Message".localized()
let userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true ]
content.userInfo = userInfo
contentHandler!(content)
}

View File

@ -71,7 +71,6 @@ final class NotificationServiceExtensionContext : NSObject, AppContext {
func ensureSleepBlocking(_ shouldBeBlocking: Bool, blockingObjectsDescription: String) { }
func frontmostViewController() -> UIViewController? { nil }
func runNowOr(whenMainAppIsActive block: @escaping AppActiveBlock) { }
func setMainAppBadgeNumber(_ value: Int) { }
func setNetworkActivityIndicatorVisible(_ value: Bool) { }
func setStatusBarHidden(_ isHidden: Bool, animated isAnimated: Bool) { }
}

View File

@ -186,10 +186,6 @@ final class ShareAppExtensionContext: NSObject, AppContext {
OWSLogger.debug("Ignoring request to block sleep.")
}
func setMainAppBadgeNumber(_ value: Int) {
owsFailDebug("")
}
func setNetworkActivityIndicatorVisible(_ value: Bool) {
owsFailDebug("")
}

View File

@ -124,6 +124,10 @@ public struct Job: Codable, Equatable, Hashable, Identifiable, FetchableRecord,
/// This is a job that runs once whenever a message is marked as read because of syncing from user config and
/// needs to get expiration from network
case getExpiration
/// This is a job which sends an invitation to a member of a group asynchronously so the admin doesn't need to
/// wait during group creation
case groupInviteMemberJob
}
public enum Behaviour: Int, Codable, DatabaseValueConvertible, CaseIterable {

View File

@ -198,7 +198,7 @@ open class Storage {
guard isValid, let dbWriter: DatabaseWriter = dbWriter else {
let error: Error = (startupError ?? StorageError.startupFailed)
SNLog("[Database Error] Statup failed with error: \(error)")
onComplete(.failure(StorageError.startupFailed), false)
onComplete(.failure(error), false)
return
}
@ -485,12 +485,16 @@ open class Storage {
try? deleteDbKeys()
}
public static func reconfigureDatabase(using dependencies: Dependencies = Dependencies()) {
dependencies[singleton: .storage].configureDatabase(using: dependencies)
}
public static func resetForCleanMigration(using dependencies: Dependencies = Dependencies()) {
// Clear existing content
resetAllStorage()
// Reconfigure
dependencies[singleton: .storage].configureDatabase(using: dependencies)
reconfigureDatabase(using: dependencies)
}
private static func deleteDatabaseFiles() {

View File

@ -4,6 +4,8 @@ import Foundation
import GRDB
public class Dependencies {
static let userInfoKey: CodingUserInfoKey = CodingUserInfoKey(rawValue: "io.oxen.dependencies.codingOptions")!
private static var singletonInstances: Atomic<[Int: Any]> = Atomic([:])
private static var cacheInstances: Atomic<[Int: MutableCacheType]> = Atomic([:])

View File

@ -73,9 +73,6 @@ NSString *NSStringForUIApplicationState(UIApplicationState value);
// Should be a NOOP if isMainApp is NO.
- (void)ensureSleepBlocking:(BOOL)shouldBeBlocking blockingObjects:(NSArray<id> *)blockingObjects;
// Should only be called if isMainApp is YES.
- (void)setMainAppBadgeNumber:(NSInteger)value;
- (void)setStatusBarHidden:(BOOL)isHidden animated:(BOOL)isAnimated;
@property (nonatomic, readonly) CGFloat statusBarHeight;

View File

@ -51,6 +51,7 @@ public enum SNUserDefaults {
case lastOpen
case lastGarbageCollection
case lastPushNotificationSync
case lastCallPreOffer
}
public enum Double: Swift.String {

View File

@ -2,13 +2,6 @@
import Foundation
public protocol BencodableType {
associatedtype ValueType: BencodableType
static var isCollection: Bool { get }
static var isDictionary: Bool { get }
}
public struct BencodeResponse<T: Decodable> {
public let info: T
public let data: Data?
@ -59,23 +52,61 @@ public enum Bencode {
using dependencies: Dependencies = Dependencies()
) throws -> BencodeResponse<T> where T: Decodable {
guard
let result: [Data] = try? decode([Data].self, from: data),
let responseData: Data = result.first
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
decodedData.remainingData.isEmpty == true, // Ensure there is no left over data
let resultArray: [Any] = decodedData.value as? [Any],
resultArray.count > 0
else { throw HTTPError.parsingFailed }
return BencodeResponse(
info: try responseData.decoded(as: T.self, using: dependencies),
data: (result.count > 1 ? result.last : nil)
info: try Bencode.decode(T.self, decodedValue: resultArray[0], using: dependencies),
data: {
guard resultArray.count > 1 else { return nil }
switch resultArray.last {
case let bencodeString as BencodeString: return bencodeString.rawValue
default: return resultArray.last as? Data
}
}()
)
}
public static func decode<T: BencodableType>(_ type: T.Type, from data: Data) throws -> T {
public static func decode<T: Decodable>(
_ type: T.Type,
from data: Data,
using dependencies: Dependencies = Dependencies()
) throws -> T {
guard
let decodedData: (value: Any, remainingData: Data) = decodeData(data),
decodedData.remainingData.isEmpty == true // Ensure there is no left over data
else { throw HTTPError.parsingFailed }
return try recursiveCast(type, from: decodedData.value)
return try Bencode.decode(T.self, decodedValue: decodedData.value, using: dependencies)
}
private static func decode<T: Decodable>(
_ type: T.Type,
decodedValue: Any,
using dependencies: Dependencies = Dependencies()
) throws -> T {
switch (decodedValue, T.self) {
case (let directResult as T, _): return directResult
case
(let bencodeString as BencodeString, is String.Type),
(let bencodeString as BencodeString, is Optional<String>.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, _):
return try bencodeString.rawValue.decoded(as: T.self, using: dependencies)
default:
guard
let jsonifiedInfo: Any = try? jsonify(decodedValue),
let infoData: Data = try? JSONSerialization.data(withJSONObject: jsonifiedInfo)
else { throw HTTPError.parsingFailed }
return try infoData.decoded(as: T.self, using: dependencies)
}
}
// MARK: - Logic
@ -191,74 +222,12 @@ public enum Bencode {
// MARK: - Internal Functions
private static func recursiveCast<T: BencodableType>(_ type: T.Type, from value: Any) throws -> T {
switch (type.isCollection, type.isDictionary) {
case (_, true):
guard let dictValue: [String: Any] = value as? [String: Any] else { throw HTTPError.parsingFailed }
return try (
dictValue.mapValues { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
case (true, _):
guard let arrayValue: [Any] = value as? [Any] else { throw HTTPError.parsingFailed }
return try (
arrayValue.map { try recursiveCast(type.ValueType.self, from: $0) } as? T ??
{ throw HTTPError.parsingFailed }()
)
default:
switch (value, type) {
case (let bencodeString as BencodeString, is String.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, is Optional<String>.Type):
return try (bencodeString.value as? T ?? { throw HTTPError.parsingFailed }())
case (let bencodeString as BencodeString, _):
return try (bencodeString.rawValue as? T ?? { throw HTTPError.parsingFailed }())
default: return try (value as? T ?? { throw HTTPError.parsingFailed }())
}
private static func jsonify(_ value: Any) throws -> Any {
switch value {
case let arrayValue as [Any]: return try arrayValue.map { try jsonify($0) } as Any
case let dictValue as [String: Any]: return try dictValue.mapValues { try jsonify($0) } as Any
case let bencodeString as BencodeString: return bencodeString.value as Any
default: return value
}
}
}
// MARK: - BencodableType Extensions
extension Data: BencodableType {
public typealias ValueType = Data
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Int: BencodableType {
public typealias ValueType = Int
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension String: BencodableType {
public typealias ValueType = String
public static var isCollection: Bool { false }
public static var isDictionary: Bool { false }
}
extension Array: BencodableType where Element: BencodableType {
public typealias ValueType = Element
public static var isCollection: Bool { true }
public static var isDictionary: Bool { false }
}
extension Dictionary: BencodableType where Key == String, Value: BencodableType {
public typealias ValueType = Value
public static var isCollection: Bool { false }
public static var isDictionary: Bool { true }
}

View File

@ -13,11 +13,37 @@ class BencodeSpec: QuickSpec {
let stringValue: String
}
struct TestType2: Codable, Equatable {
let stringValue: String
let boolValue: Bool
}
struct TestType3: Codable, Equatable {
let stringValue: String
let boolValue: Bool
init(_ stringValue: String, _ boolValue: Bool) {
self.stringValue = stringValue
self.boolValue = boolValue
}
init(from decoder: Decoder) throws {
let container: KeyedDecodingContainer<CodingKeys> = try decoder.container(keyedBy: CodingKeys.self)
self = TestType3(
try container.decode(String.self, forKey: .stringValue),
((try? container.decode(Bool.self, forKey: .boolValue)) ?? false)
)
}
}
// MARK: - Spec
override func spec() {
describe("Bencode") {
// MARK: - when decoding
context("when decoding") {
// MARK: -- should decode a basic string
it("should decode a basic string") {
let basicStringData: Data = "5:howdy".data(using: .utf8)!
let result = try? Bencode.decode(String.self, from: basicStringData)
@ -25,6 +51,7 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal("howdy"))
}
// MARK: -- should decode a basic integer
it("should decode a basic integer") {
let basicIntegerData: Data = "i3e".data(using: .utf8)!
let result = try? Bencode.decode(Int.self, from: basicIntegerData)
@ -32,6 +59,7 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal(3))
}
// MARK: -- should decode a list of integers
it("should decode a list of integers") {
let basicIntListData: Data = "li1ei2ee".data(using: .utf8)!
let result = try? Bencode.decode([Int].self, from: basicIntListData)
@ -39,57 +67,246 @@ class BencodeSpec: QuickSpec {
expect(result).to(equal([1, 2]))
}
// MARK: -- should decode a basic dict
it("should decode a basic dict") {
let basicDictData: Data = "d4:spaml1:a1:bee".data(using: .utf8)!
let result = try? Bencode.decode([String: [String]].self, from: basicDictData)
expect(result).to(equal(["spam": ["a", "b"]]))
}
// MARK: -- decodes a decodable type
it("decodes a decodable type") {
let data: Data = "d8:intValuei100e11:stringValue4:Test".data(using: .utf8)!
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
}
// MARK: -- decodes a stringified decodable type
it("decodes a stringified decodable type") {
let data: Data = "37:{\"intValue\":100,\"stringValue\":\"Test\"}".data(using: .utf8)!
let result: TestType? = try? Bencode.decode(TestType.self, from: data)
expect(result).to(equal(TestType(intValue: 100, stringValue: "Test")))
}
}
// MARK: - when decoding a response
context("when decoding a response") {
it("decodes successfully") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
// MARK: -- with a decodable type
context("with a decodable type") {
// MARK: ---- decodes successfully
it("decodes successfully") {
let data: Data = "ld8:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
}
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
// MARK: -- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "ld8:intValuei100e11:stringValue4:Teste"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
}
// MARK: ---- throws a parsing error when given an invalid length
it("throws a parsing error when given an invalid length") {
let data: Data = "ld12:intValuei100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
// MARK: ---- throws a parsing error when given an invalid key
it("throws a parsing error when given an invalid key") {
let data: Data = "ld7:INVALIDi100e11:stringValue4:Teste5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
// MARK: ---- decodes correctly when trying to decode an int to a bool with custom handling
it("decodes correctly when trying to decode an int to a bool with custom handling") {
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType3> = try Bencode.decodeResponse(from: data)
_ = result
}.toNot(throwError(HTTPError.parsingFailed))
}
// MARK: ---- throws a parsing error when trying to decode an int to a bool
it("throws a parsing error when trying to decode an int to a bool") {
let data: Data = "ld9:boolValuei1e11:stringValue4:testee"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType2> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
it("decodes successfully with no body") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
// MARK: -- with stringified json info
context("with stringified json info") {
// MARK: -- decodes successfully
it("decodes successfully") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: Data([1, 2, 3, 4, 5])
)
))
}
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
// MARK: -- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "l37:{\"intValue\":100,\"stringValue\":\"Test\"}e"
.data(using: .utf8)!
let result: BencodeResponse<TestType>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: TestType(
intValue: 100,
stringValue: "Test"
),
data: nil
)
))
}
// MARK: -- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
it("throws a parsing error when invalid") {
let data: Data = "l36:{\"INVALID\":100,\"stringValue\":\"Test\"}5:\u{01}\u{02}\u{03}\u{04}\u{05}e"
.data(using: .utf8)!
expect {
let result: BencodeResponse<TestType> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
// MARK: -- with a string value
context("with a string value") {
// MARK: ---- decodes successfully
it("decodes successfully") {
let data: Data = "l4:Test5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: "Test",
data: Data([1, 2, 3, 4, 5])
)
))
}
// MARK: ---- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "l4:Teste".data(using: .utf8)!
let result: BencodeResponse<String>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: "Test",
data: nil
)
))
}
// MARK: ---- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l10:Teste".data(using: .utf8)!
expect {
let result: BencodeResponse<String> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
// MARK: -- with an int value
context("with an int value") {
// MARK: ---- decodes successfully
it("decodes successfully") {
let data: Data = "li100e5:\u{01}\u{02}\u{03}\u{04}\u{05}e".data(using: .utf8)!
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: 100,
data: Data([1, 2, 3, 4, 5])
)
))
}
// MARK: ---- decodes successfully with no body
it("decodes successfully with no body") {
let data: Data = "li100ee".data(using: .utf8)!
let result: BencodeResponse<Int>? = try? Bencode.decodeResponse(from: data)
expect(result)
.to(equal(
BencodeResponse(
info: 100,
data: nil
)
))
}
// MARK: ---- throws a parsing error when invalid
it("throws a parsing error when invalid") {
let data: Data = "l4:Teste".data(using: .utf8)!
expect {
let result: BencodeResponse<Int> = try Bencode.decodeResponse(from: data)
_ = result
}.to(throwError(HTTPError.parsingFailed))
}
}
}
}

View File

@ -11,12 +11,35 @@ public enum AppSetup {
private static let hasRun: Atomic<Bool> = Atomic(false)
public static func setupEnvironment(
retrySetupIfDatabaseInvalid: Bool = false,
appSpecificBlock: @escaping () -> (),
migrationProgressChanged: ((CGFloat, TimeInterval) -> ())? = nil,
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> (),
using dependencies: Dependencies = Dependencies()
) {
guard !AppSetup.hasRun.wrappedValue else { return }
// If we've already run the app setup then only continue under certain circumstances
guard !AppSetup.hasRun.wrappedValue else {
let storageIsValid: Bool = dependencies[singleton: .storage].isValid
switch (retrySetupIfDatabaseInvalid, storageIsValid) {
case (true, false):
Storage.reconfigureDatabase(using: dependencies)
AppSetup.hasRun.mutate { $0 = false }
AppSetup.setupEnvironment(
retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop
appSpecificBlock: appSpecificBlock,
migrationProgressChanged: migrationProgressChanged,
migrationsCompletion: migrationsCompletion
)
default:
migrationsCompletion(
(storageIsValid ? .success(()) : .failure(StorageError.startupFailed)),
false
)
}
return
}
AppSetup.hasRun.mutate { $0 = true }
@ -67,14 +90,6 @@ public enum AppSetup {
migrationsCompletion: @escaping (Result<Void, Error>, Bool) -> (),
using dependencies: Dependencies
) {
// If the database can't be initialised into a valid state then error
guard dependencies[singleton: .storage].isValid else {
DispatchQueue.main.async {
migrationsCompletion(Result.failure(StorageError.databaseInvalid), false)
}
return
}
var backgroundTask: OWSBackgroundTask? = (backgroundTask ?? OWSBackgroundTask(labelStr: #function))
dependencies[singleton: .storage].perform(