diff --git a/LibSession-Util b/LibSession-Util index de7d8a6580..f19df114e5 160000 --- a/LibSession-Util +++ b/LibSession-Util @@ -1 +1 @@ -Subproject commit de7d8a6580d8317007460d8dcbf4ce821644f80a +Subproject commit f19df114e5f4f6c29112f49c0b4897d7b93b78f1 diff --git a/Scripts/drone-static-upload.sh b/Scripts/drone-static-upload.sh index 1363a4b830..a840462f62 100755 --- a/Scripts/drone-static-upload.sh +++ b/Scripts/drone-static-upload.sh @@ -34,12 +34,15 @@ else fi if [ -n "$DRONE_TAG" ]; then - # For a tag build use something like `session-ios-v1.2.3` + # For a tag build use something like `session-ios-v1.2.3`, stored directly in the repo directory base="session-ios-$DRONE_TAG-$suffix" + upload_to="oxen.rocks/${DRONE_REPO// /_}" else # Otherwise build a length name from the datetime and commit hash, such as: # session-ios-20200522T212342Z-04d7dcc54 + # stored in a branch directory for the repo base="session-ios-$(date --date=@$DRONE_BUILD_CREATED +%Y%m%dT%H%M%SZ)-${DRONE_COMMIT:0:9}-$suffix" + upload_to="oxen.rocks/${DRONE_REPO// /_}/${DRONE_BRANCH// /_}" fi # Copy over the build products @@ -51,8 +54,6 @@ cp -av $target_path "$base" archive="$base.tar.xz" tar cJvf "$archive" "$base" -upload_to="oxen.rocks/${DRONE_REPO// /_}/${DRONE_BRANCH// /_}" - # sftp doesn't have any equivalent to mkdir -p, so we have to split the above up into a chain of # -mkdir a/, -mkdir a/b/, -mkdir a/b/c/, ... commands. The leading `-` allows the command to fail # without error. diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index fcc09622fa..96b9cddb65 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -621,7 +621,6 @@ FD5E93D12C100FD70038C25A /* FileUploadResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4387127B5BB3B00C60D73 /* FileUploadResponse.swift */; }; FD5E93D22C12B0580038C25A /* AppVersionResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC4383727B3863200C60D73 /* AppVersionResponse.swift */; }; FD5E93D82C12E3B50038C25A /* FileUploadResponseSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FD3C905F27E410F700CD579F /* FileUploadResponseSpec.swift */; }; - FD6A38E62C2A4D8E00762359 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E52C2A4D8E00762359 /* GRDB */; }; FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */; }; FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EB2C2A63B500762359 /* KeychainSwift */; }; FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */ = {isa = PBXBuildFile; productRef = FD6A38EE2C2A641200762359 /* DifferenceKit */; }; @@ -832,6 +831,8 @@ 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 */; }; + FDC289422C86AB5800020BC2 /* GRDB in Frameworks */ = {isa = PBXBuildFile; productRef = FDC289412C86AB5800020BC2 /* GRDB */; }; + FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDC289462C881A3800020BC2 /* MutableIdentifiable.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 */; }; @@ -884,7 +885,7 @@ FDD250702837199200198BDA /* GarbageCollectionJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */; }; FDD250722837234B00198BDA /* MediaGalleryNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */; }; FDD82C3F2A205D0A00425F05 /* ProcessResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */; }; - FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */; }; + FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */; }; FDDD554C2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */; }; FDDD554E2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */; }; FDDF074429C3E3D000E5E8B5 /* FetchRequest+Utilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */; }; @@ -1989,6 +1990,7 @@ FDC13D552A171FE4007267C7 /* UnsubscribeRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeRequest.swift; sourceTree = ""; }; FDC13D572A17207D007267C7 /* UnsubscribeResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnsubscribeResponse.swift; sourceTree = ""; }; FDC13D592A1721C5007267C7 /* LegacyNotifyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyNotifyRequest.swift; sourceTree = ""; }; + FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MutableIdentifiable.swift; sourceTree = ""; }; FDC2908627D7047F005DAE71 /* RoomSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomSpec.swift; sourceTree = ""; }; FDC2908827D70656005DAE71 /* RoomPollInfoSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RoomPollInfoSpec.swift; sourceTree = ""; }; FDC2908A27D707F3005DAE71 /* SendMessageRequestSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendMessageRequestSpec.swift; sourceTree = ""; }; @@ -2042,7 +2044,7 @@ FDD2506F2837199200198BDA /* GarbageCollectionJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GarbageCollectionJob.swift; sourceTree = ""; }; FDD250712837234B00198BDA /* MediaGalleryNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaGalleryNavigationController.swift; sourceTree = ""; }; FDD82C3E2A205D0A00425F05 /* ProcessResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProcessResult.swift; sourceTree = ""; }; - FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LibSessionTypeConversionUtilitiesSpec.swift; sourceTree = ""; }; + FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TypeConversionUtilitiesSpec.swift; sourceTree = ""; }; FDDD554B2C1FC812006CBF03 /* CheckForAppUpdatesJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckForAppUpdatesJob.swift; sourceTree = ""; }; FDDD554D2C1FCB77006CBF03 /* _019_ScheduleAppUpdateCheckJob.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = _019_ScheduleAppUpdateCheckJob.swift; sourceTree = ""; }; FDDF074329C3E3D000E5E8B5 /* FetchRequest+Utilities.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "FetchRequest+Utilities.swift"; sourceTree = ""; }; @@ -2221,10 +2223,10 @@ files = ( FD6A39662C2D21E400762359 /* libwebp in Frameworks */, FD7F74632BAAA4CA006DDFD8 /* libSessionUtil.a in Frameworks */, - FD6A38E62C2A4D8E00762359 /* GRDB in Frameworks */, FD6A38EC2C2A63B500762359 /* KeychainSwift in Frameworks */, FD6A38EF2C2A641200762359 /* DifferenceKit in Frameworks */, FD6A396D2C2D284B00762359 /* YYImage in Frameworks */, + FDC289422C86AB5800020BC2 /* GRDB in Frameworks */, FD6A38E92C2A630E00762359 /* CocoaLumberjackSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; @@ -3643,6 +3645,7 @@ FDFF9FDE2A787F57005E0628 /* JSONEncoder+Utilities.swift */, FD6A38F02C2A66B100762359 /* KeychainStorageType.swift */, FD09797127FAA2F500936362 /* Optional+Utilities.swift */, + FDC289462C881A3800020BC2 /* MutableIdentifiable.swift */, FD09797C27FBDB2000936362 /* Notification+Utilities.swift */, FDF222082818D2B0000A4995 /* NSAttributedString+Utilities.swift */, FDC0F0032BFECE12002CBFB9 /* TimeUnit.swift */, @@ -4126,6 +4129,7 @@ FD83B9B927CF20A5005E1583 /* General */, FDDF074829DAB35200E5E8B5 /* JobRunner */, FD9B30F1293EA0AF008DEE3E /* Networking */, + FDC289482C881C5500020BC2 /* LibSession */, FDFBB7522A2023DE00CA7350 /* Utilities */, ); path = SessionUtilitiesKitTests; @@ -4191,7 +4195,6 @@ FD8ECF802934385900C0D1BB /* LibSession */ = { isa = PBXGroup; children = ( - FDDC08F029A300D500BF9681 /* Utilities */, FDA1E83829A5771A00C5C3BD /* LibSessionUtilSpec.swift */, FDA1E83C29AC71A800C5C3BD /* LibSessionSpec.swift */, ); @@ -4267,6 +4270,14 @@ path = Types; sourceTree = ""; }; + FDC289482C881C5500020BC2 /* LibSession */ = { + isa = PBXGroup; + children = ( + FDDC08F029A300D500BF9681 /* Utilities */, + ); + path = LibSession; + sourceTree = ""; + }; FDC2909227D710A9005DAE71 /* Types */ = { isa = PBXGroup; children = ( @@ -4380,7 +4391,7 @@ FDDC08F029A300D500BF9681 /* Utilities */ = { isa = PBXGroup; children = ( - FDDC08F129A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift */, + FDDC08F129A300E800BF9681 /* TypeConversionUtilitiesSpec.swift */, ); path = Utilities; sourceTree = ""; @@ -4744,12 +4755,12 @@ ); name = SessionUtilitiesKit; packageProductDependencies = ( - FD6A38E52C2A4D8E00762359 /* GRDB */, FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */, FD6A38EB2C2A63B500762359 /* KeychainSwift */, FD6A38EE2C2A641200762359 /* DifferenceKit */, FD6A39652C2D21E400762359 /* libwebp */, FD6A396C2C2D284B00762359 /* YYImage */, + FDC289412C86AB5800020BC2 /* GRDB */, ); productName = SessionUtilities; productReference = C3C2A679255388CC00C340D1 /* SessionUtilitiesKit.framework */; @@ -4787,6 +4798,7 @@ D221A087169C9E5E00537ABF /* Resources */, 453518771FC635DD00210559 /* Embed Foundation Extensions */, 4535189F1FC63DBF00210559 /* Embed Frameworks */, + FDC289452C88113300020BC2 /* Copy GRDB framework */, FDD82C422A2085B900425F05 /* Add Commit Hash To Build Info Plist */, FD5E93D32C12D3990038C25A /* Add App Group To Build Info Plist */, FDC498BF2AC1747900EDD897 /* Ensure Localizable.strings included */, @@ -5096,7 +5108,6 @@ ); mainGroup = D221A07E169C9E5E00537ABF; packageReferences = ( - FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */, FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */, FD6A38EA2C2A63B500762359 /* XCRemoteSwiftPackageReference "keychain-swift" */, FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */, @@ -5107,6 +5118,7 @@ FD6A39392C2AD3A300762359 /* XCRemoteSwiftPackageReference "Nimble" */, FD6A39642C2D21E400762359 /* XCRemoteSwiftPackageReference "libwebp-Xcode" */, FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */, + FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */, ); productRefGroup = D221A08A169C9E5E00537ABF /* Products */; projectDirPath = ""; @@ -5397,6 +5409,26 @@ shellScript = "\"${SRCROOT}/Scripts/build_libSession_util.sh\"\n"; showEnvVarsInLog = 0; }; + FDC289452C88113300020BC2 /* Copy GRDB framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Copy GRDB framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "# This script copies GRDB.framework to the bundle and signs it\n# It's required because GRDB is not an explicit app dependency\n# and as such it can't be selected in \"Copy Frameworks\" build phase.\ngrdb_source_dir=\"${BUILT_PRODUCTS_DIR}/GRDB.framework\"\ngrdb_install_dir=\"${BUILT_PRODUCTS_DIR}/${FRAMEWORKS_FOLDER_PATH}/GRDB.framework\"\n\n# Remove any existing files in the destination\nrm -rf \"${grdb_install_dir}\"\nmkdir -p \"${grdb_install_dir}\"\n\n# Copy the framework and the Info.plist\ncp -f \"${grdb_source_dir}/GRDB\" \"${grdb_source_dir}/Info.plist\" \"${grdb_install_dir}\"\n\n# Sign the framework directory contents\n/usr/bin/codesign \\\n --force \\\n --sign \"${EXPANDED_CODE_SIGN_IDENTITY}\" \\\n --timestamp=none \\\n --preserve-metadata=identifier,entitlements,flags \\\n --generate-entitlement-der \"${grdb_install_dir}\"\n"; + showEnvVarsInLog = 0; + }; FDC498BF2AC1747900EDD897 /* Ensure Localizable.strings included */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -5839,6 +5871,7 @@ B87EF18126377A1D00124B3C /* Features.swift in Sources */, FD09797727FAB7A600936362 /* Data+Image.swift in Sources */, FD7F745B2BAAA35E006DDFD8 /* LibSession.swift in Sources */, + FDC289472C881A3800020BC2 /* MutableIdentifiable.swift in Sources */, C300A60D2554B31900555489 /* Logging.swift in Sources */, FD6A39242C2AAE4500762359 /* CGRect+Utilities.swift in Sources */, B8FF8EA625C11FEF004D1F22 /* IPv4.swift in Sources */, @@ -6368,7 +6401,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( - FDDC08F229A300E800BF9681 /* LibSessionTypeConversionUtilitiesSpec.swift in Sources */, + FDDC08F229A300E800BF9681 /* TypeConversionUtilitiesSpec.swift in Sources */, FDC2909427D710B4005DAE71 /* SOGSEndpointSpec.swift in Sources */, FD96F3A529DBC3DC00401309 /* MessageSendJobSpec.swift in Sources */, FDC2909127D709CA005DAE71 /* SOGSMessageSpec.swift in Sources */, @@ -7673,7 +7706,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 483; + CURRENT_PROJECT_VERSION = 488; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7710,7 +7743,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.7.3; + MARKETING_VERSION = 2.7.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -7751,7 +7784,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 483; + CURRENT_PROJECT_VERSION = 488; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -7783,7 +7816,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.7.3; + MARKETING_VERSION = 2.7.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -8585,14 +8618,6 @@ /* End XCConfigurationList section */ /* Begin XCRemoteSwiftPackageReference section */ - FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { - isa = XCRemoteSwiftPackageReference; - repositoryURL = "https://github.com/oxen-io/session-grdb-swift"; - requirement = { - kind = upToNextMajorVersion; - minimumVersion = 106.27.0; - }; - }; FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/CocoaLumberjack/CocoaLumberjack.git"; @@ -8673,6 +8698,14 @@ minimumVersion = 1.1.0; }; }; + FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/oxen-io/session-grdb-swift.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 106.29.2; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -8706,11 +8739,6 @@ package = FD6A38ED2C2A641200762359 /* XCRemoteSwiftPackageReference "DifferenceKit" */; productName = DifferenceKit; }; - FD6A38E52C2A4D8E00762359 /* GRDB */ = { - isa = XCSwiftPackageProductDependency; - package = FD6A38E42C2A4D8E00762359 /* XCRemoteSwiftPackageReference "session-grdb-swift" */; - productName = GRDB; - }; FD6A38E82C2A630E00762359 /* CocoaLumberjackSwift */ = { isa = XCSwiftPackageProductDependency; package = FD6A38E72C2A630E00762359 /* XCRemoteSwiftPackageReference "CocoaLumberjack" */; @@ -8809,6 +8837,11 @@ package = FD6A39672C2D283A00762359 /* XCRemoteSwiftPackageReference "session-ios-yyimage" */; productName = YYImage; }; + FDC289412C86AB5800020BC2 /* GRDB */ = { + isa = XCSwiftPackageProductDependency; + package = FDC289402C86AB5800020BC2 /* XCRemoteSwiftPackageReference "session-grdb-swift" */; + productName = GRDB; + }; FDEF57292C3CF50B00131302 /* WebRTC */ = { isa = XCSwiftPackageProductDependency; package = FD6A390E2C2A93CD00762359 /* XCRemoteSwiftPackageReference "WebRTC" */; diff --git a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index d7d38c716e..52c3e4e66f 100644 --- a/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Session.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "b77f342bc30b5d1971118f9ad70bf3af24442c4edb9a716222ad6bc0fb9b3f8e", + "originHash" : "77d5fda90891573a263c0fefeb989f3f1bb56e612a09be32106558b6d5fb4564", "pins" : [ { "identity" : "cocoalumberjack", @@ -85,10 +85,10 @@ { "identity" : "session-grdb-swift", "kind" : "remoteSourceControl", - "location" : "https://github.com/oxen-io/session-grdb-swift", + "location" : "https://github.com/oxen-io/session-grdb-swift.git", "state" : { - "revision" : "52043c998154b39ecd8e069ba22244bf36464c61", - "version" : "106.27.1" + "revision" : "426149dd868219517df20eebeca27ff385fee34a", + "version" : "106.29.2" } }, { diff --git a/Session/Calls/Call Management/SessionCallManager.swift b/Session/Calls/Call Management/SessionCallManager.swift index c2ccfac720..fe787544fc 100644 --- a/Session/Calls/Call Management/SessionCallManager.swift +++ b/Session/Calls/Call Management/SessionCallManager.swift @@ -205,7 +205,7 @@ public final class SessionCallManager: NSObject, CallManagerProtocol { // Stop all jobs except for message sending and when completed suspend the database JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { _ in LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() Log.flush() } } diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 521f6ea57b..2c16174bdc 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -192,7 +192,7 @@ extension ConversationVC: threadVariant: SessionThread.Variant, messageText: String? ) { - sendMessage(text: (messageText ?? ""), attachments: attachments, using: viewModel.dependencies) + sendMessage(text: (messageText ?? ""), attachments: attachments) resetMentions() dismiss(animated: true) { [weak self] in @@ -222,7 +222,7 @@ extension ConversationVC: threadVariant: SessionThread.Variant, messageText: String? ) { - sendMessage(text: (messageText ?? ""), attachments: attachments, using: viewModel.dependencies) + sendMessage(text: (messageText ?? ""), attachments: attachments) resetMentions() dismiss(animated: true) { [weak self] in @@ -298,7 +298,7 @@ extension ConversationVC: let threadId: String = self.viewModel.threadData.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant - Permissions.requestLibraryPermissionIfNeeded { [weak self, dependencies = viewModel.dependencies] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self, dependencies = viewModel.dependencies] in DispatchQueue.main.async { let sendMediaNavController = SendMediaNavigationController.showingMediaLibraryFirst( threadId: threadId, @@ -467,8 +467,7 @@ extension ConversationVC: attachments: [SignalAttachment] = [], linkPreviewDraft: LinkPreviewDraft? = nil, quoteModel: QuotedReplyModel? = nil, - hasPermissionToSendSeed: Bool = false, - using dependencies: Dependencies = Dependencies() + hasPermissionToSendSeed: Bool = false ) { guard !showBlockedModalIfNeeded() else { return } @@ -539,17 +538,14 @@ extension ConversationVC: quoteModel: quoteModel ) - sendMessage(optimisticData: optimisticData, using: dependencies) + sendMessage(optimisticData: optimisticData) } - private func sendMessage( - optimisticData: ConversationViewModel.OptimisticMessageData, - using dependencies: Dependencies - ) { + private func sendMessage(optimisticData: ConversationViewModel.OptimisticMessageData) { let threadId: String = self.viewModel.threadData.threadId let threadVariant: SessionThread.Variant = self.viewModel.threadData.threadVariant - DispatchQueue.global(qos:.userInitiated).async(using: dependencies) { + DispatchQueue.global(qos:.userInitiated).async(using: viewModel.dependencies) { [dependencies = viewModel.dependencies] in // Generate the quote thumbnail if needed (want this to happen outside of the DBWrite thread as // this can take up to 0.5s let quoteThumbnailAttachment: Attachment? = optimisticData.quoteModel?.attachment?.cloneAsQuoteThumbnail() @@ -902,8 +898,22 @@ extension ConversationVC: // For call info messages show the "call missed" modal guard cellViewModel.variant != .infoCall else { - let callMissedTipsModal: CallMissedTipsModal = CallMissedTipsModal(caller: cellViewModel.authorName) - present(callMissedTipsModal, animated: true, completion: nil) + // If the failure was due to the mic permission being denied then we want to show the permission modal, + // otherwise we want to show the call missed tips modal + guard + let infoMessageData: Data = (cellViewModel.rawBody ?? "").data(using: .utf8), + let messageInfo: CallMessage.MessageInfo = try? JSONDecoder().decode( + CallMessage.MessageInfo.self, + from: infoMessageData + ), + messageInfo.state == .permissionDeniedMicrophone + else { + let callMissedTipsModal: CallMissedTipsModal = CallMissedTipsModal(caller: cellViewModel.authorName) + present(callMissedTipsModal, animated: true, completion: nil) + return + } + + Permissions.requestMicrophonePermissionIfNeeded(presentingViewController: self) return } @@ -1891,7 +1901,7 @@ extension ConversationVC: } // Try to send the optimistic message again - sendMessage(optimisticData: optimisticMessageData, using: dependencies) + sendMessage(optimisticData: optimisticMessageData) return } @@ -2326,30 +2336,35 @@ extension ConversationVC: guard !mediaAttachments.isEmpty else { return } - mediaAttachments.forEach { attachment, originalFilePath in - PHPhotoLibrary.shared().performChanges( - { - if attachment.isImage || attachment.isAnimated { - PHAssetChangeRequest.creationRequestForAssetFromImage( - atFileURL: URL(fileURLWithPath: originalFilePath) - ) - } - else if attachment.isVideo { - PHAssetChangeRequest.creationRequestForAssetFromVideo( - atFileURL: URL(fileURLWithPath: originalFilePath) - ) - } - }, - completionHandler: { _, _ in } - ) - } - - // Send a 'media saved' notification if needed - guard self.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { - return + Permissions.requestLibraryPermissionIfNeeded( + isSavingMedia: true, + presentingViewController: self + ) { [weak self] in + mediaAttachments.forEach { attachment, originalFilePath in + PHPhotoLibrary.shared().performChanges( + { + if attachment.isImage || attachment.isAnimated { + PHAssetChangeRequest.creationRequestForAssetFromImage( + atFileURL: URL(fileURLWithPath: originalFilePath) + ) + } + else if attachment.isVideo { + PHAssetChangeRequest.creationRequestForAssetFromVideo( + atFileURL: URL(fileURLWithPath: originalFilePath) + ) + } + }, + completionHandler: { _, _ in } + ) + } + + // Send a 'media saved' notification if needed + guard self?.viewModel.threadData.threadVariant == .contact, cellViewModel.variant == .standardIncoming else { + return + } + + self?.sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) } - - sendDataExtraction(kind: .mediaSaved(timestamp: UInt64(cellViewModel.timestampMs))) } func ban(_ cellViewModel: MessageViewModel, using dependencies: Dependencies) { @@ -2598,7 +2613,7 @@ extension ConversationVC: } // Send attachment - sendMessage(text: "", attachments: [attachment], using: dependencies) + sendMessage(text: "", attachments: [attachment]) } func cancelVoiceMessageRecording() { diff --git a/Session/Conversations/Message Cells/CallMessageCell.swift b/Session/Conversations/Message Cells/CallMessageCell.swift index d6b2176b8e..7476d2f9db 100644 --- a/Session/Conversations/Message Cells/CallMessageCell.swift +++ b/Session/Conversations/Message Cells/CallMessageCell.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import UIKit +import AVFAudio import SessionUIKit import SessionMessagingKit import SessionUtilitiesKit @@ -147,14 +148,15 @@ final class CallMessageCell: MessageCell { switch messageInfo.state { case .outgoing: return UIImage(named: "CallOutgoing")?.withRenderingMode(.alwaysTemplate) case .incoming: return UIImage(named: "CallIncoming")?.withRenderingMode(.alwaysTemplate) - case .missed, .permissionDenied: return UIImage(named: "CallMissed")?.withRenderingMode(.alwaysTemplate) + case .missed, .permissionDenied, .permissionDeniedMicrophone: + return UIImage(named: "CallMissed")?.withRenderingMode(.alwaysTemplate) default: return nil } }() iconImageView.themeTintColor = { switch messageInfo.state { case .outgoing, .incoming: return .textPrimary - case .missed, .permissionDenied: return .danger + case .missed, .permissionDenied, .permissionDeniedMicrophone: return .danger default: return nil } }() @@ -162,8 +164,13 @@ final class CallMessageCell: MessageCell { iconImageViewHeightConstraint.constant = (iconImageView.image != nil ? CallMessageCell.iconSize : 0) let shouldShowInfoIcon: Bool = ( - messageInfo.state == .permissionDenied && - !Storage.shared[.areCallsEnabled] + ( + messageInfo.state == .permissionDenied && + !Storage.shared[.areCallsEnabled] + ) || ( + messageInfo.state == .permissionDeniedMicrophone && + AVAudioSession.sharedInstance().recordPermission != .granted + ) ) infoImageViewWidthConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) infoImageViewHeightConstraint.constant = (shouldShowInfoIcon ? CallMessageCell.iconSize : 0) @@ -217,7 +224,15 @@ final class CallMessageCell: MessageCell { else { return } // Should only be tappable if the info icon is visible - guard messageInfo.state == .permissionDenied && !Storage.shared[.areCallsEnabled] else { return } + guard + ( + messageInfo.state == .permissionDenied && + !Storage.shared[.areCallsEnabled] + ) || ( + messageInfo.state == .permissionDeniedMicrophone && + AVAudioSession.sharedInstance().recordPermission != .granted + ) + else { return } self.delegate?.handleItemTapped(cellViewModel, cell: self, cellLocation: gestureRecognizer.location(in: self)) } diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index dde25b3353..a500ee7c94 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -92,14 +92,13 @@ class PhotoCapture: NSObject { self?.session.beginConfiguration() defer { self?.session.commitConfiguration() } - try self?.updateCurrentInput(position: .back) + do { try self?.updateCurrentInput(position: .back) } + catch { throw PhotoCaptureError.initializationFailed } guard let photoOutput = self?.captureOutput.photoOutput, self?.session.canAddOutput(photoOutput) == true - else { - throw PhotoCaptureError.initializationFailed - } + else { throw PhotoCaptureError.initializationFailed } if let connection = photoOutput.connection(with: .video) { if connection.isVideoStabilizationSupported { @@ -610,7 +609,13 @@ class PhotoCaptureOutputAdaptee: NSObject, ImageCaptureOutput { } func photoOutput(_ output: AVCapturePhotoOutput, didFinishProcessingPhoto photo: AVCapturePhoto, error: Error?) { - var data = photo.fileDataRepresentation()! + guard var data: Data = photo.fileDataRepresentation() else { + DispatchQueue.main.async { + self.delegate?.captureOutputDidFinishProcessing(photoData: nil, error: error) + } + return + } + // Call normalized here to fix the orientation if let srcImage = UIImage(data: data) { data = srcImage.normalizedImage().jpegData(compressionQuality: 1.0)! diff --git a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift index afc401df65..a6de5d0215 100644 --- a/Session/Media Viewing & Editing/PhotoCaptureViewController.swift +++ b/Session/Media Viewing & Editing/PhotoCaptureViewController.swift @@ -12,14 +12,12 @@ protocol PhotoCaptureViewControllerDelegate: AnyObject { func photoCaptureViewControllerDidCancel(_ photoCaptureViewController: PhotoCaptureViewController) } -enum PhotoCaptureError: Error { +enum PhotoCaptureError: Error, CustomStringConvertible { case assertionError(description: String) case initializationFailed case captureFailed -} -extension PhotoCaptureError: LocalizedError { - var localizedDescription: String { + var description: String { switch self { case .initializationFailed: return NSLocalizedString("PHOTO_CAPTURE_UNABLE_TO_INITIALIZE_CAMERA", comment: "alert title") diff --git a/Session/Media Viewing & Editing/SendMediaNavigationController.swift b/Session/Media Viewing & Editing/SendMediaNavigationController.swift index b83b23ca4a..897c791480 100644 --- a/Session/Media Viewing & Editing/SendMediaNavigationController.swift +++ b/Session/Media Viewing & Editing/SendMediaNavigationController.swift @@ -154,7 +154,7 @@ class SendMediaNavigationController: UINavigationController { } private func didTapMediaLibraryModeButton() { - Permissions.requestLibraryPermissionIfNeeded { [weak self] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in DispatchQueue.main.async { self?.fadeTo(viewControllers: ((self?.mediaLibraryViewController).map { [$0] } ?? [])) } diff --git a/Session/Meta/AppDelegate.swift b/Session/Meta/AppDelegate.swift index 155ec6bcd2..5b615bc712 100644 --- a/Session/Meta/AppDelegate.swift +++ b/Session/Meta/AppDelegate.swift @@ -147,7 +147,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD /// Apple's documentation on the matter) UNUserNotificationCenter.current().delegate = self - Storage.resumeDatabaseAccess(using: dependencies) + dependencies.storage.resumeDatabaseAccess() LibSession.resumeNetworkAccess() // Reset the 'startTime' (since it would be invalid from the last launch) @@ -212,7 +212,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD JobRunner.stopAndClearPendingJobs(exceptForVariant: .messageSend, using: dependencies) { [dependencies] neededBackgroundProcessing in if !self.hasCallOngoing() && (!neededBackgroundProcessing || Singleton.hasAppContext && Singleton.appContext.isInBackground) { LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() Log.info("[AppDelegate] completed network and database shutdowns.") Log.flush() } @@ -238,7 +238,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD UserDefaults.sharedLokiProject?[.isMainAppActive] = true // FIXME: Seems like there are some discrepancies between the expectations of how the iOS lifecycle methods work, we should look into them and ensure the code behaves as expected (in this case there were situations where these two wouldn't get called when returning from the background) - Storage.resumeDatabaseAccess(using: dependencies) + dependencies.storage.resumeDatabaseAccess() LibSession.resumeNetworkAccess() ensureRootViewController(calledFrom: .didBecomeActive) @@ -288,7 +288,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD func application(_ application: UIApplication, performFetchWithCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { Log.appResumedExecution() Log.info("Starting background fetch.") - Storage.resumeDatabaseAccess(using: dependencies) + dependencies.storage.resumeDatabaseAccess() LibSession.resumeNetworkAccess() let queue: DispatchQueue = .global(qos: .userInitiated) @@ -312,7 +312,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if Singleton.hasAppContext && Singleton.appContext.isInBackground { LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() Log.flush() } @@ -338,7 +338,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD if Singleton.hasAppContext && Singleton.appContext.isInBackground { LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() Log.flush() } @@ -471,7 +471,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD case .databaseError: alert.addAction(UIAlertAction(title: "vc_restore_title".localized(), style: .destructive) { [dependencies] _ in // Reset the current database for a clean migration - Storage.resetForCleanMigration() + dependencies.storage.resetForCleanMigration() // Hide the top banner if there was one TopBannerController.hide() @@ -805,10 +805,9 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD public func startPollersIfNeeded(shouldStartGroupPollers: Bool = true) { guard Identity.userExists() else { return } - /// There is a fun issue where if you launch without any valid paths then the pollers are guaranteed to fail their first poll due to - /// trying and failing to build paths without having the `SnodeAPI.snodePool` populated, by waiting for the - /// `JobRunner.blockingQueue` to complete we can have more confidence that paths won't fail to build incorrectly - JobRunner.afterBlockingQueue { [weak self] in + /// Start the pollers on a background thread so that any database queries they need to run don't + /// block the main thread + DispatchQueue.global(qos: .background).async { [weak self] in self?.poller.start() guard shouldStartGroupPollers else { return } @@ -820,7 +819,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD public func stopPollers(shouldStopUserPoller: Bool = true) { if shouldStopUserPoller { - poller.stopAllPollers() + poller.stop() } ClosedGroupPoller.shared.stopAllPollers() diff --git a/Session/Meta/SessionApp.swift b/Session/Meta/SessionApp.swift index 1dc53a1a35..fc02eb667c 100644 --- a/Session/Meta/SessionApp.swift +++ b/Session/Meta/SessionApp.swift @@ -117,7 +117,7 @@ public struct SessionApp { LibSession.clearSnodeCache() LibSession.suspendNetworkAccess() PushNotificationAPI.resetKeys() - Storage.resetAllStorage() + dependencies.storage.resetAllStorage() ProfileManager.resetProfileStorage() Attachment.resetAttachmentStorage() AppEnvironment.shared.notificationPresenter.clearAllNotifications() diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 8e7ed5c316..3e6fec49a0 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -244,7 +244,10 @@ public class NotificationPresenter: NotificationsProtocol { else { return } // Only notify missed calls - guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return } + switch messageInfo.state { + case .missed, .permissionDenied, .permissionDeniedMicrophone: break + default: return + } let category = AppNotificationCategory.errorMessage let previewType: Preferences.NotificationPreviewType = db[.preferencesNotificationPreviewType] @@ -264,6 +267,11 @@ public class NotificationPresenter: NotificationsProtocol { format: "modal_call_missed_tips_explanation".localized(), senderName ) + case .permissionDeniedMicrophone: + return String( + format: "call_missed".localized(), + senderName + ) case .missed: return String( format: "call_missed".localized(), diff --git a/Session/Notifications/PushRegistrationManager.swift b/Session/Notifications/PushRegistrationManager.swift index 3c1f825314..a5cf5b0906 100644 --- a/Session/Notifications/PushRegistrationManager.swift +++ b/Session/Notifications/PushRegistrationManager.swift @@ -293,7 +293,7 @@ public enum PushRegistrationError: Error { // FIXME: Initialise the `PushRegistrationManager` with a dependencies instance let dependencies: Dependencies = Dependencies() - Storage.resumeDatabaseAccess(using: dependencies) + dependencies.storage.resumeDatabaseAccess() LibSession.resumeNetworkAccess() let maybeCall: SessionCall? = Storage.shared.write { db in diff --git a/Session/Onboarding/DisplayNameScreen.swift b/Session/Onboarding/DisplayNameScreen.swift index 92dd172bb3..cd3392887e 100644 --- a/Session/Onboarding/DisplayNameScreen.swift +++ b/Session/Onboarding/DisplayNameScreen.swift @@ -116,7 +116,7 @@ struct DisplayNameScreen: View { // Try to save the user name but ignore the result ProfileManager.updateLocal( queue: .global(qos: .default), - profileName: displayName, + displayNameUpdate: .currentUserUpdate(displayName), using: dependencies ) diff --git a/Session/Settings/NukeDataModal.swift b/Session/Settings/NukeDataModal.swift index 4ca61915fc..93c5ac819b 100644 --- a/Session/Settings/NukeDataModal.swift +++ b/Session/Settings/NukeDataModal.swift @@ -179,7 +179,17 @@ final class NukeDataModal: Modal { .distinct() .asRequest(of: String.self) .fetchSet(db) - .map { ($0, try OpenGroupAPI.preparedClearInbox(db, on: $0, using: dependencies))} + .map { server in + ( + server, + try OpenGroupAPI.preparedClearInbox( + db, + on: server, + requestAndPathBuildTimeout: Network.defaultTimeout, + using: dependencies + ) + ) + } } .defaulting(to: []) .compactMap { server, preparedRequest in @@ -193,7 +203,10 @@ final class NukeDataModal: Modal { .subscribe(on: DispatchQueue.global(qos: .userInitiated)) .flatMap { results in SnodeAPI - .deleteAllMessages(namespace: .all) + .deleteAllMessages( + namespace: .all, + requestAndPathBuildTimeout: Network.defaultTimeout + ) .map { results.reduce($0) { result, next in result.updated(with: next) } } .eraseToAnyPublisher() } diff --git a/Session/Settings/SettingsViewModel.swift b/Session/Settings/SettingsViewModel.swift index 5c710fa341..eab23ed277 100644 --- a/Session/Settings/SettingsViewModel.swift +++ b/Session/Settings/SettingsViewModel.swift @@ -20,11 +20,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl private lazy var imagePickerHandler: ImagePickerHandler = ImagePickerHandler( onTransition: { [weak self] in self?.transitionToScreen($0, transitionType: $1) }, onImageDataPicked: { [weak self] resultImageData in - guard let oldDisplayName: String = self?.oldDisplayName else { return } - self?.updatedProfilePictureSelected( - name: oldDisplayName, - avatarUpdate: .uploadImageData(resultImageData) + displayPictureUpdate: .currentUserUploadImageData(resultImageData) ) } ) @@ -205,10 +202,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl self?.setIsEditing(false) self?.oldDisplayName = updatedNickname - self?.updateProfile( - name: updatedNickname, - avatarUpdate: .none - ) + self?.updateProfile(displayNameUpdate: .currentUserUpdate(updatedNickname)) } ] } @@ -527,8 +521,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl onConfirm: { modal in modal.close() }, onCancel: { [weak self] modal in self?.updateProfile( - name: existingDisplayName, - avatarUpdate: .remove, + displayPictureUpdate: .currentUserRemove, onComplete: { [weak modal] in modal?.close() } ) }, @@ -544,7 +537,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl self.transitionToScreen(modal, transitionType: .present) } - fileprivate func updatedProfilePictureSelected(name: String, avatarUpdate: ProfileManager.AvatarUpdate) { + fileprivate func updatedProfilePictureSelected(displayPictureUpdate: ProfileManager.DisplayPictureUpdate) { guard let info: ConfirmationModal.Info = self.editProfilePictureModalInfo else { return } self.editProfilePictureModal?.updateContent( @@ -552,8 +545,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl body: .image( placeholderData: UIImage(named: "profile_placeholder")?.pngData(), valueData: { - switch avatarUpdate { - case .uploadImageData(let imageData): return imageData + switch displayPictureUpdate { + case .currentUserUploadImageData(let imageData): return imageData default: return nil } }(), @@ -568,8 +561,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl confirmEnabled: true, onConfirm: { [weak self] modal in self?.updateProfile( - name: name, - avatarUpdate: avatarUpdate, + displayPictureUpdate: displayPictureUpdate, onComplete: { [weak modal] in modal?.close() } ) } @@ -578,7 +570,7 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } private func showPhotoLibraryForAvatar() { - Permissions.requestLibraryPermissionIfNeeded { [weak self] in + Permissions.requestLibraryPermissionIfNeeded(isSavingMedia: false) { [weak self] in DispatchQueue.main.async { let picker: UIImagePickerController = UIImagePickerController() picker.sourceType = .photoLibrary @@ -591,15 +583,15 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } fileprivate func updateProfile( - name: String, - avatarUpdate: ProfileManager.AvatarUpdate, + displayNameUpdate: ProfileManager.DisplayNameUpdate = .none, + displayPictureUpdate: ProfileManager.DisplayPictureUpdate = .none, onComplete: (() -> ())? = nil ) { let viewController = ModalActivityIndicatorViewController(canCancel: false) { [weak self] modalActivityIndicator in ProfileManager.updateLocal( queue: .global(qos: .default), - profileName: name, - avatarUpdate: avatarUpdate, + displayNameUpdate: displayNameUpdate, + displayPictureUpdate: displayPictureUpdate, success: { db in // Wait for the database transaction to complete before updating the UI db.afterNextTransactionNested { _ in @@ -614,8 +606,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl DispatchQueue.main.async { modalActivityIndicator.dismiss { let title: String = { - switch (avatarUpdate, error) { - case (.remove, _): return "update_profile_modal_remove_error_title".localized() + switch (displayPictureUpdate, error) { + case (.currentUserRemove, _): return "update_profile_modal_remove_error_title".localized() case (_, .avatarUploadMaxFileSizeExceeded): return "update_profile_modal_max_size_error_title".localized() @@ -623,8 +615,8 @@ class SettingsViewModel: SessionTableViewModel, NavigationItemSource, Navigatabl } }() let message: String? = { - switch (avatarUpdate, error) { - case (.remove, _): return nil + switch (displayPictureUpdate, error) { + case (.currentUserRemove, _): return nil case (_, .avatarUploadMaxFileSizeExceeded): return "update_profile_modal_max_size_error_message".localized() diff --git a/Session/Utilities/IP2Country.swift b/Session/Utilities/IP2Country.swift index 59fdf72d54..3fa8f382d5 100644 --- a/Session/Utilities/IP2Country.swift +++ b/Session/Utilities/IP2Country.swift @@ -8,10 +8,14 @@ import GRDB import SessionSnodeKit import SessionUtilitiesKit -private extension Log.Category { - static var ip2Country: Log.Category = "IP2Country" +// MARK: - Log.Level + +public extension Log.Category { + static let ip2Country: Log.Category = .create("IP2Country", defaultLevel: .info) } +// MARK: - IP2Country + public enum IP2Country { public static var isInitialized: Atomic = Atomic(false) private static var countryNamesCache: Atomic<[String: String]> = Atomic([:]) diff --git a/Session/Utilities/Permissions.swift b/Session/Utilities/Permissions.swift index f360cc7928..9557288776 100644 --- a/Session/Utilities/Permissions.swift +++ b/Session/Utilities/Permissions.swift @@ -3,6 +3,7 @@ import UIKit import Photos import PhotosUI +import AVFAudio import SessionUIKit import SessionUtilitiesKit import SessionMessagingKit @@ -96,12 +97,14 @@ public enum Permissions { } public static func requestLibraryPermissionIfNeeded( + isSavingMedia: Bool, presentingViewController: UIViewController? = nil, onAuthorized: @escaping () -> Void ) { let authorizationStatus: PHAuthorizationStatus if #available(iOS 14, *) { - authorizationStatus = PHPhotoLibrary.authorizationStatus(for: .readWrite) + let targetPermission: PHAccessLevel = (isSavingMedia ? .addOnly : .readWrite) + authorizationStatus = PHPhotoLibrary.authorizationStatus(for: targetPermission) if authorizationStatus == .notDetermined { // When the user chooses to select photos (which is the .limit status), // the PHPhotoUI will present the picker view on the top of the front view. @@ -113,7 +116,7 @@ public enum Permissions { // from showing when we request the photo library permission. SessionEnvironment.shared?.isRequestingPermission = true - PHPhotoLibrary.requestAuthorization(for: .readWrite) { status in + PHPhotoLibrary.requestAuthorization(for: targetPermission) { status in SessionEnvironment.shared?.isRequestingPermission = false if [ PHAuthorizationStatus.authorized, PHAuthorizationStatus.limited ].contains(status) { onAuthorized() diff --git a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift index f263738614..2a24082788 100644 --- a/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift +++ b/SessionMessagingKit/Crypto/Crypto+SessionMessagingKit.swift @@ -4,6 +4,7 @@ import Foundation import GRDB +import SessionSnodeKit import SessionUtil import SessionUtilitiesKit @@ -256,4 +257,25 @@ public extension Crypto.Generator { return try decryptedData ?? { throw MessageReceiverError.decryptionFailed }() } } + + static func messageServerHash( + swarmPubkey: String, + namespace: SnodeAPI.Namespace, + data: Data + ) -> Crypto.Generator { + return Crypto.Generator( + id: "messageServerHash", + args: [swarmPubkey, namespace, data] + ) { + let cSwarmPubkey: [CChar] = try swarmPubkey.cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + let cData: [CChar] = try data.base64EncodedString().cString(using: .utf8) ?? { throw LibSessionError.invalidCConversion }() + var cHash: [CChar] = [CChar](repeating: 0, count: 65) + + guard session_compute_message_hash(cSwarmPubkey, Int16(namespace.rawValue), cData, &cHash) else { + throw MessageReceiverError.decryptionFailed + } + + return String(cString: cHash) + } + } } diff --git a/SessionMessagingKit/Database/Models/SessionThread.swift b/SessionMessagingKit/Database/Models/SessionThread.swift index 2c6465139f..d3ab96bb5c 100644 --- a/SessionMessagingKit/Database/Models/SessionThread.swift +++ b/SessionMessagingKit/Database/Models/SessionThread.swift @@ -493,8 +493,24 @@ public extension SessionThread { // If the thread is a message request then we only want to notify for the first message if self.variant == .contact && isMessageRequest { + let numInteractions: Int = { + switch interaction.serverHash { + case .some(let serverHash): + return (try? self.interactions + .filter(Interaction.Columns.serverHash != serverHash) + .fetchCount(db)) + .defaulting(to: 0) + + case .none: + return (try? self.interactions + .filter(Interaction.Columns.timestampMs != interaction.timestampMs) + .fetchCount(db)) + .defaulting(to: 0) + } + }() + // We only want to show a notification for the first interaction in the thread - guard ((try? self.interactions.fetchCount(db)) ?? 0) <= 1 else { return false } + guard numInteractions == 0 else { return false } // Need to re-show the message requests section if it had been hidden if db[.hasHiddenMessageRequests] { diff --git a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift index 57d73d0afd..153a639ea4 100644 --- a/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift +++ b/SessionMessagingKit/Jobs/Types/ConfigurationSyncJob.swift @@ -203,7 +203,7 @@ public extension ConfigurationSyncJob { static func enqueue( _ db: Database, publicKey: String, - dependencies: Dependencies = Dependencies() + using dependencies: Dependencies = Dependencies() ) { // Upsert a config sync job if needed dependencies.jobRunner.upsert( diff --git a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift index 8ed6855fc7..1530f19f35 100644 --- a/SessionMessagingKit/Jobs/Types/MessageSendJob.swift +++ b/SessionMessagingKit/Jobs/Types/MessageSendJob.swift @@ -23,7 +23,7 @@ public enum MessageSendJob: JobExecutor { let detailsData: Data = job.details, let details: Details = try? JSONDecoder().decode(Details.self, from: detailsData) else { - Log.error("[MessageSendJob] Failing due to missing details") + Log.error("[MessageSendJob] Failing (\(job.id ?? -1)) due to missing details") return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) } @@ -50,7 +50,7 @@ public enum MessageSendJob: JobExecutor { let jobId: Int64 = job.id, let interactionId: Int64 = job.interactionId else { - Log.error("[MessageSendJob] Failing due to missing details") + Log.error("[MessageSendJob] Failing (\(job.id ?? -1)) due to missing details") return failure(job, JobRunnerError.missingRequiredDetails, true, dependencies) } @@ -62,7 +62,7 @@ public enum MessageSendJob: JobExecutor { // If the original interaction no longer exists then don't bother sending the message (ie. the // message was deleted before it even got sent) guard try Interaction.exists(db, id: interactionId) else { - Log.warn("[MessageSendJob] Failing due to missing interaction") + Log.warn("[MessageSendJob] Failing (\(job.id ?? -1)) due to missing interaction") return (StorageError.objectNotFound, [], []) } @@ -78,7 +78,7 @@ public enum MessageSendJob: JobExecutor { // If there were failed attachments then this job should fail (can't send a // message which has associated attachments if the attachments fail to upload) guard !allAttachmentStateInfo.contains(where: { $0.state == .failedDownload }) else { - Log.info("[MessageSendJob] Failing due to failed attachment upload") + Log.info("[MessageSendJob] Failing (\(job.id ?? -1)) due to failed attachment upload") return (AttachmentError.notUploaded, [], fileIds) } @@ -157,7 +157,7 @@ public enum MessageSendJob: JobExecutor { } } - Log.info("[MessageSendJob] Deferring due to pending attachment uploads") + Log.info("[MessageSendJob] Deferring (\(job.id ?? -1)) due to pending attachment uploads") return deferred(job, dependencies) } @@ -193,11 +193,11 @@ public enum MessageSendJob: JobExecutor { receiveCompletion: { result in switch result { case .finished: - Log.info("[MessageSendJob] Completed sending \(messageType) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s).") + Log.info("[MessageSendJob] Completed sending \(messageType) (\(job.id ?? -1)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s).") success(job, false, dependencies) case .failure(let error): - Log.info("[MessageSendJob] Failed to send \(messageType) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s) due to error: \(error).") + Log.info("[MessageSendJob] Failed to send \(messageType) (\(job.id ?? -1)) after \(.seconds(dependencies.dateNow.timeIntervalSince1970 - startTime), unit: .s) due to error: \(error).") // Actual error handling switch (error, details.message) { @@ -208,7 +208,7 @@ public enum MessageSendJob: JobExecutor { failure(job, error, true, dependencies) case (SnodeAPIError.clockOutOfSync, _): - Log.error("[MessageSendJob] \(originalSentTimestamp != nil ? "Permanently Failing" : "Failing") to send \(type(of: details.message)) due to clock out of sync issue.") + Log.error("[MessageSendJob] \(originalSentTimestamp != nil ? "Permanently Failing" : "Failing") to send \(type(of: details.message)) (\(job.id ?? -1)) due to clock out of sync issue.") failure(job, error, (originalSentTimestamp != nil), dependencies) // Don't bother retrying (it can just send a new one later but allowing retries diff --git a/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift b/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift index 21afcc6dec..2af0e650a6 100644 --- a/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift +++ b/SessionMessagingKit/Jobs/Types/UpdateProfilePictureJob.swift @@ -48,8 +48,7 @@ public enum UpdateProfilePictureJob: JobExecutor { ProfileManager.updateLocal( queue: queue, - profileName: profile.name, - avatarUpdate: (profilePictureData.map { .uploadImageData($0) } ?? .none), + displayPictureUpdate: (profilePictureData.map { .currentUserUploadImageData($0) } ?? .none), success: { db in // Need to call the 'success' closure asynchronously on the queue to prevent a reentrancy // issue as it will write to the database and this closure is already called within diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift index 27e60b391e..6ce1f5e335 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+Contacts.swift @@ -315,16 +315,13 @@ internal extension LibSession { // Update the profile data (if there is one - users we have sent a message request to may // not have profile info in certain situations) if let updatedProfile: Profile = info.profile { - let oldAvatarUrl: String? = String(libSessionVal: contact.profile_pic.url) - let oldAvatarKey: Data? = Data( - libSessionVal: contact.profile_pic.key, - count: ProfileManager.avatarAES256KeyByteLength - ) + let oldAvatarUrl: String? = contact.get(\.profile_pic.url) + let oldAvatarKey: Data? = contact.get(\.profile_pic.key) - contact.name = updatedProfile.name.toLibSession() - contact.nickname = updatedProfile.nickname.toLibSession() - contact.profile_pic.url = updatedProfile.profilePictureUrl.toLibSession() - contact.profile_pic.key = updatedProfile.profileEncryptionKey.toLibSession() + contact.set(\.name, to: updatedProfile.name) + contact.set(\.nickname, to: updatedProfile.nickname) + contact.set(\.profile_pic.url, to: updatedProfile.profilePictureUrl) + contact.set(\.profile_pic.key, to: updatedProfile.profileEncryptionKey) // Download the profile picture if needed (this can be triggered within // database reads/writes so dispatch the download to a separate queue to @@ -399,7 +396,7 @@ internal extension LibSession { guard var cContactId: [CChar] = contactData.id.cString(using: .utf8), contacts_get(conf, &contact, &cContactId), - String(libSessionVal: contact.name, nullIfEmpty: true) != nil + contact.get(\.name, nullIfEmpty: true) != nil else { LibSessionError.clear(conf) return contactData.id @@ -682,29 +679,21 @@ private extension LibSession { while !contacts_iterator_done(contactIterator, &contact) { try LibSession.checkLoopLimitReached(&infiniteLoopGuard, for: .contacts) - let contactId: String = String(cString: withUnsafeBytes(of: contact.session_id) { [UInt8]($0) } - .map { CChar($0) } - .nullTerminated() - ) + let contactId: String = contact.get(\.session_id) let contactResult: Contact = Contact( id: contactId, isApproved: contact.approved, isBlocked: contact.blocked, didApproveMe: contact.approved_me ) - let profilePictureUrl: String? = String(libSessionVal: contact.profile_pic.url, nullIfEmpty: true) + let profilePictureUrl: String? = contact.get(\.profile_pic.url, nullIfEmpty: true) let profileResult: Profile = Profile( id: contactId, - name: String(libSessionVal: contact.name), + name: contact.get(\.name), lastNameUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000), - nickname: String(libSessionVal: contact.nickname, nullIfEmpty: true), + nickname: contact.get(\.nickname, nullIfEmpty: true), profilePictureUrl: profilePictureUrl, - profileEncryptionKey: (profilePictureUrl == nil ? nil : - Data( - libSessionVal: contact.profile_pic.key, - count: ProfileManager.avatarAES256KeyByteLength - ) - ), + profileEncryptionKey: (profilePictureUrl == nil ? nil : contact.get(\.profile_pic.key)), lastProfilePictureUpdate: (TimeInterval(latestConfigSentTimestampMs) / 1000) ) let configResult: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration( @@ -728,3 +717,7 @@ private extension LibSession { return result } } + +// MARK: - C Conformance + +extension contacts_contact: CAccessible & CMutable {} diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift index 5f3259e8da..65ec663bcf 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+ConvoInfoVolatile.swift @@ -558,7 +558,7 @@ public extension LibSession { if convo_info_volatile_it_is_1to1(convoIterator, &oneToOne) { result.append( VolatileThreadInfo( - threadId: String(libSessionVal: oneToOne.session_id), + threadId: oneToOne.get(\.session_id), variant: .contact, changes: [ .markedAsUnread(oneToOne.unread), @@ -568,12 +568,9 @@ public extension LibSession { ) } else if convo_info_volatile_it_is_community(convoIterator, &community) { - let server: String = String(libSessionVal: community.base_url) - let roomToken: String = String(libSessionVal: community.room) - let publicKey: String = Data( - libSessionVal: community.pubkey, - count: OpenGroup.pubkeyByteLength - ).toHexString() + let server: String = community.get(\.base_url) + let roomToken: String = community.get(\.room) + let publicKey: String = community.getHex(\.pubkey) result.append( VolatileThreadInfo( @@ -595,7 +592,7 @@ public extension LibSession { else if convo_info_volatile_it_is_legacy_group(convoIterator, &legacyGroup) { result.append( VolatileThreadInfo( - threadId: String(libSessionVal: legacyGroup.group_id), + threadId: legacyGroup.get(\.group_id), variant: .legacyGroup, changes: [ .markedAsUnread(legacyGroup.unread), @@ -640,3 +637,8 @@ fileprivate extension [LibSession.VolatileThreadInfo.Change] { } } +// MARK: - C Conformance + +extension convo_info_volatile_1to1: CAccessible & CMutable {} +extension convo_info_volatile_community: CAccessible & CMutable {} +extension convo_info_volatile_legacy_group: CAccessible & CMutable {} diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift index 16074d6988..e377f5387c 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserGroups.swift @@ -45,8 +45,8 @@ internal extension LibSession { try LibSession.checkLoopLimitReached(&infiniteLoopGuard, for: .userGroups) if user_groups_it_is_community(groupsIterator, &community) { - let server: String = String(libSessionVal: community.base_url) - let roomToken: String = String(libSessionVal: community.room) + let server: String = community.get(\.base_url) + let roomToken: String = community.get(\.room) communities.append( PrioritisedData( @@ -54,33 +54,24 @@ internal extension LibSession { threadId: OpenGroup.idFor(roomToken: roomToken, server: server), server: server, roomToken: roomToken, - publicKey: Data( - libSessionVal: community.pubkey, - count: OpenGroup.pubkeyByteLength - ).toHexString() + publicKey: community.getHex(\.pubkey) ), priority: community.priority ) ) } else if user_groups_it_is_legacy_group(groupsIterator, &legacyGroup) { - let groupId: String = String(libSessionVal: legacyGroup.session_id) + let groupId: String = legacyGroup.get(\.session_id) let members: [String: Bool] = LibSession.memberInfo(in: &legacyGroup) legacyGroups.append( LegacyGroupInfo( id: groupId, - name: String(libSessionVal: legacyGroup.name), + name: legacyGroup.get(\.name), lastKeyPair: ClosedGroupKeyPair( threadId: groupId, - publicKey: Data( - libSessionVal: legacyGroup.enc_pubkey, - count: ClosedGroup.pubkeyByteLength - ), - secretKey: Data( - libSessionVal: legacyGroup.enc_seckey, - count: ClosedGroup.secretKeyByteLength - ), + publicKey: legacyGroup.get(\.enc_pubkey), + secretKey: legacyGroup.get(\.enc_seckey), receivedTimestamp: (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) ), disappearingConfig: DisappearingMessagesConfiguration @@ -431,31 +422,20 @@ internal extension LibSession { // Assign all properties to match the updated group (if there is one) if let updatedName: String = legacyGroup.name { - userGroup.pointee.name = updatedName.toLibSession() - - // Store the updated group (needs to happen before variables go out of scope) - user_groups_set_legacy_group(conf, userGroup) - try LibSessionError.throwIfNeeded(conf) { ugroups_legacy_group_free(userGroup) } + userGroup.set(\.name, to: updatedName) } if let lastKeyPair: ClosedGroupKeyPair = legacyGroup.lastKeyPair { - userGroup.pointee.enc_pubkey = lastKeyPair.publicKey.toLibSession() - userGroup.pointee.enc_seckey = lastKeyPair.secretKey.toLibSession() - userGroup.pointee.have_enc_keys = true - - // Store the updated group (needs to happen before variables go out of scope) - user_groups_set_legacy_group(conf, userGroup) - try LibSessionError.throwIfNeeded(conf) { ugroups_legacy_group_free(userGroup) } + userGroup.set(\.enc_pubkey, to: lastKeyPair.publicKey) + userGroup.set(\.enc_seckey, to: lastKeyPair.secretKey) + userGroup.set(\.have_enc_keys, to: true) } // Assign all properties to match the updated disappearing messages config (if there is one) if let updatedConfig: DisappearingMessagesConfiguration = legacyGroup.disappearingConfig { - userGroup.pointee.disappearing_timer = (!updatedConfig.isEnabled ? 0 : + userGroup.set(\.disappearing_timer, to: (!updatedConfig.isEnabled ? 0 : Int64(floor(updatedConfig.durationSeconds)) - ) - - user_groups_set_legacy_group(conf, userGroup) - try LibSessionError.throwIfNeeded(conf) { ugroups_legacy_group_free(userGroup) } + )) } // Add/Remove the group members and admins @@ -511,11 +491,11 @@ internal extension LibSession { } if let joinedAt: Int64 = legacyGroup.joinedAt { - userGroup.pointee.joined_at = joinedAt + userGroup.set(\.joined_at, to: joinedAt) } // Store the updated group (can't be sure if we made any changes above) - userGroup.pointee.priority = (legacyGroup.priority ?? userGroup.pointee.priority) + userGroup.set(\.priority, to: (legacyGroup.priority ?? userGroup.pointee.priority)) // Note: Need to free the legacy group pointer user_groups_set_free_legacy_group(conf, userGroup) @@ -946,3 +926,8 @@ extension LibSession { let priority: Int32 } } + +// MARK: - C Conformance + +extension ugroups_community_info: CAccessible & CMutable {} +extension ugroups_legacy_group_info: CAccessible & CMutable {} diff --git a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift index fcdd8084fe..17b550a08f 100644 --- a/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift +++ b/SessionMessagingKit/LibSession/Config Handling/LibSession+UserProfile.swift @@ -36,22 +36,19 @@ internal extension LibSession { let userPublicKey: String = getUserHexEncodedPublicKey(db) let profileName: String = String(cString: profileNamePtr) let profilePic: user_profile_pic = user_profile_get_pic(conf) - let profilePictureUrl: String? = String(libSessionVal: profilePic.url, nullIfEmpty: true) + let profilePictureUrl: String? = profilePic.get(\.url, nullIfEmpty: true) // Handle user profile changes try ProfileManager.updateProfileIfNeeded( db, publicKey: userPublicKey, - name: profileName, - avatarUpdate: { - guard let profilePictureUrl: String = profilePictureUrl else { return .remove } + displayNameUpdate: .currentUserUpdate(profileName), + displayPictureUpdate: { + guard let profilePictureUrl: String = profilePictureUrl else { return .currentUserRemove } - return .updateTo( + return .currentUserUpdateTo( url: profilePictureUrl, - key: Data( - libSessionVal: profilePic.key, - count: ProfileManager.avatarAES256KeyByteLength - ), + key: profilePic.get(\.key), fileName: nil ) }(), @@ -184,8 +181,8 @@ internal extension LibSession { // Either assign the updated profile pic, or sent a blank profile pic (to remove the current one) var profilePic: user_profile_pic = user_profile_pic() - profilePic.url = profile.profilePictureUrl.toLibSession() - profilePic.key = profile.profileEncryptionKey.toLibSession() + profilePic.set(\.url, to: profile.profilePictureUrl) + profilePic.set(\.key, to: profile.profileEncryptionKey) user_profile_set_pic(conf, profilePic) try LibSessionError.throwIfNeeded(conf) } @@ -227,3 +224,7 @@ extension LibSession { return user_profile_get_blinded_msgreqs(conf) } } + +// MARK: - C Conformance + +extension user_profile_pic: CAccessible & CMutable {} diff --git a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift index f246300b58..7db2107df7 100644 --- a/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift +++ b/SessionMessagingKit/LibSession/LibSession+SessionMessagingKit.swift @@ -229,70 +229,70 @@ public extension LibSession { /// config while we are reading (which could result in crashes) return try existingDumpVariants .reduce(into: PendingChanges()) { result, variant in - guard - let conf = dependencies.caches[.libSession] - .config(for: variant, publicKey: publicKey) - .wrappedValue - else { return } - - // Check if the config needs to be pushed - guard config_needs_push(conf) else { - // If not then try retrieve any obsolete hashes to be removed - guard let cObsoletePtr: UnsafeMutablePointer = config_old_hashes(conf) else { - return - } - - let obsoleteHashes: [String] = [String]( - pointer: cObsoletePtr.pointee.value, - count: cObsoletePtr.pointee.len, - defaultValue: [] - ) - - // If there are no obsolete hashes then no need to return anything - guard !obsoleteHashes.isEmpty else { return } - - result.append(hashes: obsoleteHashes) - return - } - - guard let cPushData: UnsafeMutablePointer = config_push(conf) else { - let configCountInfo: String = { - switch variant { - case .userProfile: return "1 profile" // stringlint:disable - case .contacts: return "\(contacts_size(conf)) contacts" // stringlint:disable - case .userGroups: return "\(user_groups_size(conf)) group conversations" // stringlint:disable - case .convoInfoVolatile: return "\(convo_info_volatile_size(conf)) volatile conversations" // stringlint:disable - case .invalid: return "Invalid" // stringlint:disable + try dependencies.caches[.libSession] + .config(for: variant, publicKey: publicKey) + .mutate { conf in + guard conf != nil else { return } + + // Check if the config needs to be pushed + guard config_needs_push(conf) else { + // If not then try retrieve any obsolete hashes to be removed + guard let cObsoletePtr: UnsafeMutablePointer = config_old_hashes(conf) else { + return + } + + let obsoleteHashes: [String] = [String]( + pointer: cObsoletePtr.pointee.value, + count: cObsoletePtr.pointee.len, + defaultValue: [] + ) + + // If there are no obsolete hashes then no need to return anything + guard !obsoleteHashes.isEmpty else { return } + + result.append(hashes: obsoleteHashes) + return + } + + guard let cPushData: UnsafeMutablePointer = config_push(conf) else { + let configCountInfo: String = { + switch variant { + case .userProfile: return "1 profile" // stringlint:disable + case .contacts: return "\(contacts_size(conf)) contacts" // stringlint:disable + case .userGroups: return "\(user_groups_size(conf)) group conversations" // stringlint:disable + case .convoInfoVolatile: return "\(convo_info_volatile_size(conf)) volatile conversations" // stringlint:disable + case .invalid: return "Invalid" // stringlint:disable + } + }() + + throw LibSessionError( + conf, + fallbackError: .unableToGeneratePushData, + logMessage: "[LibSession] Failed to generate push data for \(variant) config data, size: \(configCountInfo), error" + ) } - }() - throw LibSessionError( - conf, - fallbackError: .unableToGeneratePushData, - logMessage: "[LibSession] Failed to generate push data for \(variant) config data, size: \(configCountInfo), error" - ) - } - - let pushData: Data = Data( - bytes: cPushData.pointee.config, - count: cPushData.pointee.config_len - ) - let obsoleteHashes: [String] = [String]( - pointer: cPushData.pointee.obsolete, - count: cPushData.pointee.obsolete_len, - defaultValue: [] - ) - let seqNo: Int64 = cPushData.pointee.seqno - cPushData.deallocate() - - result.append( - data: PendingChanges.PushData( - data: pushData, - seqNo: seqNo, - variant: variant - ), - hashes: obsoleteHashes - ) + let pushData: Data = Data( + bytes: cPushData.pointee.config, + count: cPushData.pointee.config_len + ) + let obsoleteHashes: [String] = [String]( + pointer: cPushData.pointee.obsolete, + count: cPushData.pointee.obsolete_len, + defaultValue: [] + ) + let seqNo: Int64 = cPushData.pointee.seqno + cPushData.deallocate() + + result.append( + data: PendingChanges.PushData( + data: pushData, + seqNo: seqNo, + variant: variant + ), + hashes: obsoleteHashes + ) + } } } diff --git a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift index 7a2cb95343..cb7900d333 100644 --- a/SessionMessagingKit/Messages/Control Messages/CallMessage.swift +++ b/SessionMessagingKit/Messages/Control Messages/CallMessage.swift @@ -226,6 +226,7 @@ public extension CallMessage { case outgoing case missed case permissionDenied + case permissionDeniedMicrophone case unknown } @@ -253,7 +254,7 @@ public extension CallMessage { threadContactDisplayName ) - case .missed, .permissionDenied: + case .missed, .permissionDenied, .permissionDeniedMicrophone: return String( format: "call_missed".localized(), threadContactDisplayName diff --git a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift index f58e201952..fa049e79aa 100644 --- a/SessionMessagingKit/Open Groups/OpenGroupAPI.swift +++ b/SessionMessagingKit/Open Groups/OpenGroupAPI.swift @@ -975,6 +975,8 @@ public enum OpenGroupAPI { public static func preparedClearInbox( _ db: Database, on server: String, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return try OpenGroupAPI @@ -986,6 +988,8 @@ public enum OpenGroupAPI { endpoint: .inbox ), responseType: DeleteInboxResponse.self, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .signed(db, with: OpenGroupAPI.signRequest, using: dependencies) @@ -1463,14 +1467,16 @@ public enum OpenGroupAPI { private static func prepareRequest( request: Request, responseType: R.Type, - timeout: TimeInterval = Network.defaultTimeout, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return Network.PreparedRequest( request: request, urlRequest: try request.generateUrlRequest(using: dependencies), responseType: responseType, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift index 798276db3b..127ea94ac2 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+Calls.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import AVFAudio import GRDB import WebRTC import SessionUtilitiesKit @@ -75,8 +76,11 @@ extension MessageReceiver { return } - guard db[.areCallsEnabled] else { - if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: .permissionDenied, using: dependencies) { + let hasMicrophonePermission: Bool = (AVAudioSession.sharedInstance().recordPermission == .granted) + guard db[.areCallsEnabled] && hasMicrophonePermission else { + let state: CallMessage.MessageInfo.State = (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied) + + if let interaction: Interaction = try MessageReceiver.insertCallInfoMessage(db, for: message, state: state, using: dependencies) { let thread: SessionThread = try SessionThread .fetchOrCreate(db, id: sender, variant: .contact, shouldBeVisible: nil) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 79116401b1..9d00e67e56 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -241,8 +241,8 @@ extension MessageReceiver { // Resubscribe for group push notifications let currentUserPublicKey: String = getUserHexEncodedPublicKey(db) - PushNotificationAPI - .subscribeToLegacyGroups( + (try? PushNotificationAPI + .preparedSubscribeToLegacyGroups( currentUserPublicKey: currentUserPublicKey, legacyGroupIds: try ClosedGroup .select(.threadId) @@ -253,8 +253,11 @@ extension MessageReceiver { ) .asRequest(of: String.self) .fetchSet(db) - .inserting(groupPublicKey) // Insert the new key just to be sure - ) + .inserting(groupPublicKey), // Insert the new key just to be sure + using: dependencies + ))? + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) .sinkUntilComplete() } diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift index 7664f03b9b..8d51eddb9c 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+MessageRequests.swift @@ -31,14 +31,14 @@ extension MessageReceiver { try ProfileManager.updateProfileIfNeeded( db, publicKey: senderId, - name: profile.displayName, - avatarUpdate: { + displayNameUpdate: .contactUpdate(profile.displayName), + displayPictureUpdate: { guard let profilePictureUrl: String = profile.profilePictureUrl, let profileKey: Data = profile.profileKey else { return .none } - return .updateTo( + return .contactUpdateTo( url: profilePictureUrl, key: profileKey, fileName: nil diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift index 4e832979ad..74806d3802 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+VisibleMessages.swift @@ -30,20 +30,20 @@ extension MessageReceiver { try ProfileManager.updateProfileIfNeeded( db, publicKey: sender, - name: profile.displayName, - blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, - avatarUpdate: { + displayNameUpdate: .contactUpdate(profile.displayName), + displayPictureUpdate: { guard let profilePictureUrl: String = profile.profilePictureUrl, let profileKey: Data = profile.profileKey - else { return .remove } + else { return .contactRemove } - return .updateTo( + return .contactUpdateTo( url: profilePictureUrl, key: profileKey, fileName: nil ) }(), + blocksCommunityMessageRequests: profile.blocksCommunityMessageRequests, sentTimestamp: messageSentTimestamp, using: dependencies ) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 7eddabef24..7d06da9406 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -7,6 +7,11 @@ import SessionUtilitiesKit import SessionSnodeKit extension MessageSender { + typealias CreateGroupDatabaseResult = ( + SessionThread, + [MessageSender.PreparedSendData], + Network.PreparedRequest? + ) public static var distributingKeyPairs: Atomic<[String: [ClosedGroupKeyPair]]> = Atomic([:]) public static func createClosedGroup( @@ -15,7 +20,7 @@ extension MessageSender { using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { dependencies.storage - .writePublisher { db -> (String, SessionThread, [MessageSender.PreparedSendData], Set) in + .writePublisher { db -> CreateGroupDatabaseResult in // Generate the group's two keys guard let groupKeyPair: KeyPair = dependencies.crypto.generate(.x25519KeyPair()), @@ -108,42 +113,54 @@ extension MessageSender { using: dependencies ) } - let allActiveLegacyGroupIds: Set = try ClosedGroup - .select(.threadId) - .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == userPublicKey) - ) - .asRequest(of: String.self) - .fetchSet(db) - .inserting(groupPublicKey) // Insert the new key just to be sure - return (userPublicKey, thread, memberSendData, allActiveLegacyGroupIds) + // Prepare the notification subscription + var preparedNotificationSubscription: Network.PreparedRequest? + + if let token: String = dependencies.standardUserDefaults[.deviceToken] { + preparedNotificationSubscription = try? PushNotificationAPI + .preparedSubscribeToLegacyGroups( + token: token, + currentUserPublicKey: userPublicKey, + legacyGroupIds: try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == userPublicKey) + ) + .asRequest(of: String.self) + .fetchSet(db) + .inserting(groupPublicKey), // Insert the new key just to be sure, + using: dependencies + ) + } + + return (thread, memberSendData, preparedNotificationSubscription) } - .flatMap { userPublicKey, thread, memberSendData, allActiveLegacyGroupIds in + .flatMap { thread, memberSendData, preparedNotificationSubscription -> AnyPublisher<(SessionThread, Network.PreparedRequest?), Error> in Publishers .MergeMany( // Send a closed group update message to all members individually - memberSendData - .map { MessageSender.sendImmediate(data: $0, using: dependencies) } - .appending( - // Resubscribe to all legacy groups - PushNotificationAPI.subscribeToLegacyGroups( - currentUserPublicKey: userPublicKey, - legacyGroupIds: allActiveLegacyGroupIds - ) - ) + memberSendData.map { MessageSender.sendImmediate(data: $0, using: dependencies) } ) .collect() - .map { _ in thread } + .map { _ in (thread, preparedNotificationSubscription) } + .eraseToAnyPublisher() } .handleEvents( - receiveOutput: { thread in + receiveOutput: { thread, preparedNotificationSubscription in + // Subscribe for push notifications (if PNs are enabled) + preparedNotificationSubscription? + .send(using: dependencies) + .subscribe(on: DispatchQueue.global(qos: .userInitiated), using: dependencies) + .sinkUntilComplete() + // Start polling ClosedGroupPoller.shared.startIfNeeded(for: thread.id, using: dependencies) } ) + .map { thread, _ -> SessionThread in thread } .eraseToAnyPublisher() } diff --git a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift index f342a81041..a8e1c962c5 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageReceiver.swift @@ -21,6 +21,7 @@ public enum MessageReceiver { var customMessage: Message? = nil let sender: String let sentTimestamp: UInt64 + let serverHash: String? let openGroupServerMessageId: UInt64? let threadVariant: SessionThread.Variant let threadIdGenerator: (Message) throws -> String @@ -40,6 +41,7 @@ public enum MessageReceiver { plaintext = data.removePadding() // Remove the padding sender = messageSender sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency + serverHash = nil openGroupServerMessageId = UInt64(messageServerId) threadVariant = .community threadIdGenerator = { message in @@ -50,10 +52,6 @@ public enum MessageReceiver { } case (_, .openGroupInbox(let timestamp, let messageServerId, let serverPublicKey, let senderId, let recipientId)): - guard let userEd25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { - throw MessageReceiverError.noUserED25519KeyPair - } - (plaintext, sender) = try dependencies.crypto.tryGenerate( .plaintextWithSessionBlindingProtocol( db, @@ -67,11 +65,12 @@ public enum MessageReceiver { plaintext = plaintext.removePadding() // Remove the padding sentTimestamp = UInt64(floor(timestamp * 1000)) // Convert to ms for database consistency + serverHash = nil openGroupServerMessageId = UInt64(messageServerId) threadVariant = .contact threadIdGenerator = { _ in sender } - case (_, .swarm(let publicKey, let namespace, _, _, _)): + case (_, .swarm(let publicKey, let namespace, let swarmServerHash, _, _)): switch namespace { case .default: guard @@ -81,9 +80,6 @@ public enum MessageReceiver { SNLog("Failed to unwrap data for message from 'default' namespace.") throw MessageReceiverError.invalidMessage } - guard let userX25519KeyPair: KeyPair = Identity.fetchUserKeyPair(db) else { - throw MessageReceiverError.noUserX25519KeyPair - } (plaintext, sender) = try dependencies.crypto.tryGenerate( .plaintextWithSessionProtocol( @@ -94,6 +90,7 @@ public enum MessageReceiver { ) plaintext = plaintext.removePadding() // Remove the padding sentTimestamp = envelope.timestamp + serverHash = swarmServerHash openGroupServerMessageId = nil threadVariant = .contact threadIdGenerator = { message in @@ -148,6 +145,16 @@ public enum MessageReceiver { (plaintext, sender) = try decrypt(keyPairs: encryptionKeyPairs) plaintext = plaintext.removePadding() // Remove the padding sentTimestamp = envelope.timestamp + + /// If we weren't given a `serverHash` then compute one locally using the same logic the swarm would + switch swarmServerHash.isEmpty { + case false: serverHash = swarmServerHash + case true: + serverHash = dependencies.crypto.generate( + .messageServerHash(swarmPubkey: publicKey, namespace: namespace, data: data) + ).defaulting(to: "") + } + openGroupServerMessageId = nil threadVariant = .legacyGroup threadIdGenerator = { _ in publicKey } @@ -170,7 +177,7 @@ public enum MessageReceiver { let message: Message = try (customMessage ?? Message.createMessageFrom(proto, sender: sender)) message.sender = sender message.recipient = userSessionId - message.serverHash = origin.serverHash + message.serverHash = serverHash message.sentTimestamp = sentTimestamp message.receivedTimestamp = UInt64(SnodeAPI.currentOffsetTimestampMs()) message.openGroupServerMessageId = openGroupServerMessageId diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift index dc8c77ff7b..bd412f24e7 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/Models/LegacyPushServerResponse.swift @@ -2,7 +2,7 @@ import Foundation -extension PushNotificationAPI { +public extension PushNotificationAPI { struct LegacyPushServerResponse: Codable { let code: Int let message: String? diff --git a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift index 54f67ee74c..f097445478 100644 --- a/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift +++ b/SessionMessagingKit/Sending & Receiving/Notifications/PushNotificationAPI.swift @@ -31,6 +31,11 @@ public enum PushNotificationAPI { isForcedUpdate: Bool, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher { + typealias SubscribeAllPreparedRequests = ( + SubscribeRequest, + String, + Network.PreparedRequest? + ) let hexEncodedToken: String = token.toHexString() let oldToken: String? = dependencies.standardUserDefaults[.deviceToken] let lastUploadTime: Double = dependencies.standardUserDefaults[.lastDeviceTokenUpload] @@ -52,7 +57,7 @@ public enum PushNotificationAPI { // TODO: Need to generate requests for each updated group as well return dependencies.storage - .readPublisher(using: dependencies) { db -> (SubscribeRequest, String, Set) in + .readPublisher(using: dependencies) { db -> SubscribeAllPreparedRequests in guard let userED25519KeyPair: KeyPair = Identity.fetchUserEd25519KeyPair(db) else { throw SnodeAPIError.noKeyPair } @@ -74,22 +79,30 @@ public enum PushNotificationAPI { ed25519PublicKey: userED25519KeyPair.publicKey, ed25519SecretKey: userED25519KeyPair.secretKey ) + let preparedLegacyGroupRequest = try PushNotificationAPI + .preparedSubscribeToLegacyGroups( + forced: true, + token: hexEncodedToken, + currentUserPublicKey: currentUserPublicKey, + legacyGroupIds: try ClosedGroup + .select(.threadId) + .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) + .joining( + required: ClosedGroup.members + .filter(GroupMember.Columns.profileId == currentUserPublicKey) + ) + .asRequest(of: String.self) + .fetchSet(db), + using: dependencies + ) return ( request, currentUserPublicKey, - try ClosedGroup - .select(.threadId) - .filter(!ClosedGroup.Columns.threadId.like("\(SessionId.Prefix.group.rawValue)%")) - .joining( - required: ClosedGroup.members - .filter(GroupMember.Columns.profileId == currentUserPublicKey) - ) - .asRequest(of: String.self) - .fetchSet(db) + preparedLegacyGroupRequest ) } - .tryFlatMap { request, currentUserPublicKey, legacyGroupIds -> AnyPublisher in + .tryFlatMap { request, currentUserPublicKey, legacyGroupRequest -> AnyPublisher in Publishers .MergeMany( [ @@ -126,14 +139,12 @@ public enum PushNotificationAPI { .map { _ in () } .eraseToAnyPublisher(), // FIXME: Remove this once legacy groups are deprecated - PushNotificationAPI.subscribeToLegacyGroups( - forced: true, - token: hexEncodedToken, - currentUserPublicKey: currentUserPublicKey, - legacyGroupIds: legacyGroupIds, - using: dependencies - ) + legacyGroupRequest? + .send(using: dependencies) + .map { _, _ in () } + .eraseToAnyPublisher() ] + .compactMap { $0 } ) .collect() .map { _ in () } @@ -281,61 +292,52 @@ public enum PushNotificationAPI { // MARK: - Legacy Groups // FIXME: Remove this once legacy groups are deprecated - public static func subscribeToLegacyGroups( + public static func preparedSubscribeToLegacyGroups( forced: Bool = false, token: String? = nil, currentUserPublicKey: String, legacyGroupIds: Set, - using dependencies: Dependencies = Dependencies() - ) -> AnyPublisher { + using dependencies: Dependencies + ) throws -> Network.PreparedRequest? { let isUsingFullAPNs = dependencies.standardUserDefaults[.isUsingFullAPNs] // Only continue if PNs are enabled and we have a device token guard + !legacyGroupIds.isEmpty, (forced || isUsingFullAPNs), let deviceToken: String = (token ?? dependencies.standardUserDefaults[.deviceToken]) - else { - return Just(()) - .setFailureType(to: Error.self) - .eraseToAnyPublisher() - } + else { return nil } - do { - return try PushNotificationAPI - .prepareRequest( - request: Request( - method: .post, - endpoint: .legacyGroupsOnlySubscribe, - body: LegacyGroupOnlyRequest( - token: deviceToken, - pubKey: currentUserPublicKey, - device: "ios", - legacyGroupPublicKeys: legacyGroupIds - ), - using: dependencies + return try PushNotificationAPI + .prepareRequest( + request: Request( + method: .post, + endpoint: .legacyGroupsOnlySubscribe, + body: LegacyGroupOnlyRequest( + token: deviceToken, + pubKey: currentUserPublicKey, + device: "ios", + legacyGroupPublicKeys: legacyGroupIds ), - responseType: LegacyPushServerResponse.self, using: dependencies - ) - .send(using: dependencies) - .retry(maxRetryCount, using: dependencies) - .handleEvents( - receiveOutput: { _, response in - guard response.code != 0 else { - return SNLog("Couldn't subscribe for legacy groups due to error: \(response.message ?? "nil").") - } - }, - receiveCompletion: { result in - switch result { - case .finished: break - case .failure: SNLog("Couldn't subscribe for legacy groups.") - } + ), + responseType: LegacyPushServerResponse.self, + retryCount: PushNotificationAPI.maxRetryCount, + using: dependencies + ) + .handleEvents( + receiveOutput: { _, response in + guard response.code != 0 else { + return Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups due to error: \(response.message ?? "nil").") } - ) - .map { _ in () } - .eraseToAnyPublisher() - } - catch { return Fail(error: error).eraseToAnyPublisher() } + }, + receiveCompletion: { result in + switch result { + case .finished: break + case .failure: Log.error("[PushNotificationAPI] Couldn't subscribe for legacy groups.") + } + } + ) } // FIXME: Remove this once legacy groups are deprecated @@ -494,7 +496,8 @@ public enum PushNotificationAPI { request: Request, responseType: R.Type, retryCount: Int = 0, - timeout: TimeInterval = Network.defaultTimeout, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return Network.PreparedRequest( @@ -502,7 +505,8 @@ public enum PushNotificationAPI { urlRequest: try request.generateUrlRequest(using: dependencies), responseType: responseType, retryCount: retryCount, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ) } } diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift index d6e2caa317..c59049f64f 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/ClosedGroupPoller.swift @@ -30,7 +30,7 @@ public final class ClosedGroupPoller: Poller { // Fetch all closed groups (excluding any don't contain the current user as a // GroupMemeber as the user is no longer a member of those) dependencies.storage - .read { db in + .read { db -> Set in try ClosedGroup .select(.threadId) .joining( @@ -38,7 +38,7 @@ public final class ClosedGroupPoller: Poller { .filter(GroupMember.Columns.profileId == getUserHexEncodedPublicKey(db, using: dependencies)) ) .asRequest(of: String.self) - .fetchAll(db) + .fetchSet(db) } .defaulting(to: []) .forEach { [weak self] publicKey in diff --git a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift index 167b95649d..205df13aff 100644 --- a/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift +++ b/SessionMessagingKit/Sending & Receiving/Pollers/OpenGroupPoller.swift @@ -253,14 +253,14 @@ extension OpenGroupAPI { .handleEvents( receiveOutput: { pollFailureCount, prunedIds in let pollEndTime: TimeInterval = dependencies.dateNow.timeIntervalSince1970 - Log.info("Open group polling to \(server) failed in \(.seconds(pollEndTime - pollStartTime), unit: .s) due to error: \(error). Setting failure count to \(pollFailureCount + 1).") + Log.error("Open group polling to \(server) failed in \(.seconds(pollEndTime - pollStartTime), unit: .s) due to error: \(error). Setting failure count to \(pollFailureCount + 1).") // Add a note to the logs that this happened if !prunedIds.isEmpty { let rooms: String = prunedIds .compactMap { $0.components(separatedBy: server).last } .joined(separator: ", ") - Log.info("Hidden open group failure count surpassed \(Poller.maxHiddenRoomFailureCount), removed hidden rooms \(rooms).") + Log.error("Hidden open group failure count surpassed \(Poller.maxHiddenRoomFailureCount), removed hidden rooms \(rooms).") } } ) diff --git a/SessionMessagingKit/Utilities/ProfileManager.swift b/SessionMessagingKit/Utilities/ProfileManager.swift index f492c85004..9261690d3e 100644 --- a/SessionMessagingKit/Utilities/ProfileManager.swift +++ b/SessionMessagingKit/Utilities/ProfileManager.swift @@ -7,11 +7,21 @@ import GRDB import SessionUtilitiesKit public struct ProfileManager { - public enum AvatarUpdate { + public enum DisplayNameUpdate { case none - case remove - case uploadImageData(Data) - case updateTo(url: String, key: Data, fileName: String?) + case contactUpdate(String?) + case currentUserUpdate(String?) + } + + public enum DisplayPictureUpdate { + case none + + case contactRemove + case contactUpdateTo(url: String, key: Data, fileName: String?) + + case currentUserRemove + case currentUserUploadImageData(Data) + case currentUserUpdateTo(url: String, key: Data, fileName: String?) } // The max bytes for a user's profile name, encoded in UTF8. @@ -281,24 +291,27 @@ public struct ProfileManager { public static func updateLocal( queue: DispatchQueue, - profileName: String, - avatarUpdate: AvatarUpdate = .none, + displayNameUpdate: DisplayNameUpdate = .none, + displayPictureUpdate: DisplayPictureUpdate = .none, success: ((Database) throws -> ())? = nil, failure: ((ProfileManagerError) -> ())? = nil, using dependencies: Dependencies = Dependencies() ) { let userPublicKey: String = getUserHexEncodedPublicKey(using: dependencies) - let isRemovingAvatar: Bool = { - switch avatarUpdate { - case .remove: return true + let isRemovingDisplayPicture: Bool = { + switch displayPictureUpdate { + case .currentUserRemove: return true default: return false } }() - switch avatarUpdate { - case .none, .remove, .updateTo: + switch displayPictureUpdate { + case .contactRemove, .contactUpdateTo: + failure?(ProfileManagerError.invalidCall) + + case .none, .currentUserRemove, .currentUserUpdateTo: dependencies.storage.writeAsync { db in - if isRemovingAvatar { + if isRemovingDisplayPicture { let existingProfileUrl: String? = try Profile .filter(id: userPublicKey) .select(.profilePictureUrl) @@ -324,8 +337,8 @@ public struct ProfileManager { try ProfileManager.updateProfileIfNeeded( db, publicKey: userPublicKey, - name: profileName, - avatarUpdate: avatarUpdate, + displayNameUpdate: displayNameUpdate, + displayPictureUpdate: displayPictureUpdate, sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -334,7 +347,7 @@ public struct ProfileManager { try success?(db) } - case .uploadImageData(let data): + case .currentUserUploadImageData(let data): prepareAndUploadAvatarImage( queue: queue, imageData: data, @@ -343,8 +356,8 @@ public struct ProfileManager { try ProfileManager.updateProfileIfNeeded( db, publicKey: userPublicKey, - name: profileName, - avatarUpdate: .updateTo(url: downloadUrl, key: newProfileKey, fileName: fileName), + displayNameUpdate: displayNameUpdate, + displayPictureUpdate: .currentUserUpdateTo(url: downloadUrl, key: newProfileKey, fileName: fileName), sentTimestamp: dependencies.dateNow.timeIntervalSince1970, using: dependencies ) @@ -494,9 +507,9 @@ public struct ProfileManager { public static func updateProfileIfNeeded( _ db: Database, publicKey: String, - name: String?, + displayNameUpdate: DisplayNameUpdate = .none, + displayPictureUpdate: DisplayPictureUpdate, blocksCommunityMessageRequests: Bool? = nil, - avatarUpdate: AvatarUpdate, sentTimestamp: TimeInterval, calledFromConfigHandling: Bool = false, using dependencies: Dependencies @@ -506,15 +519,21 @@ public struct ProfileManager { var profileChanges: [ConfigColumnAssignment] = [] // Name - if let name: String = name, !name.isEmpty, name != profile.name { - if sentTimestamp > (profile.lastNameUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { + // FIXME: This 'lastNameUpdate' approach is buggy - we should have a timestamp on the ConvoInfoVolatile + switch (displayNameUpdate, isCurrentUser, (sentTimestamp > (profile.lastNameUpdate ?? 0))) { + case (.none, _, _): break + case (.currentUserUpdate(let name), true, _), (.contactUpdate(let name), false, true): + guard let name: String = name, !name.isEmpty, name != profile.name else { break } + profileChanges.append(Profile.Columns.name.set(to: name)) profileChanges.append(Profile.Columns.lastNameUpdate.set(to: sentTimestamp)) - } + + // Don't want profiles in messages to modify the current users profile info so ignore those cases + default: break } - // Blocks community message requets flag - if let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > (profile.lastBlocksCommunityMessageRequests ?? 0) { + // Blocks community message requets flag (only update for other users) + if !isCurrentUser, let blocksCommunityMessageRequests: Bool = blocksCommunityMessageRequests, sentTimestamp > (profile.lastBlocksCommunityMessageRequests ?? 0) { profileChanges.append(Profile.Columns.blocksCommunityMessageRequests.set(to: blocksCommunityMessageRequests)) profileChanges.append(Profile.Columns.lastBlocksCommunityMessageRequests.set(to: sentTimestamp)) } @@ -522,43 +541,49 @@ public struct ProfileManager { // Profile picture & profile key var avatarNeedsDownload: Bool = false var targetAvatarUrl: String? = nil + let shouldUpdateAvatar: Bool = ( + (!isCurrentUser && (sentTimestamp > (profile.lastProfilePictureUpdate ?? 0))) || // Update other users + (isCurrentUser && calledFromConfigHandling) // Only update the current user via config messages + ) - if sentTimestamp > (profile.lastProfilePictureUpdate ?? 0) || (isCurrentUser && calledFromConfigHandling) { - switch avatarUpdate { - case .none: break - case .uploadImageData: preconditionFailure("Invalid options for this function") - - case .remove: - profileChanges.append(Profile.Columns.profilePictureUrl.set(to: nil)) - profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: nil)) - profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil)) - profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) - - case .updateTo(let url, let key, let fileName): - if url != profile.profilePictureUrl { - profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url)) - avatarNeedsDownload = true - targetAvatarUrl = url - } - - if key != profile.profileEncryptionKey && key.count == ProfileManager.avatarAES256KeyByteLength { - profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key)) - } - - // Profile filename (this isn't synchronized between devices) - if let fileName: String = fileName { - profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName)) - - // If we have already downloaded the image then no need to download it again - avatarNeedsDownload = ( - avatarNeedsDownload && - !ProfileManager.hasProfileImageData(with: fileName) - ) - } + switch (displayPictureUpdate, isCurrentUser) { + case (.none, _): break + case (.currentUserUploadImageData, _): preconditionFailure("Invalid options for this function") + + case (.contactRemove, false), (.currentUserRemove, true): + profileChanges.append(Profile.Columns.profilePictureUrl.set(to: nil)) + profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: nil)) + profileChanges.append(Profile.Columns.profilePictureFileName.set(to: nil)) + profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) + + case (.contactUpdateTo(let url, let key, let fileName), false), + (.currentUserUpdateTo(let url, let key, let fileName), true): + if url != profile.profilePictureUrl { + profileChanges.append(Profile.Columns.profilePictureUrl.set(to: url)) + avatarNeedsDownload = true + targetAvatarUrl = url + } + + if key != profile.profileEncryptionKey && key.count == ProfileManager.avatarAES256KeyByteLength { + profileChanges.append(Profile.Columns.profileEncryptionKey.set(to: key)) + } + + // Profile filename (this isn't synchronized between devices) + if let fileName: String = fileName { + profileChanges.append(Profile.Columns.profilePictureFileName.set(to: fileName)) - // Update the 'lastProfilePictureUpdate' timestamp for either external or local changes - profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) - } + // If we have already downloaded the image then no need to download it again + avatarNeedsDownload = ( + avatarNeedsDownload && + !ProfileManager.hasProfileImageData(with: fileName) + ) + } + + // Update the 'lastProfilePictureUpdate' timestamp for either external or local changes + profileChanges.append(Profile.Columns.lastProfilePictureUpdate.set(to: sentTimestamp)) + + // Don't want profiles in messages to modify the current users profile info so ignore those cases + default: break } // Persist any changes diff --git a/SessionMessagingKit/Utilities/ProfileManagerError.swift b/SessionMessagingKit/Utilities/ProfileManagerError.swift index a522e492ec..d709cfac90 100644 --- a/SessionMessagingKit/Utilities/ProfileManagerError.swift +++ b/SessionMessagingKit/Utilities/ProfileManagerError.swift @@ -1,4 +1,6 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation @@ -17,7 +19,7 @@ public enum ProfileManagerError: LocalizedError { case .avatarEncryptionFailed: return "Avatar encryption failed." case .avatarUploadFailed: return "Avatar upload failed." case .avatarUploadMaxFileSizeExceeded: return "Maximum file size exceeded." - case .invalidCall: return "Attempted to remove avatar using the wrong method." + case .invalidCall: return "Attempted to modify profile using the wrong method." } } } diff --git a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift index 9a05d2b103..404fc7bf34 100644 --- a/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift +++ b/SessionMessagingKitTests/Jobs/Types/MessageSendJobSpec.swift @@ -9,6 +9,10 @@ import Nimble @testable import SessionMessagingKit @testable import SessionUtilitiesKit +extension Job: MutableIdentifiable { + public mutating func setId(_ id: Int64?) { self.id = id } +} + class MessageSendJobSpec: QuickSpec { override class func spec() { // MARK: Configuration diff --git a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift index ee3ed687f7..c8f8b2584b 100644 --- a/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift +++ b/SessionMessagingKitTests/LibSession/LibSessionUtilSpec.swift @@ -325,13 +325,13 @@ fileprivate extension LibSessionUtilSpec { var contact2: contacts_contact = contacts_contact() expect(contacts_get_or_construct(conf, &contact2, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact2.name)).to(beEmpty()) - expect(String(libSessionVal: contact2.nickname)).to(beEmpty()) + expect(contact2.get(\.name, nullIfEmpty: false)).to(beEmpty()) + expect(contact2.get(\.nickname, nullIfEmpty: false)).to(beEmpty()) expect(contact2.approved).to(beFalse()) expect(contact2.approved_me).to(beFalse()) expect(contact2.blocked).to(beFalse()) expect(contact2.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact2.profile_pic.url)).to(beEmpty()) + expect(contact2.get(\.profile_pic.url, nullIfEmpty: false)).to(beEmpty()) expect(contact2.created).to(equal(0)) expect(contact2.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) expect(contact2.mute_until).to(equal(0)) @@ -344,8 +344,8 @@ fileprivate extension LibSessionUtilSpec { pushData1.deallocate() // Update the contact data - contact2.name = "Joe".toLibSession() - contact2.nickname = "Joey".toLibSession() + contact2.set(\.name, to: "Joe") + contact2.set(\.nickname, to: "Joey") contact2.approved = true contact2.approved_me = true contact2.created = createdTs @@ -358,17 +358,17 @@ fileprivate extension LibSessionUtilSpec { // Ensure the contact details were updated var contact3: contacts_contact = contacts_contact() expect(contacts_get(conf, &contact3, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact3.name)).to(equal("Joe")) - expect(String(libSessionVal: contact3.nickname)).to(equal("Joey")) + expect(contact3.get(\.name, nullIfEmpty: false)).to(equal("Joe")) + expect(contact3.get(\.nickname, nullIfEmpty: false)).to(equal("Joey")) expect(contact3.approved).to(beTrue()) expect(contact3.approved_me).to(beTrue()) expect(contact3.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact3.profile_pic.url)).to(beEmpty()) + expect(contact3.get(\.profile_pic.url, nullIfEmpty: false)).to(beEmpty()) expect(contact3.blocked).to(beFalse()) - expect(String(libSessionVal: contact3.session_id)).to(equal(definitelyRealId)) + expect(contact3.get(\.session_id, nullIfEmpty: false)).to(equal(definitelyRealId)) expect(contact3.created).to(equal(createdTs)) expect(contact2.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(contact2.mute_until).to(equal(nowTs + 1800)) + expect(contact2.mute_until).to(equal(Int64(nowTs + 1800))) // Since we've made changes, we should need to push new config to the swarm, *and* should need @@ -417,12 +417,12 @@ fileprivate extension LibSessionUtilSpec { // Ensure the contact details were updated var contact4: contacts_contact = contacts_contact() expect(contacts_get(conf2, &contact4, &cDefinitelyRealId)).to(beTrue()) - expect(String(libSessionVal: contact4.name)).to(equal("Joe")) - expect(String(libSessionVal: contact4.nickname)).to(equal("Joey")) + expect(contact4.get(\.name, nullIfEmpty: false)).to(equal("Joe")) + expect(contact4.get(\.nickname, nullIfEmpty: false)).to(equal("Joey")) expect(contact4.approved).to(beTrue()) expect(contact4.approved_me).to(beTrue()) expect(contact4.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact4.profile_pic.url)).to(beEmpty()) + expect(contact4.get(\.profile_pic.url, nullIfEmpty: false)).to(beEmpty()) expect(contact4.blocked).to(beFalse()) expect(contact4.created).to(equal(createdTs)) @@ -430,12 +430,12 @@ fileprivate extension LibSessionUtilSpec { var cAnotherId: [CChar] = anotherId.cString(using: .utf8)! var contact5: contacts_contact = contacts_contact() expect(contacts_get_or_construct(conf2, &contact5, &cAnotherId)).to(beTrue()) - expect(String(libSessionVal: contact5.name)).to(beEmpty()) - expect(String(libSessionVal: contact5.nickname)).to(beEmpty()) + expect(contact5.get(\.name, nullIfEmpty: false)).to(beEmpty()) + expect(contact5.get(\.nickname, nullIfEmpty: false)).to(beEmpty()) expect(contact5.approved).to(beFalse()) expect(contact5.approved_me).to(beFalse()) expect(contact5.profile_pic).toNot(beNil()) // Creates an empty instance apparently - expect(String(libSessionVal: contact5.profile_pic.url)).to(beEmpty()) + expect(contact5.get(\.profile_pic.url, nullIfEmpty: false)).to(beEmpty()) expect(contact5.blocked).to(beFalse()) // We're not setting any fields, but we should still keep a record of the session id @@ -473,8 +473,8 @@ fileprivate extension LibSessionUtilSpec { var contact6: contacts_contact = contacts_contact() let contactIterator: UnsafeMutablePointer = contacts_iterator_new(conf) while !contacts_iterator_done(contactIterator, &contact6) { - sessionIds.append(String(libSessionVal: contact6.session_id)) - nicknames.append(String(libSessionVal: contact6.nickname, nullIfEmpty: true) ?? "(N/A)") + sessionIds.append(contact6.get(\.session_id)) + nicknames.append(contact6.get(\.nickname, nullIfEmpty: true) ?? "(N/A)") contacts_iterator_advance(contactIterator) } contacts_iterator_free(contactIterator) // Need to free the iterator @@ -496,11 +496,11 @@ fileprivate extension LibSessionUtilSpec { var cThirdId: [CChar] = thirdId.cString(using: .utf8)! var contact7: contacts_contact = contacts_contact() expect(contacts_get_or_construct(conf2, &contact7, &cThirdId)).to(beTrue()) - contact7.nickname = "Nickname 3".toLibSession() + contact7.set(\.nickname, to: "Nickname 3") contact7.approved = true contact7.approved_me = true - contact7.profile_pic.url = "http://example.com/huge.bmp".toLibSession() - contact7.profile_pic.key = "qwerty78901234567890123456789012".data(using: .utf8)!.toLibSession() + contact7.set(\.profile_pic.url, to: "http://example.com/huge.bmp") + contact7.set(\.profile_pic.key, to: "qwerty78901234567890123456789012".data(using: .utf8)!) contacts_set(conf2, &contact7) expect(config_needs_push(conf)).to(beTrue()) @@ -581,8 +581,8 @@ fileprivate extension LibSessionUtilSpec { var contact8: contacts_contact = contacts_contact() let contactIterator2: UnsafeMutablePointer = contacts_iterator_new(conf) while !contacts_iterator_done(contactIterator2, &contact8) { - sessionIds2.append(String(libSessionVal: contact8.session_id)) - nicknames2.append(String(libSessionVal: contact8.nickname, nullIfEmpty: true) ?? "(N/A)") + sessionIds2.append(contact8.get(\.session_id)) + nicknames2.append(contact8.get(\.nickname, nullIfEmpty: true) ?? "(N/A)") contacts_iterator_advance(contactIterator2) } contacts_iterator_free(contactIterator2) // Need to free the iterator @@ -627,23 +627,17 @@ fileprivate extension LibSessionUtilSpec { case .exp_seconds: contact.exp_seconds = Int32.max case .name: - contact.name = rand.nextBytes(count: LibSession.libSessionMaxNameByteLength) - .toHexString() - .toLibSession() + contact.set(\.name, to: rand.nextBytes(count: LibSession.libSessionMaxNameByteLength).toHexString()) case .nickname: - contact.nickname = rand.nextBytes(count: LibSession.libSessionMaxNameByteLength) - .toHexString() - .toLibSession() + contact.set(\.nickname, to: rand.nextBytes(count: LibSession.libSessionMaxNameByteLength).toHexString()) case .profile_pic: - contact.profile_pic = user_profile_pic( - url: rand.nextBytes(count: LibSession.libSessionMaxProfileUrlByteLength) - .toHexString() - .toLibSession(), - key: Data(rand.nextBytes(count: 32)) - .toLibSession() + contact.set( + \.profile_pic.url, + to: rand.nextBytes(count: LibSession.libSessionMaxProfileUrlByteLength).toHexString() ) + contact.set(\.profile_pic.key, to: rand.nextBytes(count: 32)) } } @@ -697,14 +691,13 @@ fileprivate extension LibSessionUtilSpec { // This should also be unset: let pic: user_profile_pic = user_profile_get_pic(conf) - expect(String(libSessionVal: pic.url)).to(beEmpty()) + expect(pic.get(\.url, nullIfEmpty: false)).to(beEmpty()) // Now let's go set a profile name and picture: expect(user_profile_set_name(conf, "Kallie")).to(equal(0)) - let p: user_profile_pic = user_profile_pic( - url: "http://example.org/omg-pic-123.bmp".toLibSession(), - key: "secret78901234567890123456789012".data(using: .utf8)!.toLibSession() - ) + var p: user_profile_pic = user_profile_pic() + p.set(\.url, to: "http://example.org/omg-pic-123.bmp") + p.set(\.key, to: "secret78901234567890123456789012".data(using: .utf8)!) expect(user_profile_set_pic(conf, p)).to(equal(0)) user_profile_set_nts_priority(conf, 9) @@ -714,9 +707,8 @@ fileprivate extension LibSessionUtilSpec { expect(String(cString: namePtr2!)).to(equal("Kallie")) let pic2: user_profile_pic = user_profile_get_pic(conf); - expect(String(libSessionVal: pic2.url)).to(equal("http://example.org/omg-pic-123.bmp")) - expect(Data(libSessionVal: pic2.key, count: ProfileManager.avatarAES256KeyByteLength)) - .to(equal("secret78901234567890123456789012".data(using: .utf8))) + expect(pic2.get(\.url, nullIfEmpty: false)).to(equal("http://example.org/omg-pic-123.bmp")) + expect(pic2.get(\.key, nullIfEmpty: false)).to(equal("secret78901234567890123456789012".data(using: .utf8))) expect(user_profile_get_nts_priority(conf)).to(equal(9)) // Since we've made changes, we should need to push new config to the swarm, *and* should need @@ -810,10 +802,9 @@ fileprivate extension LibSessionUtilSpec { user_profile_set_name(conf2, "Raz") // And, on conf2, we're also going to change the profile pic: - let p2: user_profile_pic = user_profile_pic( - url: "http://new.example.com/pic".toLibSession(), - key: "qwert\0yuio1234567890123456789012".data(using: .utf8)!.toLibSession() - ) + var p2: user_profile_pic = user_profile_pic() + p2.set(\.url, to: "http://new.example.com/pic") + p2.set(\.key, to: "qwert\0yuio1234567890123456789012".data(using: .utf8)!) user_profile_set_pic(conf2, p2) user_profile_set_nts_expiry(conf2, 86400) @@ -902,16 +893,12 @@ fileprivate extension LibSessionUtilSpec { // Since only one of them set a profile pic there should be no conflict there: let pic3: user_profile_pic = user_profile_get_pic(conf) - expect(pic3.url).toNot(beNil()) - expect(String(libSessionVal: pic3.url)).to(equal("http://new.example.com/pic")) - expect(pic3.key).toNot(beNil()) - expect(Data(libSessionVal: pic3.key, count: 32).toHexString()) + expect(pic3.get(\.url, nullIfEmpty: true)).to(equal("http://new.example.com/pic")) + expect(pic3.getHex(\.key, nullIfEmpty: true)) .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) let pic4: user_profile_pic = user_profile_get_pic(conf2) - expect(pic4.url).toNot(beNil()) - expect(String(libSessionVal: pic4.url)).to(equal("http://new.example.com/pic")) - expect(pic4.key).toNot(beNil()) - expect(Data(libSessionVal: pic4.key, count: 32).toHexString()) + expect(pic4.get(\.url, nullIfEmpty: true)).to(equal("http://new.example.com/pic")) + expect(pic4.getHex(\.key, nullIfEmpty: true)) .to(equal("7177657274007975696f31323334353637383930313233343536373839303132")) expect(user_profile_get_nts_priority(conf)).to(equal(9)) expect(user_profile_get_nts_priority(conf2)).to(equal(9)) @@ -987,7 +974,7 @@ fileprivate extension LibSessionUtilSpec { var oneToOne2: convo_info_volatile_1to1 = convo_info_volatile_1to1() expect(convo_info_volatile_get_or_construct_1to1(conf, &oneToOne2, &cDefinitelyRealId)) .to(beTrue()) - expect(String(libSessionVal: oneToOne2.session_id)).to(equal(definitelyRealId)) + expect(oneToOne2.get(\.session_id, nullIfEmpty: false)).to(equal(definitelyRealId)) expect(oneToOne2.last_read).to(equal(0)) expect(oneToOne2.unread).to(beFalse()) @@ -1032,9 +1019,10 @@ fileprivate extension LibSessionUtilSpec { .bytes var community1: convo_info_volatile_community = convo_info_volatile_community() expect(convo_info_volatile_get_or_construct_community(conf, &community1, &cOpenGroupBaseUrl, &cOpenGroupRoom, &cOpenGroupPubkey)).to(beTrue()) - expect(String(libSessionVal: community1.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community1.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + + expect(community1.get(\.base_url, nullIfEmpty: false)).to(equal(openGroupBaseUrlResult)) + expect(community1.get(\.room, nullIfEmpty: false)).to(equal(openGroupRoomResult)) + expect(community1.getHex(\.pubkey, nullIfEmpty: false)) .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) community1.unread = true @@ -1070,14 +1058,14 @@ fileprivate extension LibSessionUtilSpec { var oneToOne4: convo_info_volatile_1to1 = convo_info_volatile_1to1() expect(convo_info_volatile_get_1to1(conf2, &oneToOne4, &cDefinitelyRealId)).to(equal(true)) expect(oneToOne4.last_read).to(equal(nowTimestampMs)) - expect(String(libSessionVal: oneToOne4.session_id)).to(equal(definitelyRealId)) + expect(oneToOne4.get(\.session_id, nullIfEmpty: false)).to(equal(definitelyRealId)) expect(oneToOne4.unread).to(beFalse()) var community2: convo_info_volatile_community = convo_info_volatile_community() expect(convo_info_volatile_get_community(conf2, &community2, &cOpenGroupBaseUrl, &cOpenGroupRoom)).to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal(openGroupBaseUrlResult)) - expect(String(libSessionVal: community2.room)).to(equal(openGroupRoomResult)) - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + expect(community2.get(\.base_url, nullIfEmpty: false)).to(equal(openGroupBaseUrlResult)) + expect(community2.get(\.room, nullIfEmpty: false)).to(equal(openGroupRoomResult)) + expect(community2.getHex(\.pubkey, nullIfEmpty: false)) .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) community2.unread = true @@ -1130,13 +1118,13 @@ fileprivate extension LibSessionUtilSpec { while !convo_info_volatile_iterator_done(it) { if convo_info_volatile_it_is_1to1(it, &c1) { - seen.append("1-to-1: \(String(libSessionVal: c1.session_id))") + seen.append("1-to-1: \(c1.get(\.session_id))") } else if convo_info_volatile_it_is_community(it, &c2) { - seen.append("og: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + seen.append("og: \(c2.get(\.base_url))/r/\(c2.get(\.room))") } else if convo_info_volatile_it_is_legacy_group(it, &c3) { - seen.append("cl: \(String(libSessionVal: c3.group_id))") + seen.append("cl: \(c3.get(\.group_id))") } convo_info_volatile_iterator_advance(it) @@ -1170,7 +1158,7 @@ fileprivate extension LibSessionUtilSpec { while !convo_info_volatile_iterator_done(it1) { expect(convo_info_volatile_it_is_1to1(it1, &c1)).to(beTrue()) - seen1.append(String(libSessionVal: c1.session_id)) + seen1.append(c1.get(\.session_id, nullIfEmpty: false)) convo_info_volatile_iterator_advance(it1) } @@ -1186,7 +1174,7 @@ fileprivate extension LibSessionUtilSpec { while !convo_info_volatile_iterator_done(it2) { expect(convo_info_volatile_it_is_community(it2, &c2)).to(beTrue()) - seen2.append(String(libSessionVal: c2.base_url)) + seen2.append(c2.get(\.base_url, nullIfEmpty: false)) convo_info_volatile_iterator_advance(it2) } @@ -1202,7 +1190,7 @@ fileprivate extension LibSessionUtilSpec { while !convo_info_volatile_iterator_done(it3) { expect(convo_info_volatile_it_is_legacy_group(it3, &c3)).to(beTrue()) - seen3.append(String(libSessionVal: c3.group_id)) + seen3.append(c3.get(\.group_id, nullIfEmpty: false)) convo_info_volatile_iterator_advance(it3) } @@ -1250,13 +1238,12 @@ fileprivate extension LibSessionUtilSpec { let legacyGroup2: UnsafeMutablePointer = user_groups_get_or_construct_legacy_group(conf, &cDefinitelyRealId) expect(legacyGroup2.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup2.pointee.session_id)) - .to(equal(definitelyRealId)) + expect(legacyGroup2.get(\.session_id, nullIfEmpty: false)).to(equal(definitelyRealId)) expect(legacyGroup2.pointee.disappearing_timer).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup2.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup2.getHex(\.enc_pubkey, nullIfEmpty: true)).to(beNil()) + expect(legacyGroup2.getHex(\.enc_seckey, nullIfEmpty: true)).to(beNil()) expect(legacyGroup2.pointee.priority).to(equal(0)) - expect(String(libSessionVal: legacyGroup2.pointee.name)).to(equal("")) + expect(legacyGroup2.get(\.name, nullIfEmpty: false)).to(equal("")) expect(legacyGroup2.pointee.joined_at).to(equal(0)) expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_DEFAULT)) expect(legacyGroup2.pointee.mute_until).to(equal(0)) @@ -1298,7 +1285,7 @@ fileprivate extension LibSessionUtilSpec { "056666666666666666666666666666666666666666666666666666666666666666" ] var cUsers: [[CChar]] = users.map { $0.cString(using: .utf8)! } - legacyGroup2.pointee.name = "Englishmen".toLibSession() + legacyGroup2.set(\.name, to: "Englishmen") legacyGroup2.pointee.disappearing_timer = 60 legacyGroup2.pointee.joined_at = createdTs legacyGroup2.pointee.notifications = CONVO_NOTIFY_ALL @@ -1341,13 +1328,13 @@ fileprivate extension LibSessionUtilSpec { // Note: this isn't exactly what Session actually does here for legacy closed // groups (rather it uses X25519 keys) but for this test the distinction doesn't matter. - legacyGroup2.pointee.enc_pubkey = Data(groupX25519PublicKey).toLibSession() - legacyGroup2.pointee.enc_seckey = Data(groupEd25519KeyPair.secretKey).toLibSession() + legacyGroup2.set(\.enc_pubkey, to: groupX25519PublicKey) + legacyGroup2.set(\.enc_seckey, to: groupEd25519KeyPair.secretKey) legacyGroup2.pointee.priority = 3 - expect(Data(libSessionVal: legacyGroup2.pointee.enc_pubkey, count: 32).toHexString()) + expect(legacyGroup2.getHex(\.enc_pubkey, nullIfEmpty: false)) .to(equal("c5ba413c336f2fe1fb9a2c525f8a86a412a1db128a7841b4e0e217fa9eb7fd5e")) - expect(Data(libSessionVal: legacyGroup2.pointee.enc_seckey, count: 32).toHexString()) + expect(legacyGroup2.getHex(\.enc_seckey, nullIfEmpty: false)) .to(equal("00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff")) // The new data doesn't get stored until we call this: @@ -1367,9 +1354,9 @@ fileprivate extension LibSessionUtilSpec { expect(user_groups_get_or_construct_community(conf, &community1, &cCommunityBaseUrl, &cCommunityRoom, &cCommunityPubkey)) .to(beTrue()) - expect(String(libSessionVal: community1.base_url)).to(equal("http://example.org:5678")) // Note: lower-case - expect(String(libSessionVal: community1.room)).to(equal("SudokuRoom")) // Note: case-preserving - expect(Data(libSessionVal: community1.pubkey, count: 32).toHexString()) + expect(community1.get(\.base_url, nullIfEmpty: false)).to(equal("http://example.org:5678")) // Note: lower-case + expect(community1.get(\.room, nullIfEmpty: false)).to(equal("SudokuRoom")) // Note: case-preserving + expect(community1.getHex(\.pubkey, nullIfEmpty: false)) .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) community1.priority = 14 @@ -1435,15 +1422,15 @@ fileprivate extension LibSessionUtilSpec { let legacyGroup4: UnsafeMutablePointer? = user_groups_get_legacy_group(conf2, &cDefinitelyRealId) expect(legacyGroup4?.pointee).toNot(beNil()) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_pubkey, fixedLength: 32)).to(equal("")) - expect(String(libSessionVal: legacyGroup4?.pointee.enc_seckey, fixedLength: 32)).to(equal("")) + expect(legacyGroup4?.getHex(\.enc_pubkey, nullIfEmpty: true)).to(beNil()) + expect(legacyGroup4?.getHex(\.enc_seckey, nullIfEmpty: true)).to(beNil()) expect(legacyGroup4?.pointee.disappearing_timer).to(equal(60)) - expect(String(libSessionVal: legacyGroup4?.pointee.session_id)).to(equal(definitelyRealId)) + expect(legacyGroup4?.get(\.session_id, nullIfEmpty: false)).to(equal(definitelyRealId)) expect(legacyGroup4?.pointee.priority).to(equal(3)) - expect(String(libSessionVal: legacyGroup4?.pointee.name)).to(equal("Englishmen")) + expect(legacyGroup4?.get(\.name, nullIfEmpty: false)).to(equal("Englishmen")) expect(legacyGroup4?.pointee.joined_at).to(equal(createdTs)) expect(legacyGroup2.pointee.notifications).to(equal(CONVO_NOTIFY_ALL)) - expect(legacyGroup2.pointee.mute_until).to(equal(nowTs + 3600)) + expect(legacyGroup2.pointee.mute_until).to(equal(Int64(nowTs + 3600))) var membersSeen3: [String: Bool] = [:] var memberSessionId3: UnsafePointer? = nil @@ -1484,10 +1471,10 @@ fileprivate extension LibSessionUtilSpec { var memberCount: Int = 0 var adminCount: Int = 0 ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + seen.append("legacy: \(c1.get(\.name)), \(adminCount) admins, \(memberCount) members") } else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + seen.append("community: \(c2.get(\.base_url))/r/\(c2.get(\.room))") } else { seen.append("unknown") @@ -1509,9 +1496,9 @@ fileprivate extension LibSessionUtilSpec { var community2: ugroups_community_info = ugroups_community_info() expect(user_groups_get_community(conf2, &community2, &cCommunity2BaseUrl, &cCommunity2Room)) .to(beTrue()) - expect(String(libSessionVal: community2.base_url)).to(equal("http://example.org:5678")) - expect(String(libSessionVal: community2.room)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value - expect(Data(libSessionVal: community2.pubkey, count: 32).toHexString()) + expect(community2.get(\.base_url, nullIfEmpty: false)).to(equal("http://example.org:5678")) + expect(community2.get(\.room, nullIfEmpty: false)).to(equal("SudokuRoom")) // Case preserved from the stored value, not the input value + expect(community2.getHex(\.pubkey, nullIfEmpty: false)) .to(equal("0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef")) expect(community2.priority).to(equal(14)) @@ -1523,7 +1510,7 @@ fileprivate extension LibSessionUtilSpec { expect(config_needs_dump(conf2)).to(beFalse()) pushData6.deallocate() - community2.room = "sudokuRoom".toLibSession() // Change capitalization + community2.set(\.room, to: "sudokuRoom") // Change capitalization user_groups_set_community(conf2, &community2) expect(config_needs_push(conf2)).to(beTrue()) @@ -1569,7 +1556,7 @@ fileprivate extension LibSessionUtilSpec { var community3: ugroups_community_info = ugroups_community_info() expect(user_groups_get_community(conf, &community3, &cCommunity3BaseUrl, &cCommunity3Room)) .to(beTrue()) - expect(String(libSessionVal: community3.room)).to(equal("sudokuRoom")) // We picked up the capitalization change + expect(community3.get(\.room, nullIfEmpty: false)).to(equal("sudokuRoom")) // We picked up the capitalization change expect(user_groups_size(conf)).to(equal(2)) expect(user_groups_size_communities(conf)).to(equal(1)) @@ -1710,10 +1697,10 @@ fileprivate extension LibSessionUtilSpec { var adminCount: Int = 0 ugroups_legacy_members_count(&c1, &memberCount, &adminCount) - seen.append("legacy: \(String(libSessionVal: c1.name)), \(adminCount) admins, \(memberCount) members") + seen.append("legacy: \(c1.get(\.name)), \(adminCount) admins, \(memberCount) members") } else if user_groups_it_is_community(it, &c2) { - seen.append("community: \(String(libSessionVal: c2.base_url))/r/\(String(libSessionVal: c2.room))") + seen.append("community: \(c2.get(\.base_url))/r/\(c2.get(\.room))") } else { seen.append("unknown") diff --git a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift b/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift deleted file mode 100644 index 6ce43ff340..0000000000 --- a/SessionMessagingKitTests/LibSession/Utilities/LibSessionTypeConversionUtilitiesSpec.swift +++ /dev/null @@ -1,373 +0,0 @@ -// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. - -import Foundation - -import Quick -import Nimble - -@testable import SessionMessagingKit - -class LibSessionTypeConversionUtilitiesSpec: QuickSpec { - override class func spec() { - // MARK: - a String - describe("a String") { - // MARK: -- can contain emoji - it("can contain emoji") { - let original: String = "Hi 👋" - let libSessionVal: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar) = original.toLibSession() - let result: String? = String(libSessionVal: libSessionVal) - - expect(result).to(equal(original)) - } - - // MARK: -- when initialised with a pointer and length - context("when initialised with a pointer and length") { - // MARK: ---- returns null when given a null pointer - it("returns null when given a null pointer") { - let test: [CChar] = [84, 101, 115, 116] - let result = test.withUnsafeBufferPointer { ptr in - String(pointer: nil, length: 5) - } - - expect(result).to(beNil()) - } - - // MARK: ---- returns a truncated string when given an incorrect length - it("returns a truncated string when given an incorrect length") { - let test: [CChar] = [84, 101, 115, 116] - let result = test.withUnsafeBufferPointer { ptr in - String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 2) - } - - expect(result).to(equal("Te")) - } - - // MARK: ---- returns a string when valid - it("returns a string when valid") { - let test: [CChar] = [84, 101, 115, 116] - let result = test.withUnsafeBufferPointer { ptr in - String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 4) - } - - expect(result).to(equal("Test")) - } - } - - // MARK: -- when initialised with a libSession value - context("when initialised with a libSession value") { - // MARK: ---- returns a string when valid and has no fixed length - it("returns a string when valid and has no fixed length") { - let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 0) - let result = String(libSessionVal: value, fixedLength: .none) - - expect(result).to(equal("Test")) - } - - // MARK: ---- returns a string when valid and has a fixed length - it("returns a string when valid and has a fixed length") { - let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116) - let result = String(libSessionVal: value, fixedLength: 5) - - expect(result).to(equal("Te\0st")) - } - - // MARK: ---- truncates at the first null termination character when fixed length is none - it("truncates at the first null termination character when fixed length is none") { - let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 115, 116) - let result = String(libSessionVal: value, fixedLength: .none) - - expect(result).to(equal("Te")) - } - - // MARK: ---- parses successfully if there is no null termination character and there is no fixed length - it("parses successfully if there is no null termination character and there is no fixed length") { - let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 115, 116, 84) - let result = String(libSessionVal: value, fixedLength: .none) - - expect(result).to(equal("TestT")) - } - - // MARK: ---- returns an empty string when given a value only containing null termination characters with a fixed length - it("returns an empty string when given a value only containing null termination characters with a fixed length") { - let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) - let result = String(libSessionVal: value, fixedLength: 5) - - expect(result).to(equal("")) - } - - // MARK: ---- defaults the fixed length value to none - it("defaults the fixed length value to none") { - let value: (CChar, CChar, CChar, CChar, CChar) = (84, 101, 0, 0, 0) - let result = String(libSessionVal: value) - - expect(result).to(equal("Te")) - } - - // MARK: ---- returns an empty string when null and not set to return null - it("returns an empty string when null and not set to return null") { - let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) - let result = String(libSessionVal: value, nullIfEmpty: false) - - expect(result).to(equal("")) - } - - // MARK: ---- returns null when specified and empty - it("returns null when specified and empty") { - let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) - let result = String(libSessionVal: value, nullIfEmpty: true) - - expect(result).to(beNil()) - } - - // MARK: ---- defaults the null if empty flag to false - it("defaults the null if empty flag to false") { - let value: (CChar, CChar, CChar, CChar, CChar) = (0, 0, 0, 0, 0) - let result = String(libSessionVal: value) - - expect(result).to(equal("")) - } - } - - // MARK: -- when converting to a libSession value - context("when converting to a libSession value") { - // MARK: ---- succeeeds with a valid value - it("succeeeds with a valid value") { - let result: (CChar, CChar, CChar, CChar, CChar) = "Test".toLibSession() - expect(result.0).to(equal(84)) - expect(result.1).to(equal(101)) - expect(result.2).to(equal(115)) - expect(result.3).to(equal(116)) - expect(result.4).to(equal(0)) - } - - // MARK: ---- truncates when too long - it("truncates when too long") { - let result: (CChar, CChar, CChar, CChar, CChar, CChar) = "TestTest".toLibSession() - expect(result.0).to(equal(84)) - expect(result.1).to(equal(101)) - expect(result.2).to(equal(115)) - expect(result.3).to(equal(116)) - expect(result.4).to(equal(84)) - expect(result.5).to(equal(0)) // Last character will always be a null termination - } - - // MARK: ---- when optional - context("when optional") { - // MARK: ------ returns empty when null - context("returns empty when null") { - let value: String? = nil - let result: (CChar, CChar, CChar, CChar, CChar) = value.toLibSession() - - expect(result.0).to(equal(0)) - expect(result.1).to(equal(0)) - expect(result.2).to(equal(0)) - expect(result.3).to(equal(0)) - expect(result.4).to(equal(0)) - } - - // MARK: ------ returns a libSession value when not null - context("returns a libSession value when not null") { - let value: String? = "Test" - let result: (CChar, CChar, CChar, CChar, CChar) = value.toLibSession() - - expect(result.0).to(equal(84)) - expect(result.1).to(equal(101)) - expect(result.2).to(equal(115)) - expect(result.3).to(equal(116)) - expect(result.4).to(equal(0)) - } - } - } - } - - // MARK: - Data - describe("Data") { - // MARK: -- when initialised with a libSession value - context("when initialised with a libSession value") { - // MARK: ---- returns truncated data when given the wrong length - it("returns truncated data when given the wrong length") { - let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5) - let result = Data(libSessionVal: value, count: 2) - - expect(result).to(equal(Data([1, 2]))) - } - - // MARK: ---- returns data when valid - it("returns data when valid") { - let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (1, 2, 3, 4, 5) - let result = Data(libSessionVal: value, count: 5) - - expect(result).to(equal(Data([1, 2, 3, 4, 5]))) - } - - // MARK: ---- returns data when all bytes are zero and nullIfEmpty is false - it("returns data when all bytes are zero and nullIfEmpty is false") { - let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0) - let result = Data(libSessionVal: value, count: 5, nullIfEmpty: false) - - expect(result).to(equal(Data([0, 0, 0, 0, 0]))) - } - - // MARK: ---- returns null when all bytes are zero and nullIfEmpty is true - it("returns null when all bytes are zero and nullIfEmpty is true") { - let value: (UInt8, UInt8, UInt8, UInt8, UInt8) = (0, 0, 0, 0, 0) - let result = Data(libSessionVal: value, count: 5, nullIfEmpty: true) - - expect(result).to(beNil()) - } - } - - // MARK: -- when converting to a libSession value - context("when converting to a libSession value") { - // MARK: ---- succeeeds with a valid value - it("succeeeds with a valid value") { - let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 5]).toLibSession() - expect(result.0).to(equal(1)) - expect(result.1).to(equal(2)) - expect(result.2).to(equal(3)) - expect(result.3).to(equal(4)) - expect(result.4).to(equal(5)) - } - - // MARK: ---- truncates when too long - it("truncates when too long") { - let result: (Int8, Int8, Int8, Int8, Int8) = Data([1, 2, 3, 4, 1, 2, 3, 4]).toLibSession() - expect(result.0).to(equal(1)) - expect(result.1).to(equal(2)) - expect(result.2).to(equal(3)) - expect(result.3).to(equal(4)) - expect(result.4).to(equal(1)) - } - - // MARK: ---- fills with empty data when too short - context("fills with empty data when too short") { - let value: Data? = Data([1, 2, 3]) - let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() - - expect(result.0).to(equal(1)) - expect(result.1).to(equal(2)) - expect(result.2).to(equal(3)) - expect(result.3).to(equal(0)) - expect(result.4).to(equal(0)) - } - - // MARK: ---- when optional - context("when optional") { - // MARK: ------ returns null when null - context("returns null when null") { - let value: Data? = nil - let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() - - expect(result.0).to(equal(0)) - expect(result.1).to(equal(0)) - expect(result.2).to(equal(0)) - expect(result.3).to(equal(0)) - expect(result.4).to(equal(0)) - } - - // MARK: ------ returns a libSession value when not null - context("returns a libSession value when not null") { - let value: Data? = Data([1, 2, 3, 4, 5]) - let result: (Int8, Int8, Int8, Int8, Int8) = value.toLibSession() - - expect(result.0).to(equal(1)) - expect(result.1).to(equal(2)) - expect(result.2).to(equal(3)) - expect(result.3).to(equal(4)) - expect(result.4).to(equal(5)) - } - } - } - } - - // MARK: - an Array - describe("an Array") { - // MARK: -- when initialised with a 2D C array - context("when initialised with a 2D C array") { - // MARK: ---- returns the correct array - it("returns the correct array") { - var test: [CChar] = ( - "Test1".cString(using: .utf8)! + - "Test2".cString(using: .utf8)! + - "Test3AndExtra".cString(using: .utf8)! - ) - let result = test.withUnsafeMutableBufferPointer { ptr in - var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) - - return [String](pointer: &mutablePtr, count: 3) - } - - expect(result).to(equal(["Test1", "Test2", "Test3AndExtra"])) - } - - // MARK: ---- returns an empty array if given one - it("returns an empty array if given one") { - var test = [CChar]() - let result = test.withUnsafeMutableBufferPointer { ptr in - var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) - - return [String](pointer: &mutablePtr, count: 0) - } - - expect(result).to(equal([])) - } - - // MARK: ---- handles empty strings without issues - it("handles empty strings without issues") { - var test: [CChar] = ( - "Test1".cString(using: .utf8)! + - "".cString(using: .utf8)! + - "Test2".cString(using: .utf8)! - ) - let result = test.withUnsafeMutableBufferPointer { ptr in - var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) - - return [String](pointer: &mutablePtr, count: 3) - } - - expect(result).to(equal(["Test1", "", "Test2"])) - } - - // MARK: ---- returns null when given a null pointer - it("returns null when given a null pointer") { - expect([String](pointer: nil, count: 5)).to(beNil()) - } - - // MARK: ---- returns null when given a null count - it("returns null when given a null count") { - var test: [CChar] = "Test1".cString(using: .utf8)! - let result = test.withUnsafeMutableBufferPointer { ptr in - var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) - - return [String](pointer: &mutablePtr, count: nil) - } - - expect(result).to(beNil()) - } - - // MARK: ---- returns the default value if given null values - it("returns the default value if given null values") { - expect([String](pointer: nil, count: 5, defaultValue: ["Test"])) - .to(equal(["Test"])) - } - } - - // MARK: -- when adding a null terminated character - context("when adding a null terminated character") { - // MARK: ---- adds a null termination character when not present - it("adds a null termination character when not present") { - let value: [CChar] = [1, 2, 3, 4, 5] - - expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 5, 0])) - } - - // MARK: ---- adds nothing when already present - it("adds nothing when already present") { - let value: [CChar] = [1, 2, 3, 4, 0] - - expect(value.nullTerminated()).to(equal([1, 2, 3, 4, 0])) - } - } - } - } -} diff --git a/SessionNotificationServiceExtension/NSENotificationPresenter.swift b/SessionNotificationServiceExtension/NSENotificationPresenter.swift index 27b0db43cd..c53fa4bb36 100644 --- a/SessionNotificationServiceExtension/NSENotificationPresenter.swift +++ b/SessionNotificationServiceExtension/NSENotificationPresenter.swift @@ -146,7 +146,10 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { else { return } // Only notify missed calls - guard messageInfo.state == .missed || messageInfo.state == .permissionDenied else { return } + switch messageInfo.state { + case .missed, .permissionDenied, .permissionDeniedMicrophone: break + default: return + } let userInfo: [String: Any] = [ NotificationServiceExtension.isFromRemoteKey: true, @@ -174,6 +177,12 @@ public class NSENotificationPresenter: NSObject, NotificationsProtocol { senderName ) } + else if messageInfo.state == .permissionDeniedMicrophone { + notificationContent.body = String( + format: "call_missed".localized(), + senderName + ) + } addNotifcationRequest( identifier: UUID().uuidString, diff --git a/SessionNotificationServiceExtension/NotificationServiceExtension.swift b/SessionNotificationServiceExtension/NotificationServiceExtension.swift index 4eb4b578e5..f8e74b0b33 100644 --- a/SessionNotificationServiceExtension/NotificationServiceExtension.swift +++ b/SessionNotificationServiceExtension/NotificationServiceExtension.swift @@ -1,6 +1,7 @@ // Copyright © 2022 Rangeproof Pty Ltd. All rights reserved. import Foundation +import AVFAudio import Combine import GRDB import CallKit @@ -11,8 +12,8 @@ import SignalUtilitiesKit import SessionUtilitiesKit public final class NotificationServiceExtension: UNNotificationServiceExtension { - private let dependencies: Dependencies = Dependencies() - private var didPerformSetup = false + private var dependencies: Dependencies = Dependencies() + private var startTime: CFTimeInterval = 0 private var contentHandler: ((UNNotificationContent) -> Void)? private var request: UNNotificationRequest? private var hasCompleted: Atomic = Atomic(false) @@ -26,6 +27,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: Did receive a remote push notification request override public func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) { + self.startTime = CACurrentMediaTime() self.contentHandler = contentHandler self.request = request @@ -34,13 +36,11 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Abort if the main app is running guard !(UserDefaults.sharedLokiProject?[.isMainAppActive]).defaulting(to: false) else { - Log.info("didReceive called while main app running.") return self.completeSilenty(handledNotification: false, isMainAppAndActive: true) } guard let notificationContent = request.content.mutableCopy() as? UNMutableNotificationContent else { - Log.info("didReceive called with no content.") - return self.completeSilenty(handledNotification: false) + return self.completeSilenty(handledNotification: false, noContent: true) } Log.info("didReceive called.") @@ -50,10 +50,12 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension Singleton.setup(appContext: NotificationServiceExtensionContext()) } - // Perform main setup - Storage.resumeDatabaseAccess(using: dependencies) + /// Perform main setup (create a new `Dependencies` instance each time so we don't need to worry about state from previous + /// notifications causing issues with new notifications + self.dependencies = Dependencies() + DispatchQueue.main.sync { - self.setUpIfNecessary() { [weak self] in + self.performSetup { [weak self] in self?.handleNotification(notificationContent, isPerformingResetup: false) } } @@ -65,16 +67,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension using: dependencies ) - guard metadata.accountId == getUserHexEncodedPublicKey(using: dependencies) else { - guard !isPerformingResetup else { - Log.error("Received notification for an accountId that isn't the current user, resetup failed.") - return self.completeSilenty(handledNotification: false) - } - - Log.warn("Received notification for an accountId that isn't the current user, attempting to resetup.") - return self.forceResetup(notificationContent) - } - guard (result == .success || result == .legacySuccess), let data: Data = maybeData @@ -158,14 +150,15 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension throw NotificationError.ignorableMessage } - switch (db[.areCallsEnabled], isCallOngoing) { + let hasMicrophonePermission: Bool = (AVAudioSession.sharedInstance().recordPermission == .granted) + switch ((db[.areCallsEnabled] && hasMicrophonePermission), isCallOngoing) { case (false, _): if let sender: String = callMessage.sender, let interaction: Interaction = try MessageReceiver.insertCallInfoMessage( db, for: callMessage, - state: .permissionDenied, + state: (db[.areCallsEnabled] ? .permissionDeniedMicrophone : .permissionDenied), using: dependencies ) { @@ -238,31 +231,35 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // If an error occurred we want to rollback the transaction (by throwing) and then handle // the error outside of the database let handleError = { - switch error { - case MessageReceiverError.noGroupKeyPair: - Log.warn("Failed due to having no legacy group decryption keys.") - self?.completeSilenty(handledNotification: false) - - case MessageReceiverError.outdatedMessage: - Log.info("Ignoring notification for already seen message.") - self?.completeSilenty(handledNotification: false) - - case NotificationError.ignorableMessage: - Log.info("Ignoring message which requires no notification.") - self?.completeSilenty(handledNotification: false) - - case MessageReceiverError.duplicateMessage, MessageReceiverError.duplicateControlMessage, - MessageReceiverError.duplicateMessageNewSnode: - Log.info("Ignoring duplicate message (probably received it just before going to the background).") - self?.completeSilenty(handledNotification: false) - - case NotificationError.messageProcessing: - self?.handleFailure(for: notificationContent, error: .messageProcessing) - - case let msgError as MessageReceiverError: - self?.handleFailure(for: notificationContent, error: .messageHandling(msgError)) - - default: self?.handleFailure(for: notificationContent, error: .other(error)) + // Dispatch to the next run loop to ensure we are out of the database write thread before + // handling the result (and suspending the database) + DispatchQueue.main.async { + switch error { + case MessageReceiverError.noGroupKeyPair: + Log.warn("Failed due to having no legacy group decryption keys.") + self?.completeSilenty(handledNotification: false) + + case MessageReceiverError.outdatedMessage: + Log.info("Ignoring notification for already seen message.") + self?.completeSilenty(handledNotification: false) + + case NotificationError.ignorableMessage: + Log.info("Ignoring message which requires no notification.") + self?.completeSilenty(handledNotification: false) + + case MessageReceiverError.duplicateMessage, MessageReceiverError.duplicateControlMessage, + MessageReceiverError.duplicateMessageNewSnode: + Log.info("Ignoring duplicate message (probably received it just before going to the background).") + self?.completeSilenty(handledNotification: false) + + case NotificationError.messageProcessing: + self?.handleFailure(for: notificationContent, error: .messageProcessing) + + case let msgError as MessageReceiverError: + self?.handleFailure(for: notificationContent, error: .messageHandling(msgError)) + + default: self?.handleFailure(for: notificationContent, error: .other(error)) + } } } @@ -277,18 +274,29 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // MARK: Setup - private func setUpIfNecessary(completion: @escaping () -> Void) { - Log.assertOnMainThread() - - // The NSE will often re-use the same process, so if we're - // already set up we want to do nothing; we're already ready - // to process new messages. - guard !didPerformSetup else { return completion() } - + private func performSetup(completion: @escaping () -> Void) { Log.info("Performing setup.") - didPerformSetup = true _ = AppVersion.shared + + // FIXME: Remove these once the database instance is fully managed via `Dependencies` + if AppSetup.hasRun { + dependencies.storage.resumeDatabaseAccess() + dependencies.storage.reconfigureDatabase() + dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() } + + // If we had already done a setup then `libSession` won't have been re-setup so + // we need to do so now (this ensures it has the correct user keys as well) + LibSession.clearMemoryState(using: dependencies) + dependencies.storage.read { [dependencies] db in + LibSession.loadState( + db, + userPublicKey: getUserHexEncodedPublicKey(db, using: dependencies), + ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey, + using: dependencies + ) + } + } AppSetup.setupEnvironment( retrySetupIfDatabaseInvalid: true, @@ -307,26 +315,39 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension // Setup LibSession LibSession.addLogger() }, - migrationsCompletion: { [weak self] result, needsConfigSync in + migrationsCompletion: { [weak self, dependencies] result, _ in switch result { case .failure(let error): Log.error("Failed to complete migrations: \(error).") self?.completeSilenty(handledNotification: false) case .success: - // We should never receive a non-voip notification on an app that doesn't support - // app extensions since we have to inform the service we wanted these, so in theory - // this path should never occur. However, the service does have our push token - // so it is possible that could change in the future. If it does, do nothing - // and don't disturb the user. Messages will be processed when they open the app. - guard Storage.shared[.isReadyForAppExtensions] else { - Log.error("Not ready for extensions.") - self?.completeSilenty(handledNotification: false) - return - } - DispatchQueue.main.async { - self?.versionMigrationsDidComplete(needsConfigSync: needsConfigSync, completion: completion) + // Ensure storage is actually valid + guard dependencies.storage.isValid else { + Log.error("Storage invalid.") + self?.completeSilenty(handledNotification: false) + return + } + + // We should never receive a non-voip notification on an app that doesn't support + // app extensions since we have to inform the service we wanted these, so in theory + // this path should never occur. However, the service does have our push token + // so it is possible that could change in the future. If it does, do nothing + // and don't disturb the user. Messages will be processed when they open the app. + guard dependencies.storage[.isReadyForAppExtensions] else { + Log.error("Not ready for extensions.") + self?.completeSilenty(handledNotification: false) + return + } + + // If the app wasn't ready then mark it as ready now + if !Singleton.appReadiness.isAppReady { + // Note that this does much more than set a flag; it will also run all deferred blocks. + Singleton.appReadiness.setAppReady() + } + + completion() } } }, @@ -334,60 +355,6 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension ) } - private func versionMigrationsDidComplete(needsConfigSync: Bool, completion: @escaping () -> Void) { - Log.assertOnMainThread() - - // If we need a config sync then trigger it now - if needsConfigSync { - Storage.shared.write { db in - ConfigurationSyncJob.enqueue(db, publicKey: getUserHexEncodedPublicKey(db)) - } - } - - // App isn't ready until storage is ready AND all version migrations are complete. - guard Storage.shared.isValid else { - Log.error("Storage invalid.") - return self.completeSilenty(handledNotification: false) - } - - // If the app wasn't ready then mark it as ready now - if !Singleton.appReadiness.isAppReady { - // Note that this does much more than set a flag; it will also run all deferred blocks. - Singleton.appReadiness.setAppReady() - } - - completion() - } - - /// It's possible for the NotificationExtension to still have some kind of cached data from the old database after it's been deleted - /// when a new account is created shortly after, this results in weird errors when receiving PNs for the new account - /// - /// In order to avoid this situation we check to see whether the received PN is targetting the current user and, if not, we call this - /// method to force a resetup of the notification extension - /// - /// **Note:** We need to reconfigure the database here because if the database was deleted it's possible for the NotificationExtension - /// to somehow still have some form of access to the old one - private func forceResetup(_ notificationContent: UNMutableNotificationContent) { - Storage.reconfigureDatabase() - LibSession.clearMemoryState(using: dependencies) - dependencies.caches.mutate(cache: .general) { $0.clearCachedUserPublicKey() } - - self.setUpIfNecessary() { [weak self, dependencies] in - // If we had already done a setup then `libSession` won't have been re-setup so - // we need to do so now (this ensures it has the correct user keys as well) - Storage.shared.read { db in - LibSession.loadState( - db, - userPublicKey: getUserHexEncodedPublicKey(db), - ed25519SecretKey: Identity.fetchUserEd25519KeyPair(db)?.secretKey, - using: dependencies - ) - } - - self?.handleNotification(notificationContent, isPerformingResetup: true) - } - } - // MARK: Handle completion override public func serviceExtensionTimeWillExpire() { @@ -397,7 +364,7 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension completeSilenty(handledNotification: false) } - private func completeSilenty(handledNotification: Bool, isMainAppAndActive: Bool = false) { + private func completeSilenty(handledNotification: Bool, isMainAppAndActive: Bool = false, noContent: Bool = false) { // Ensure we only run this once guard hasCompleted.mutate({ hasCompleted in @@ -408,14 +375,21 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension else { return } let silentContent: UNMutableNotificationContent = UNMutableNotificationContent() - silentContent.badge = Storage.shared - .read { db in try Interaction.fetchUnreadCount(db) } - .map { NSNumber(value: $0) } - .defaulting(to: NSNumber(value: 0)) - Log.info(handledNotification ? "Completed after handling notification." : "Completed silently.") if !isMainAppAndActive { - Storage.suspendDatabaseAccess(using: dependencies) + silentContent.badge = dependencies.storage + .read { db in try Interaction.fetchUnreadCount(db) } + .map { NSNumber(value: $0) } + .defaulting(to: NSNumber(value: 0)) + dependencies.storage.suspendDatabaseAccess() + } + + let duration: CFTimeInterval = (CACurrentMediaTime() - startTime) + switch (isMainAppAndActive, handledNotification, noContent) { + case (true, _, _): Log.info("Called while main app running, ignoring after \(.seconds(duration), unit: .ms).") + case (_, _, true): Log.info("Called with no content, ignoring after \(.seconds(duration), unit: .ms).") + case (_, true, _): Log.info("Completed after handling notification in \(.seconds(duration), unit: .ms).") + default: Log.info("Completed silently after \(.seconds(duration), unit: .ms).") } Log.flush() @@ -494,8 +468,10 @@ public final class NotificationServiceExtension: UNNotificationServiceExtension } private func handleFailure(for content: UNMutableNotificationContent, error: NotificationError) { - Log.error("Show generic failure message due to error: \(error).") - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() + + let duration: CFTimeInterval = (CACurrentMediaTime() - startTime) + Log.error("Show generic failure message after \(.seconds(duration), unit: .ms) due to error: \(error).") Log.flush() content.title = "Session" diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 259f9ef5d6..52b17832a7 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -111,7 +111,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView // When the thread picker disappears it means the user has left the screen (this will be called // whether the user has sent the message or cancelled sending) LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: viewModel.dependencies) + viewModel.dependencies.storage.suspendDatabaseAccess() Log.flush() } @@ -240,7 +240,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView shareNavController?.dismiss(animated: true, completion: nil) ModalActivityIndicatorViewController.present(fromViewController: shareNavController!, canCancel: false, message: "vc_share_sending_message".localized()) { [dependencies = viewModel.dependencies] activityIndicator in - Storage.resumeDatabaseAccess(using: dependencies) + dependencies.storage.resumeDatabaseAccess() LibSession.resumeNetworkAccess() let swarmPublicKey: String = { @@ -336,7 +336,7 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView .sinkUntilComplete( receiveCompletion: { [weak self] result in LibSession.suspendNetworkAccess() - Storage.suspendDatabaseAccess(using: dependencies) + dependencies.storage.suspendDatabaseAccess() Log.flush() activityIndicator.dismiss { } diff --git a/SessionSnodeKit/LibSession/LibSession+Networking.swift b/SessionSnodeKit/LibSession/LibSession+Networking.swift index 6d03653bbd..162f9abd33 100644 --- a/SessionSnodeKit/LibSession/LibSession+Networking.swift +++ b/SessionSnodeKit/LibSession/LibSession+Networking.swift @@ -174,10 +174,10 @@ public extension LibSession { .flatMap { network in CallbackWrapper .create { wrapper in - let cSwarmPublicKey: [CChar] = swarmPublicKey + guard let cSwarmPublicKey: [CChar] = swarmPublicKey .suffix(64) // Quick way to drop '05' prefix if present - .cArray - .nullTerminated() + .cString(using: .utf8) + else { throw LibSessionError.invalidCConversion } network_get_swarm(network, cSwarmPublicKey, { swarmPtr, swarmSize, ctx in guard @@ -230,10 +230,11 @@ public extension LibSession { to destination: Network.Destination, body: T?, swarmPublicKey: String?, - timeout: TimeInterval, + requestTimeout: TimeInterval, + requestAndPathBuildTimeout: TimeInterval?, using dependencies: Dependencies ) -> AnyPublisher<(ResponseInfoType, Data?), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?) + typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return getOrCreateNetwork() .tryFlatMap { network in @@ -269,10 +270,13 @@ public extension LibSession { cPayloadBytes, cPayloadBytes.count, cSwarmPublicKey, - Int64(floor(timeout * 1000)), - { success, timeout, statusCode, dataPtr, dataLen, ctx in + Int64(floor(requestTimeout * 1000)), + Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), + { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper + .headers(cHeaders, cHeaderVals, headerLen) let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), data)) + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) }, wrapper.unsafePointer() ) @@ -283,18 +287,21 @@ public extension LibSession { try wrapper.cServerDestination(destination), cPayloadBytes, cPayloadBytes.count, - Int64(floor(timeout * 1000)), - { success, timeout, statusCode, dataPtr, dataLen, ctx in + Int64(floor(requestTimeout * 1000)), + Int64(floor((requestAndPathBuildTimeout ?? 0) * 1000)), + { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper + .headers(cHeaders, cHeaderVals, headerLen) let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), data)) + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) }, wrapper.unsafePointer() ) } } - .tryMap { success, timeout, statusCode, data -> (any ResponseInfoType, Data?) in - try throwErrorIfNeeded(success, timeout, statusCode, data) - return (Network.ResponseInfo(code: statusCode), data) + .tryMap { success, timeout, statusCode, headers, data -> (any ResponseInfoType, Data?) in + try throwErrorIfNeeded(success, timeout, statusCode, headers, data) + return (Network.ResponseInfo(code: statusCode, headers: headers), data) } } .eraseToAnyPublisher() @@ -306,7 +313,7 @@ public extension LibSession { fileName: String?, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<(ResponseInfoType, FileUploadResponse), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?) + typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return getOrCreateNetwork() .tryFlatMap { network in @@ -319,20 +326,23 @@ public extension LibSession { data.count, fileName?.cString(using: .utf8), Int64(floor(Network.fileUploadTimeout * 1000)), - { success, timeout, statusCode, dataPtr, dataLen, ctx in + 0, + { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper + .headers(cHeaders, cHeaderVals, headerLen) let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), data)) + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) }, wrapper.unsafePointer() ) } - .tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, FileUploadResponse) in - try throwErrorIfNeeded(success, timeout, statusCode, maybeData) + .tryMap { success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, FileUploadResponse) in + try throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData) guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return ( - Network.ResponseInfo(code: statusCode), + Network.ResponseInfo(code: statusCode, headers: headers), try FileUploadResponse.decoded(from: data, using: dependencies) ) } @@ -340,7 +350,7 @@ public extension LibSession { } static func downloadFile(from server: Network.Destination) -> AnyPublisher<(ResponseInfoType, Data), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?) + typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return getOrCreateNetwork() .tryFlatMap { network in @@ -350,20 +360,23 @@ public extension LibSession { network, try wrapper.cServerDestination(server), Int64(floor(Network.fileDownloadTimeout * 1000)), - { success, timeout, statusCode, dataPtr, dataLen, ctx in + 0, + { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper + .headers(cHeaders, cHeaderVals, headerLen) let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), data)) + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) }, wrapper.unsafePointer() ) } - .tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, Data) in - try throwErrorIfNeeded(success, timeout, statusCode, maybeData) + .tryMap { success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, Data) in + try throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData) guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return ( - Network.ResponseInfo(code: statusCode), + Network.ResponseInfo(code: statusCode, headers: headers), data ) } @@ -374,7 +387,7 @@ public extension LibSession { ed25519SecretKey: [UInt8], using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<(ResponseInfoType, AppVersionResponse), Error> { - typealias Output = (success: Bool, timeout: Bool, statusCode: Int, data: Data?) + typealias Output = (success: Bool, timeout: Bool, statusCode: Int, headers: [String: String], data: Data?) return getOrCreateNetwork() .tryFlatMap { network in @@ -387,20 +400,23 @@ public extension LibSession { CLIENT_PLATFORM_IOS, &cEd25519SecretKey, Int64(floor(Network.fileDownloadTimeout * 1000)), - { success, timeout, statusCode, dataPtr, dataLen, ctx in + 0, + { success, timeout, statusCode, cHeaders, cHeaderVals, headerLen, dataPtr, dataLen, ctx in + let headers: [String: String] = CallbackWrapper + .headers(cHeaders, cHeaderVals, headerLen) let data: Data? = dataPtr.map { Data(bytes: $0, count: dataLen) } - CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), data)) + CallbackWrapper.run(ctx, (success, timeout, Int(statusCode), headers, data)) }, wrapper.unsafePointer() ) } - .tryMap { success, timeout, statusCode, maybeData -> (any ResponseInfoType, AppVersionResponse) in - try throwErrorIfNeeded(success, timeout, statusCode, maybeData) + .tryMap { success, timeout, statusCode, headers, maybeData -> (any ResponseInfoType, AppVersionResponse) in + try throwErrorIfNeeded(success, timeout, statusCode, headers, maybeData) guard let data: Data = maybeData else { throw NetworkError.parsingFailed } return ( - Network.ResponseInfo(code: statusCode), + Network.ResponseInfo(code: statusCode, headers: headers), try AppVersionResponse.decoded(from: data, using: dependencies) ) } @@ -538,10 +554,16 @@ public extension LibSession { _ success: Bool, _ timeout: Bool, _ statusCode: Int, + _ headers: [String: String], _ data: Data? ) throws { guard !success || statusCode < 200 || statusCode > 299 else { return } - guard !timeout else { throw NetworkError.timeout } + guard !timeout else { + switch data.map({ String(data: $0, encoding: .ascii) }) { + case .none: throw NetworkError.timeout(error: "\(NetworkError.unknown)", rawData: data) + case .some(let responseString): throw NetworkError.timeout(error: responseString, rawData: data) + } + } /// Handle status codes with specific meanings switch (statusCode, data.map { String(data: $0, encoding: .ascii) }) { @@ -551,7 +573,8 @@ public extension LibSession { case (401, _): Log.warn("Unauthorised (Failed to verify the signature).") throw NetworkError.unauthorised - + + case (403, _): throw NetworkError.forbidden case (404, _): throw NetworkError.notFound /// A snode will return a `406` but onion requests v4 seems to return `425` so handle both @@ -618,30 +641,18 @@ extension LibSession { public var description: String { address } public var cSnode: network_service_node { - return network_service_node( - ip: ip.toLibSession(), - quic_port: quicPort, - ed25519_pubkey_hex: ed25519PubkeyHex.toLibSession() - ) + var result: network_service_node = network_service_node() + result.ipString = ip + result.set(\.quic_port, to: quicPort) + result.set(\.ed25519_pubkey_hex, to: ed25519PubkeyHex) + + return result } init(_ cSnode: network_service_node) { - ip = "\(cSnode.ip.0).\(cSnode.ip.1).\(cSnode.ip.2).\(cSnode.ip.3)" - quicPort = cSnode.quic_port - ed25519PubkeyHex = String(libSessionVal: cSnode.ed25519_pubkey_hex) - } - - internal init?(nodeString: String) { - let parts: [String] = nodeString.components(separatedBy: "|") - - guard - parts.count == 4, - let port: UInt16 = UInt16(parts[1]) - else { return nil } - - ip = parts[0] - quicPort = port - ed25519PubkeyHex = parts[3] + ip = cSnode.ipString + quicPort = cSnode.get(\.quic_port) + ed25519PubkeyHex = cSnode.get(\.ed25519_pubkey_hex) } public func hash(into hasher: inout Hasher) { @@ -681,6 +692,17 @@ public extension Network.Destination { } internal extension LibSession.CallbackWrapper { + static func headers( + _ cHeaders: UnsafeMutablePointer?>?, + _ cHeaderVals: UnsafeMutablePointer?>?, + _ count: Int + ) -> [String: String] { + let headers: [String] = [String](pointer: cHeaders, count: count, defaultValue: []) + let headerVals: [String] = [String](pointer: cHeaderVals, count: count, defaultValue: []) + + return zip(headers, headerVals) + .reduce(into: [:]) { result, next in result[next.0] = next.1 } + } func cServerDestination(_ destination: Network.Destination) throws -> network_server_destination { guard case .server(let url, let method, let headers, let x25519PublicKey) = destination, @@ -758,3 +780,20 @@ internal extension LibSession.CallbackWrapper { return cServerDestination } } + +// MARK: - Convenience C Access + +extension network_service_node: CAccessible, CMutable { + var ipString: String { + get { "\(ip.0).\(ip.1).\(ip.2).\(ip.3)" } + set { + let ipParts: [UInt8] = newValue + .components(separatedBy: ".") + .compactMap { UInt8($0) } + + guard ipParts.count == 4 else { return } + + self.ip = (ipParts[0], ipParts[1], ipParts[2], ipParts[3]) + } + } +} diff --git a/SessionSnodeKit/Networking/Network+OnionRequest.swift b/SessionSnodeKit/Networking/Network+OnionRequest.swift index 25058c7f80..2a56efa250 100644 --- a/SessionSnodeKit/Networking/Network+OnionRequest.swift +++ b/SessionSnodeKit/Networking/Network+OnionRequest.swift @@ -11,20 +11,22 @@ public extension Network.RequestType { _ payload: Data, to snode: LibSession.Snode, swarmPublicKey: String?, - timeout: TimeInterval = Network.defaultTimeout + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil ) -> Network.RequestType { return Network.RequestType( id: "onionRequest", url: "quic://\(snode.address)", method: "POST", body: payload, - args: [payload, snode, swarmPublicKey, timeout] + args: [payload, snode, swarmPublicKey, requestTimeout, requestAndPathBuildTimeout] ) { dependencies in LibSession.sendOnionRequest( to: Network.Destination.snode(snode), body: payload, swarmPublicKey: swarmPublicKey, - timeout: timeout, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } @@ -34,7 +36,8 @@ public extension Network.RequestType { _ request: URLRequest, to server: String, with x25519PublicKey: String, - timeout: TimeInterval = Network.defaultTimeout + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil ) -> Network.RequestType { return Network.RequestType( id: "onionRequest", @@ -42,9 +45,9 @@ public extension Network.RequestType { method: request.httpMethod, headers: request.allHTTPHeaderFields, body: request.httpBody, - args: [request, server, x25519PublicKey, timeout] + args: [request, server, x25519PublicKey, requestTimeout, requestAndPathBuildTimeout] ) { dependencies in - guard let url = request.url, let host = request.url?.host else { + guard let url = request.url else { return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher() } @@ -57,7 +60,8 @@ public extension Network.RequestType { ), body: request.httpBody, swarmPublicKey: nil, - timeout: timeout, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) } diff --git a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift index 6cad6c491f..5d057c95d0 100644 --- a/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift +++ b/SessionSnodeKit/Networking/PreparedRequest+OnionRequest.swift @@ -32,7 +32,8 @@ public extension Network.PreparedRequest { request, to: serverTarget.server, with: serverTarget.x25519PublicKey, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ), using: dependencies ) @@ -46,7 +47,8 @@ public extension Network.PreparedRequest { payload, to: snodeTarget.snode, swarmPublicKey: snodeTarget.swarmPublicKey, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ), using: dependencies ) @@ -62,7 +64,8 @@ public extension Network.PreparedRequest { payload, to: snode, swarmPublicKey: randomTarget.swarmPublicKey, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ), using: dependencies ) @@ -74,7 +77,12 @@ public extension Network.PreparedRequest { return LibSession.getSwarm(swarmPublicKey: randomTarget.swarmPublicKey) .tryFlatMapWithRandomSnode(retry: randomTarget.retryCount, using: dependencies) { snode in SnodeAPI - .getNetworkTime(from: snode, using: dependencies) + .getNetworkTime( + from: snode, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, + using: dependencies + ) .tryFlatMap { timestampMs in guard let updatedRequest: URLRequest = try? randomTarget @@ -88,7 +96,8 @@ public extension Network.PreparedRequest { payload, to: snode, swarmPublicKey: randomTarget.swarmPublicKey, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ), using: dependencies ) diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index ede95b9aad..335dad11b9 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -740,7 +740,7 @@ public final class SnodeAPI { ) .send(using: dependencies) .tryMap { _, response -> Void in - try response.validResultMap( + _ = try response.validResultMap( swarmPublicKey: getUserHexEncodedPublicKey(), validationData: subkeyToRevoke, using: dependencies @@ -860,6 +860,8 @@ public final class SnodeAPI { /// Clears all the user's data from their swarm. Returns a dictionary of snode public key to deletion confirmation. public static func deleteAllMessages( namespace: SnodeAPI.Namespace, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies = Dependencies() ) -> AnyPublisher<[String: Bool], Error> { guard let userED25519KeyPair = Identity.fetchUserEd25519KeyPair() else { @@ -882,9 +884,12 @@ public final class SnodeAPI { timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), ed25519PublicKey: userED25519KeyPair.publicKey, ed25519SecretKey: userED25519KeyPair.secretKey - ) + ), + retryCount: 0 // Don't auto retry this request (user can manually retry on failure) ), responseType: DeleteAllMessagesResponse.self, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .send(using: dependencies) @@ -931,7 +936,8 @@ public final class SnodeAPI { timestampMs: UInt64(SnodeAPI.currentOffsetTimestampMs()), ed25519PublicKey: userED25519KeyPair.publicKey, ed25519SecretKey: userED25519KeyPair.secretKey - ) + ), + retryCount: 0 // Don't auto retry this request (user can manually retry on failure) ), responseType: DeleteAllBeforeResponse.self, using: dependencies @@ -953,6 +959,8 @@ public final class SnodeAPI { public static func getNetworkTime( from snode: LibSession.Snode, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) -> AnyPublisher { do { @@ -964,6 +972,8 @@ public final class SnodeAPI { body: [String: String]() ), responseType: GetNetworkTimestampResponse.self, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, using: dependencies ) .send(using: dependencies) @@ -987,7 +997,8 @@ public final class SnodeAPI { responseType: R.Type, requireAllBatchResponses: Bool = true, retryCount: Int = 0, - timeout: TimeInterval = Network.defaultTimeout, + requestTimeout: TimeInterval = Network.defaultTimeout, + requestAndPathBuildTimeout: TimeInterval? = nil, using dependencies: Dependencies ) throws -> Network.PreparedRequest { return Network.PreparedRequest( @@ -996,7 +1007,8 @@ public final class SnodeAPI { responseType: responseType, requireAllBatchResponses: requireAllBatchResponses, retryCount: retryCount, - timeout: timeout + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout ) .handleEvents( receiveOutput: { _, response in @@ -1140,7 +1152,7 @@ private extension Request { swarmPublicKey: String, requiresLatestNetworkTime: Bool, body: B, - retryCount: Int = SnodeAPI.maxRetryCount + retryCount: Int ) where T == SnodeRequest, Endpoint == SnodeAPI.Endpoint, B: Encodable & UpdatableTimestamp { self = Request( method: .post, diff --git a/SessionSnodeKit/Types/SnodeAPINamespace.swift b/SessionSnodeKit/Types/SnodeAPINamespace.swift index 5718edd832..56a6fdb527 100644 --- a/SessionSnodeKit/Types/SnodeAPINamespace.swift +++ b/SessionSnodeKit/Types/SnodeAPINamespace.swift @@ -68,6 +68,16 @@ public extension SnodeAPI { } } + public var isCurrentUserNamespace: Bool { + switch self { + case .default, .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups: + return true + + case .configClosedGroupInfo, .legacyClosedGroup, .unknown, .all: + return false + } + } + public var isConfigNamespace: Bool { switch self { case .configUserProfile, .configContacts, .configConvoInfoVolatile, .configUserGroups, diff --git a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift b/SessionSnodeKitTests/Models/SnodeRequestSpec.swift index 8890d9d936..f4a5df89df 100644 --- a/SessionSnodeKitTests/Models/SnodeRequestSpec.swift +++ b/SessionSnodeKitTests/Models/SnodeRequestSpec.swift @@ -40,7 +40,7 @@ class SnodeRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) diff --git a/SessionUtilitiesKit/Database/Storage.swift b/SessionUtilitiesKit/Database/Storage.swift index 68792df55f..82352e7be5 100644 --- a/SessionUtilitiesKit/Database/Storage.swift +++ b/SessionUtilitiesKit/Database/Storage.swift @@ -91,7 +91,7 @@ open class Storage { /// **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() + var tmpKeySpec: Data = try getOrGenerateDatabaseKeySpec() tmpKeySpec.resetBytes(in: 0.. Data { + @discardableResult private func getOrGenerateDatabaseKeySpec() throws -> Data { do { - var keySpec: Data = try getDatabaseCipherKeySpec() + var keySpec: Data = try Storage.getDatabaseCipherKeySpec() defer { keySpec.resetBytes(in: 0.. (dbPath: String, keyPath: String) { - var keySpec: Data = try Storage.getOrGenerateDatabaseKeySpec() + var keySpec: Data = try getOrGenerateDatabaseKeySpec() defer { keySpec.resetBytes(in: 0..: TransactionObserver where let updatedItems: [T] = { do { return try dataQuery(targetRowIds).fetchAll(db) } catch { - SNLog("[PagedDatabaseObserver] Error fetching data during change: \(error)") + // If the database is suspended then don't bother logging (as we already know why) + if !Storage.shared.isSuspended { + Log.error("[PagedDatabaseObserver] Error fetching data during change: \(error)") + } + return [] } }() diff --git a/SessionUtilitiesKit/General/Logging.swift b/SessionUtilitiesKit/General/Logging.swift index b936ec0e63..eb08e5f2c0 100644 --- a/SessionUtilitiesKit/General/Logging.swift +++ b/SessionUtilitiesKit/General/Logging.swift @@ -25,19 +25,42 @@ public enum Log { case error case critical case off + + case `default` } - public struct Category: ExpressibleByStringLiteral, ExpressibleByExtendedGraphemeClusterLiteral, ExpressibleByUnicodeScalarLiteral { - public typealias StringLiteralType = String + public struct Category: Hashable { + public let rawValue: String + fileprivate let customPrefix: String + public let defaultLevel: Log.Level + + fileprivate static let identifierPrefix: String = "logLevel-" + fileprivate var identifier: String { "\(Category.identifierPrefix)\(rawValue)" } + + private init(rawValue: String, customPrefix: String, defaultLevel: Log.Level) { + self.rawValue = rawValue + self.customPrefix = customPrefix + self.defaultLevel = defaultLevel + + AllLoggingCategories.register(category: self) + } - fileprivate let rawValue: String + fileprivate init?(identifier: String) { + guard identifier.hasPrefix(Category.identifierPrefix) else { return nil } + + self.init( + rawValue: identifier.substring(from: Category.identifierPrefix.count), + customPrefix: "", + defaultLevel: .default + ) + } - public init(stringLiteral value: String) { - self.rawValue = value + public init(rawValue: String, customPrefix: String = "") { + self.init(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: .default) } - public init(unicodeScalarLiteral value: Character) { - self.rawValue = String(value) + @discardableResult public static func create(_ rawValue: String, customPrefix: String = "", defaultLevel: Log.Level) -> Log.Category { + return Log.Category(rawValue: rawValue, customPrefix: customPrefix, defaultLevel: defaultLevel) } } @@ -446,7 +469,7 @@ public class Logger { } } catch { - self?.completeResumeLogging(error: "Unable to write extension logs to current log file") + self?.completeResumeLogging(error: "Unable to write extension logs to current log file due to error: \(error)") return } @@ -501,7 +524,7 @@ public class Logger { (DispatchQueue.isDBWriteQueue ? "DBWrite" : nil) ] .compactMap { $0 } - .appending(contentsOf: categories.map { $0.rawValue }) + .appending(contentsOf: categories.map { "\($0.customPrefix)\($0.rawValue)" }) .joined(separator: ", ") return "[\(prefixes)] " @@ -516,7 +539,7 @@ public class Logger { .trimmingCharacters(in: .whitespacesAndNewlines) switch level { - case .off: return + case .off, .default: return case .verbose: DDLogVerbose("💙 \(logMessage)", file: file, function: function, line: line) case .debug: DDLogDebug("💚 \(logMessage)", file: file, function: function, line: line) case .info: DDLogInfo("💛 \(logMessage)", file: file, function: function, line: line) @@ -525,7 +548,7 @@ public class Logger { case .critical: DDLogError("🔥 \(logMessage)", file: file, function: function, line: line) } - let mainCategory: String = (categories.first?.rawValue ?? "[General]") + let mainCategory: String = (categories.first?.rawValue ?? "General") var systemLogger: SystemLoggerType? = systemLoggers.wrappedValue[mainCategory] if systemLogger == nil { @@ -577,7 +600,7 @@ private class SystemLogger: SystemLoggerType { public func log(_ level: Log.Level, _ log: String) { switch level { - case .off: return + case .off, .default: return case .verbose: logger.trace("\(log)") case .debug: logger.debug("\(log)") case .info: logger.info("\(log)") @@ -623,3 +646,34 @@ private extension DispatchQueue { public func SNLog(_ message: String, forceNSLog: Bool = false) { Log.info(message) } + +// MARK: - AllLoggingCategories + +public struct AllLoggingCategories { + public static var defaultLevels: [Log.Category: Log.Level] { + return AllLoggingCategories.registeredCategoryDefaults.wrappedValue + .reduce(into: [:]) { result, cat in result[cat] = cat.defaultLevel } + } + private static let registeredCategoryDefaults: Atomic> = Atomic([]) + + // MARK: - Initialization + + public let rawValue: Int + + public init(rawValue: Int) { + self.rawValue = -1 // `0` is a protected value so can't use it + } + + fileprivate static func register(category: Log.Category) { + guard + !registeredCategoryDefaults.wrappedValue.contains(where: { cat in + /// **Note:** We only want to use the `rawValue` to distinguish between logging categories + /// as the `defaultLevel` can change via the dev settings and any additional metadata could + /// be file/class specific + category.rawValue == cat.rawValue + }) + else { return } + + registeredCategoryDefaults.mutate { $0.insert(category) } + } +} diff --git a/SessionUtilitiesKit/LibSession/LibSession.swift b/SessionUtilitiesKit/LibSession/LibSession.swift index 585fedee60..950b20fc80 100644 --- a/SessionUtilitiesKit/LibSession/LibSession.swift +++ b/SessionUtilitiesKit/LibSession/LibSession.swift @@ -8,12 +8,6 @@ import SessionUtil // MARK: - LibSession public enum LibSession { - private static let logLevels: [LogCategory: LOG_LEVEL] = [ - .config: LOG_LEVEL_INFO, - .network: LOG_LEVEL_INFO, - .manual: LOG_LEVEL_INFO, - ] - public static var version: String { String(cString: LIBSESSION_UTIL_VERSION_STR) } } @@ -21,72 +15,75 @@ public enum LibSession { extension LibSession { public static func addLogger() { + /// Setup any custom category defaul log levels for libSession + Log.Category.create("config", defaultLevel: .info) + Log.Category.create("network", defaultLevel: .info) + /// Set the default log level first (unless specified we only care about semi-dangerous logs) session_logger_set_level_default(LOG_LEVEL_WARN) /// Then set any explicit category log levels we have - logLevels.forEach { cat, level in - guard let cCat: [CChar] = cat.rawValue.cString(using: .utf8) else { return } + AllLoggingCategories.defaultLevels.forEach { category, level in + guard + let cCat: [CChar] = category.rawValue.cString(using: .utf8), + let cLogLevel: LOG_LEVEL = level.libSession + else { return } - session_logger_set_level(cCat, level) + session_logger_set_level(cCat, cLogLevel) } /// Finally register the actual logger callback session_add_logger_full({ msgPtr, msgLen, catPtr, catLen, lvl in - guard let msg: String = String(pointer: msgPtr, length: msgLen, encoding: .utf8) else { return } + guard + let msg: String = String(pointer: msgPtr, length: msgLen, encoding: .utf8), + let cat: String = String(pointer: catPtr, length: catLen, encoding: .utf8) + else { return } - /// Logs from libSession come through in the format: - /// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}` - /// We want to remove the extra data because it doesn't help the logs - let processedMessage: String = { - let logParts: [String] = msg.components(separatedBy: "] ") - - guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) } + /// Dispatch to another thread so we don't block thread triggering the log + DispatchQueue.global(qos: .background).async { + /// Logs from libSession come through in the format: + /// `[yyyy-MM-dd hh:mm:ss] [+{lifetime}s] [{cat}:{lvl}|log.hpp:{line}] {message}` + /// We want to remove the extra data because it doesn't help the logs + let processedMessage: String = { + let logParts: [String] = msg.components(separatedBy: "] ") + + guard logParts.count == 4 else { return msg.trimmingCharacters(in: .whitespacesAndNewlines) } + + let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines) + + return "\(logParts[1])] \(message)" + }() - let message: String = String(logParts[3]).trimmingCharacters(in: .whitespacesAndNewlines) - - return "\(logParts[1])] \(message)" - }() - - Log.custom(Log.Level(lvl), [LogCategory(catPtr, catLen).logCat], processedMessage) + Log.custom( + Log.Level(lvl), + [Log.Category(rawValue: cat, customPrefix: "libSession:")], + processedMessage + ) + } }) } public static func clearLoggers() { session_clear_loggers() } - - // MARK: - Internal - - fileprivate enum LogCategory: String { - case libSession - case config - case network - case quic - case manual - - var logCat: Log.Category { - switch self { - case .libSession: return "libSession" - case .config: return "libSession:config" - case .network: return "libSession:network" - case .quic: return "libSession:quic" - case .manual: return "libSession:manual" - } - } - - init(_ catPtr: UnsafePointer?, _ catLen: Int) { - switch String(pointer: catPtr, length: catLen, encoding: .utf8).map({ LogCategory(rawValue: $0) }) { - case .some(let cat): self = cat - case .none: self = .libSession - } - } - } } // MARK: - Convenience fileprivate extension Log.Level { + var libSession: LOG_LEVEL? { + switch self { + case .verbose: return LOG_LEVEL_TRACE + case .debug: return LOG_LEVEL_DEBUG + case .info: return LOG_LEVEL_INFO + case .warn: return LOG_LEVEL_WARN + case .error: return LOG_LEVEL_ERROR + case .critical: return LOG_LEVEL_CRITICAL + case .off: return LOG_LEVEL_OFF + case .default: return nil // It'll use the default value by default so just return nil + } + } + init(_ level: LOG_LEVEL) { switch level { case LOG_LEVEL_TRACE: self = .verbose diff --git a/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift b/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift index 20fa32f4ee..75ed5caa3a 100644 --- a/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift +++ b/SessionUtilitiesKit/LibSession/Utilities/TypeConversion+Utilities.swift @@ -1,4 +1,6 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. +// +// stringlint:disable import Foundation import SessionUtil @@ -15,173 +17,648 @@ public extension String { self = result } +} + +// MARK: - Array + +public extension Array where Element == String { + init?(pointer: UnsafeMutablePointer?>?, count: Int?) { + self.init(pointee: pointer.map { $0.pointee.map { UnsafePointer($0) } }, count: count) + } + + init?(pointer: UnsafeMutablePointer?>?, count: Int?) { + self.init(pointee: pointer.map { $0.pointee }, count: count) + } - init( - libSessionVal: T, - fixedLength: Int? = .none + init( + pointer: UnsafeMutablePointer?>?, + count: Int?, + defaultValue: [String] ) { - guard let fixedLength: Int = fixedLength else { - // Note: The `String(cString:)` function requires that the value is null-terminated - // so add a null-termination character if needed - self = String( - cString: withUnsafeBytes(of: libSessionVal) { [UInt8]($0) } - .nullTerminated() - ) - return - } - - guard - let fixedLengthData: Data = Data( - libSessionVal: libSessionVal, - count: fixedLength, - nullIfEmpty: true - ), - let result: String = String(data: fixedLengthData, encoding: .utf8) - else { - self = "" - return - } - - self = result + self = ([String](pointer: pointer, count: count) ?? defaultValue) } - init?( - libSessionVal: T, - fixedLength: Int? = .none, - nullIfEmpty: Bool + init( + pointer: UnsafeMutablePointer?>?, + count: Int?, + defaultValue: [String] ) { - let result = String(libSessionVal: libSessionVal, fixedLength: fixedLength) - - guard !nullIfEmpty || !result.isEmpty else { return nil } - - self = result + self = ([String](pointer: pointer, count: count) ?? defaultValue) } - func toLibSession() -> T { - let targetSize: Int = MemoryLayout.stride + init?( + pointee: UnsafePointer?, + count: Int? + ) { + guard + let count: Int = count, + let pointee: UnsafePointer = pointee + else { return nil } - // Limit the string to be the destination size - 1 (for the null terminated character), this will - // mean instead of crashing by trying to set a value that is too large, we truncate the value) - let sizeLimitedString: String = String(self.substring(to: min(count, targetSize - 1))) - var dataMatchingDestinationSize: [CChar] = [CChar](repeating: 0, count: targetSize) - dataMatchingDestinationSize.replaceSubrange( - 0.. 0 else { + self = [] + return + } - return dataMatchingDestinationSize.withUnsafeBytes { ptr in - ptr.baseAddress!.assumingMemoryBound(to: T.self).pointee + self = (0..(_ keyPath: KeyPath) -> T + + // String variants + + func get(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> String + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + + // Data variants + + func get(_ keyPath: KeyPath) -> Data + func get(_ keyPath: KeyPath) -> [UInt8] + func getHex(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> Data + func get(_ keyPath: KeyPath) -> [UInt8] + func getHex(_ keyPath: KeyPath) -> String + func get(_ keyPath: KeyPath) -> Data + func get(_ keyPath: KeyPath) -> [UInt8] + func getHex(_ keyPath: KeyPath) -> String + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> Data? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> [UInt8]? + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> Data? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> [UInt8]? + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> Data? + func get(_ keyPath: KeyPath, nullIfEmpty: Bool) -> [UInt8]? + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool) -> String? } -public extension Optional { - func toLibSession() -> T { - switch self { - case .some(let value): return value.toLibSession() - case .none: return "".toLibSession() - } +public extension CAccessible { + // General types + + func get(_ keyPath: KeyPath) -> T { withUnsafePointer(to: self) { $0.get(keyPath) } } + + // String variants + + func get(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.get(keyPath) } } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + + // Data variants + + func get(_ keyPath: KeyPath) -> Data { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> [UInt8] { withUnsafePointer(to: self) { $0.get(keyPath) } } + func getHex(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.getHex(keyPath) } } + func get(_ keyPath: KeyPath) -> Data { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> [UInt8] { withUnsafePointer(to: self) { $0.get(keyPath) } } + func getHex(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.getHex(keyPath) } } + func get(_ keyPath: KeyPath) -> Data { withUnsafePointer(to: self) { $0.get(keyPath) } } + func get(_ keyPath: KeyPath) -> [UInt8] { withUnsafePointer(to: self) { $0.get(keyPath) } } + func getHex(_ keyPath: KeyPath) -> String { withUnsafePointer(to: self) { $0.getHex(keyPath) } } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.getHex(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.getHex(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + withUnsafePointer(to: self) { $0.get(keyPath, nullIfEmpty: nullIfEmpty) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + withUnsafePointer(to: self) { $0.getHex(keyPath, nullIfEmpty: nullIfEmpty) } } } -// MARK: - Data +// MARK: - CMutable -public extension Data { - init(libSessionVal: T, count: Int) { - let result: Data = Swift.withUnsafePointer(to: libSessionVal) { - Data(bytes: $0, count: count) - } - - self = result +public protocol CMutable { + // General types + + mutating func set(_ keyPath: WritableKeyPath, to value: T) + + // String variants + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) + mutating func set(_ keyPath: WritableKeyPath, to value: String?) + mutating func set(_ keyPath: WritableKeyPath, to value: String?) + mutating func set(_ keyPath: WritableKeyPath, to value: String?) + mutating func set(_ keyPath: WritableKeyPath, to value: String?) + + // Data variants + + mutating func set(_ keyPath: WritableKeyPath, to value: T?) + mutating func set(_ keyPath: WritableKeyPath, to value: T?) + mutating func set(_ keyPath: WritableKeyPath, to value: T?) +} + +public extension CMutable { + // General types + + mutating func set(_ keyPath: WritableKeyPath, to value: T) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } } - init?(libSessionVal: T, count: Int, nullIfEmpty: Bool) { - let result: Data = Data(libSessionVal: libSessionVal, count: count) - - // If all of the values are 0 then return the data as null - guard !nullIfEmpty || result.contains(where: { $0 != 0 }) else { return nil } - - self = result + // Data variants + + mutating func set(_ keyPath: WritableKeyPath, to value: T?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } } - func toLibSession() -> T { - let targetSize: Int = MemoryLayout.stride - var dataMatchingDestinationSize: Data = Data(count: targetSize) - dataMatchingDestinationSize.replaceSubrange( - 0..(_ keyPath: WritableKeyPath, to value: T?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + mutating func set(_ keyPath: WritableKeyPath, to value: T?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + // String variants + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } + } + + mutating func set(_ keyPath: WritableKeyPath, to value: String?) { + withUnsafeMutablePointer(to: &self) { $0.set(keyPath, to: value) } } } -public extension Optional { - func toLibSession() -> T { - switch self { - case .some(let value): return value.toLibSession() - case .none: return Data().toLibSession() - } +// MARK: - Pointer Convenience + +public extension UnsafeMutablePointer { + // General types + + func get(_ keyPath: KeyPath) -> T { UnsafePointer(self).get(keyPath) } + + // String variants + + func get(_ keyPath: KeyPath) -> String { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> String { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> String { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> String { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> String { UnsafePointer(self).get(keyPath) } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + + // Data variants + + func get(_ keyPath: KeyPath) -> Data { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> [UInt8] { UnsafePointer(self).get(keyPath) } + func getHex(_ keyPath: KeyPath) -> String { UnsafePointer(self).getHex(keyPath) } + func get(_ keyPath: KeyPath) -> Data { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> [UInt8] { UnsafePointer(self).get(keyPath) } + func getHex(_ keyPath: KeyPath) -> String { UnsafePointer(self).getHex(keyPath) } + func get(_ keyPath: KeyPath) -> Data { UnsafePointer(self).get(keyPath) } + func get(_ keyPath: KeyPath) -> [UInt8] { UnsafePointer(self).get(keyPath) } + func getHex(_ keyPath: KeyPath) -> String { UnsafePointer(self).getHex(keyPath) } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).getHex(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).getHex(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + UnsafePointer(self).get(keyPath, nullIfEmpty: nullIfEmpty) + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + UnsafePointer(self).getHex(keyPath, nullIfEmpty: nullIfEmpty) } } -// MARK: - Array +public extension UnsafeMutablePointer { + // General types + + func set(_ keyPath: WritableKeyPath, to value: T) { pointee[keyPath: keyPath] = value } + + // String variants + + func set(_ keyPath: WritableKeyPath, to value: String?) { setCString(keyPath, value, maxLength: 65) } + func set(_ keyPath: WritableKeyPath, to value: String?) { setCString(keyPath, value, maxLength: 67) } + func set(_ keyPath: WritableKeyPath, to value: String?) { setCString(keyPath, value, maxLength: 101) } + func set(_ keyPath: WritableKeyPath, to value: String?) { setCString(keyPath, value, maxLength: 224) } + func set(_ keyPath: WritableKeyPath, to value: String?) { setCString(keyPath, value, maxLength: 268) } + + // Data variants + + func set(_ keyPath: WritableKeyPath, to value: T?) { + setData(keyPath, value.map { Data($0) }, length: 32) + } + + func set(_ keyPath: WritableKeyPath, to value: T?) { + setData(keyPath, value.map { Data($0) }, length: 64) + } + + func set(_ keyPath: WritableKeyPath, to value: T?) { + setData(keyPath, value.map { Data($0) }, length: 100) + } +} -public extension Array where Element == String { - init?( - pointer: UnsafeMutablePointer?>?, - count: Int? - ) { - guard let count: Int = count else { return nil } - - // If we were given a count but it's 0 then trying to access the pointer could - // crash (as it could be bad memory) so just return an empty array - guard count > 0 else { - self = [] - return - } - guard let pointee: UnsafeMutablePointer = pointer?.pointee else { return nil } - - self = (0..(_ keyPath: KeyPath) -> T { pointee[keyPath: keyPath] } + + // String variants + + func get(_ keyPath: KeyPath) -> String { getCString(keyPath, maxLength: 65) } + func get(_ keyPath: KeyPath) -> String { getCString(keyPath, maxLength: 67) } + func get(_ keyPath: KeyPath) -> String { getCString(keyPath, maxLength: 101) } + func get(_ keyPath: KeyPath) -> String { getCString(keyPath, maxLength: 224) } + func get(_ keyPath: KeyPath) -> String { getCString(keyPath, maxLength: 268) } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getCString(keyPath, maxLength: 65, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getCString(keyPath, maxLength: 67, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getCString(keyPath, maxLength: 101, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getCString(keyPath, maxLength: 224, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getCString(keyPath, maxLength: 268, nullIfEmpty: nullIfEmpty) } - init( - pointer: UnsafeMutablePointer?>?, - count: Int?, - defaultValue: [String] - ) { - self = ([String](pointer: pointer, count: count) ?? defaultValue) + // Data variants + + func get(_ keyPath: KeyPath) -> Data { getData(keyPath, length: 32) } + func get(_ keyPath: KeyPath) -> [UInt8] { Array(getData(keyPath, length: 32)) } + func getHex(_ keyPath: KeyPath) -> String { getData(keyPath, length: 32).toHexString() } + func get(_ keyPath: KeyPath) -> Data { getData(keyPath, length: 64) } + func get(_ keyPath: KeyPath) -> [UInt8] { Array(getData(keyPath, length: 64)) } + func getHex(_ keyPath: KeyPath) -> String { getData(keyPath, length: 64).toHexString() } + func get(_ keyPath: KeyPath) -> Data { getData(keyPath, length: 100) } + func get(_ keyPath: KeyPath) -> [UInt8] { Array(getData(keyPath, length: 100)) } + func getHex(_ keyPath: KeyPath) -> String { getData(keyPath, length: 100).toHexString() } + + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + getData(keyPath, length: 32, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + getData(keyPath, length: 32, nullIfEmpty: nullIfEmpty).map { Array($0) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getData(keyPath, length: 32, nullIfEmpty: nullIfEmpty).map { $0.toHexString() } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + getData(keyPath, length: 64, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + getData(keyPath, length: 64, nullIfEmpty: nullIfEmpty).map { Array($0) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getData(keyPath, length: 64, nullIfEmpty: nullIfEmpty).map { $0.toHexString() } + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> Data? { + getData(keyPath, length: 100, nullIfEmpty: nullIfEmpty) + } + func get(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> [UInt8]? { + getData(keyPath, length: 100, nullIfEmpty: nullIfEmpty).map { Array($0) } + } + func getHex(_ keyPath: KeyPath, nullIfEmpty: Bool = false) -> String? { + getData(keyPath, length: 100, nullIfEmpty: nullIfEmpty).map { $0.toHexString() } } } -public extension Array where Element == CChar { - func nullTerminated() -> [Element] { - guard self.last != CChar(0) else { return self } +// MARK: - Internal Logic + +private extension UnsafeMutablePointer { + private func getData(_ keyPath: KeyPath, length: Int) -> Data { + return UnsafePointer(self).getData(keyPath, length: length) + } + + private func getData(_ keyPath: KeyPath, length: Int, nullIfEmpty: Bool) -> Data? { + return UnsafePointer(self).getData(keyPath, length: length, nullIfEmpty: nullIfEmpty) + } + + private func setData(_ keyPath: WritableKeyPath, _ value: Data?, length: Int) { + if let value: Data = value, value.count > length { + Log.warn("Setting \(keyPath) to data with \(value.count) length, expected: \(length), value will be truncated.") + } - return self.appending(CChar(0)) + var mutableSelf = pointee + withUnsafeMutableBytes(of: &mutableSelf[keyPath: keyPath]) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return } + + let buffer = baseAddress.assumingMemoryBound(to: UInt8.self) + guard let value: Data = value else { + // Zero-fill the data + memset(buffer, 0, length) + return + } + + value.copyBytes(to: buffer, count: min(length, value.count)) + + if value.count < length { + // Zero-fill any remaining bytes + memset(buffer.advanced(by: value.count), 0, length - value.count) + } + } + pointee = mutableSelf + } + + private func getCString(_ keyPath: KeyPath, maxLength: Int) -> String { + return UnsafePointer(self).getCString(keyPath, maxLength: maxLength) + } + + private func getCString(_ keyPath: KeyPath, maxLength: Int, nullIfEmpty: Bool) -> String? { + return UnsafePointer(self).getCString(keyPath, maxLength: maxLength, nullIfEmpty: nullIfEmpty) + } + + private func setCString(_ keyPath: WritableKeyPath, _ value: String?, maxLength: Int) { + var mutableSelf = pointee + withUnsafeMutableBytes(of: &mutableSelf[keyPath: keyPath]) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return } + + let buffer: UnsafeMutablePointer = baseAddress.assumingMemoryBound(to: CChar.self) + guard let value: String = value else { + // Zero-fill the data + memset(buffer, 0, maxLength) + return + } + guard let nullTerminatedString: [CChar] = value.cString(using: .utf8) else { return } + + let copyLength: Int = min(maxLength - 1, nullTerminatedString.count - 1) + strncpy(buffer, nullTerminatedString, copyLength) + buffer[copyLength] = 0 // Ensure null termination + } + pointee = mutableSelf } } -public extension Array where Element == UInt8 { - func nullTerminated() -> [Element] { - guard self.last != UInt8(0) else { return self } - - return self.appending(UInt8(0)) +private extension UnsafePointer { + func getData(_ keyPath: KeyPath, length: Int) -> Data { + let byteArray = pointee[keyPath: keyPath] + return withUnsafeBytes(of: byteArray) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return Data() } + + return Data(bytes: baseAddress, count: length) + } + } + + func getData(_ keyPath: KeyPath, length: Int, nullIfEmpty: Bool) -> Data? { + let byteArray = pointee[keyPath: keyPath] + return withUnsafeBytes(of: byteArray) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return nil } + + let result: Data = Data(bytes: baseAddress, count: length) + + // If all of the values are 0 then return the data as null + guard !nullIfEmpty || result.contains(where: { $0 != 0 }) else { return nil } + + return result + } + } + + func getCString(_ keyPath: KeyPath, maxLength: Int) -> String { + let charArray = pointee[keyPath: keyPath] + return withUnsafeBytes(of: charArray) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return "" } + + let buffer = baseAddress.assumingMemoryBound(to: CChar.self) + return String(cString: buffer) + } + } + + func getCString(_ keyPath: KeyPath, maxLength: Int, nullIfEmpty: Bool) -> String? { + let charArray = pointee[keyPath: keyPath] + return withUnsafeBytes(of: charArray) { rawBufferPointer in + guard let baseAddress = rawBufferPointer.baseAddress else { return nil } + + let buffer = baseAddress.assumingMemoryBound(to: CChar.self) + let result: String = String(cString: buffer) + + guard !nullIfEmpty || !result.isEmpty else { return nil } + + return result + } } } + +// MARK: - Fixed Length Types + +public typealias CUChar32 = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8 +) + +public typealias CUChar64 = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8 +) + +public typealias CUChar100 = ( + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, + UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8 +) + +public typealias CChar65 = ( + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar +) + +public typealias CChar67 = ( + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar +) + +public typealias CChar101 = ( + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar +) + +public typealias CChar224 = ( + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar +) + +public typealias CChar268 = ( + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, + CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar +) diff --git a/SessionUtilitiesKit/Networking/NetworkError.swift b/SessionUtilitiesKit/Networking/NetworkError.swift index 05ed293191..6b3cbf6449 100644 --- a/SessionUtilitiesKit/Networking/NetworkError.swift +++ b/SessionUtilitiesKit/Networking/NetworkError.swift @@ -7,6 +7,7 @@ import Foundation public enum NetworkError: Error, Equatable, CustomStringConvertible { case invalidURL case invalidPreparedRequest + case forbidden case notFound case parsingFailed case invalidResponse @@ -18,7 +19,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible { case gatewayTimeout case badRequest(error: String, rawData: Data?) case requestFailed(error: String, rawData: Data?) - case timeout + case timeout(error: String, rawData: Data?) case suspended case unknown @@ -26,6 +27,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible { switch self { case .invalidURL: return "Invalid URL (NetworkError.invalidURL)." case .invalidPreparedRequest: return "Invalid PreparedRequest provided (NetworkError.invalidPreparedRequest)." + case .forbidden: return "Forbidden (NetworkError.forbidden)." case .notFound: return "Not Found (NetworkError.notFound)." case .parsingFailed: return "Invalid response (NetworkError.parsingFailed)." case .invalidResponse: return "Invalid response (NetworkError.invalidResponse)." @@ -36,7 +38,7 @@ public enum NetworkError: Error, Equatable, CustomStringConvertible { case .serviceUnavailable: return "Service unavailable (NetworkError.serviceUnavailable)." case .gatewayTimeout: return "Gateway timeout (NetworkError.gatewayTimeout)." case .badRequest(let error, _), .requestFailed(let error, _): return error - case .timeout: return "The request timed out (NetworkError.timeout)." + case .timeout(let error, _): return "The request timed out with error: \(error) (NetworkError.timeout)." case .suspended: return "Network requests are suspended (NetworkError.suspended)." case .unknown: return "An unknown error occurred (NetworkError.unknown)." } diff --git a/SessionUtilitiesKit/Networking/PreparedRequest.swift b/SessionUtilitiesKit/Networking/PreparedRequest.swift index aac24022a5..c77502e7bc 100644 --- a/SessionUtilitiesKit/Networking/PreparedRequest.swift +++ b/SessionUtilitiesKit/Networking/PreparedRequest.swift @@ -19,7 +19,8 @@ public extension Network { public let originalType: Decodable.Type public let responseType: R.Type public let retryCount: Int - public let timeout: TimeInterval + public let requestTimeout: TimeInterval + public let requestAndPathBuildTimeout: TimeInterval? public let cachedResponse: CachedResponse? fileprivate let responseConverter: ((ResponseInfoType, Any) throws -> R) public let subscriptionHandler: (() -> Void)? @@ -49,7 +50,8 @@ public extension Network { responseType: R.Type, requireAllBatchResponses: Bool = true, retryCount: Int = 0, - timeout: TimeInterval + requestTimeout: TimeInterval, + requestAndPathBuildTimeout: TimeInterval? = nil ) where R: Decodable { let batchRequests: [Network.BatchRequest.Child]? = (request.body as? BatchRequestChildRetrievable)?.requests let batchEndpoints: [E] = (batchRequests? @@ -68,7 +70,8 @@ public extension Network { self.originalType = R.self self.responseType = responseType self.retryCount = retryCount - self.timeout = timeout + self.requestTimeout = requestTimeout + self.requestAndPathBuildTimeout = requestAndPathBuildTimeout self.cachedResponse = nil // When we are making a batch request we also want to call though any sub request event @@ -246,7 +249,8 @@ public extension Network { originalType: U.Type, responseType: R.Type, retryCount: Int, - timeout: TimeInterval, + requestTimeout: TimeInterval, + requestAndPathBuildTimeout: TimeInterval?, cachedResponse: CachedResponse?, responseConverter: @escaping (ResponseInfoType, Any) throws -> R, subscriptionHandler: (() -> Void)?, @@ -272,7 +276,8 @@ public extension Network { self.originalType = originalType self.responseType = responseType self.retryCount = retryCount - self.timeout = timeout + self.requestTimeout = requestTimeout + self.requestAndPathBuildTimeout = requestAndPathBuildTimeout self.cachedResponse = cachedResponse self.responseConverter = responseConverter self.subscriptionHandler = subscriptionHandler @@ -420,7 +425,8 @@ public extension Network.PreparedRequest { originalType: originalType, responseType: responseType, retryCount: retryCount, - timeout: timeout, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse, responseConverter: responseConverter, subscriptionHandler: subscriptionHandler, @@ -464,7 +470,8 @@ public extension Network.PreparedRequest { originalType: originalType, responseType: O.self, retryCount: retryCount, - timeout: timeout, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse.map { data in (try? responseConverter(data.info, data.convertedData)) .map { convertedData in @@ -573,7 +580,8 @@ public extension Network.PreparedRequest { originalType: originalType, responseType: responseType, retryCount: retryCount, - timeout: timeout, + requestTimeout: requestTimeout, + requestAndPathBuildTimeout: requestAndPathBuildTimeout, cachedResponse: cachedResponse, responseConverter: responseConverter, subscriptionHandler: subscriptionHandler, @@ -610,7 +618,8 @@ public extension Network.PreparedRequest { originalType: R.self, responseType: R.self, retryCount: 0, - timeout: 0, + requestTimeout: 0, + requestAndPathBuildTimeout: nil, cachedResponse: Network.PreparedRequest.CachedResponse( info: Network.ResponseInfo(code: 0, headers: [:]), originalData: cachedResponse, diff --git a/SessionUtilitiesKit/Utilities/MutableIdentifiable.swift b/SessionUtilitiesKit/Utilities/MutableIdentifiable.swift new file mode 100644 index 0000000000..a6d5e8fd9f --- /dev/null +++ b/SessionUtilitiesKit/Utilities/MutableIdentifiable.swift @@ -0,0 +1,7 @@ +// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +public protocol MutableIdentifiable: Identifiable { + mutating func setId(_ id: ID) +} diff --git a/SessionUtilitiesKitTests/LibSession/Utilities/TypeConversionUtilitiesSpec.swift b/SessionUtilitiesKitTests/LibSession/Utilities/TypeConversionUtilitiesSpec.swift new file mode 100644 index 0000000000..3243266d5e --- /dev/null +++ b/SessionUtilitiesKitTests/LibSession/Utilities/TypeConversionUtilitiesSpec.swift @@ -0,0 +1,402 @@ +// Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. + +import Foundation + +import Quick +import Nimble + +@testable import SessionUtilitiesKit + +class TypeConversionUtilitiesSpec: QuickSpec { + override class func spec() { + // MARK: - a String + describe("a String") { + // MARK: -- can contain emoji + it("can contain emoji") { + let original: String = "Hi 👋" + var test: TestClass = TestClass() + test.set(\.testString, to: original) + let result: String? = test.get(\.testString) + + expect(result).to(equal(original)) + } + + // MARK: -- when initialised with a pointer and length + context("when initialised with a pointer and length") { + // MARK: ---- returns null when given a null pointer + it("returns null when given a null pointer") { + let test: [CChar] = [84, 101, 115, 116] + let result = test.withUnsafeBufferPointer { ptr in + String(pointer: nil, length: 5) + } + + expect(result).to(beNil()) + } + + // MARK: ---- returns a truncated string when given an incorrect length + it("returns a truncated string when given an incorrect length") { + let test: [CChar] = [84, 101, 115, 116] + let result = test.withUnsafeBufferPointer { ptr in + String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 2) + } + + expect(result).to(equal("Te")) + } + + // MARK: ---- returns a string when valid + it("returns a string when valid") { + let test: [CChar] = [84, 101, 115, 116] + let result = test.withUnsafeBufferPointer { ptr in + String(pointer: UnsafeRawPointer(ptr.baseAddress), length: 4) + } + + expect(result).to(equal("Test")) + } + } + + // MARK: -- when initialised with a libSession value + context("when initialised with a libSession value") { + // MARK: ---- stores a value correctly + it("stores a value correctly") { + var test: TestClass = TestClass() + test.set(\.testString, to: "Test") + expect(test.testString.0).to(equal(84)) + expect(test.testString.1).to(equal(101)) + expect(test.testString.2).to(equal(115)) + expect(test.testString.3).to(equal(116)) + expect(test.testString.4).to(equal(0)) + } + + // MARK: ---- truncates when too long + it("truncates when too long") { + let chars30: String = "ThisStringIs_30_CharactersLong" + let original: String = "\(chars30)\(chars30)\(chars30)" + var test: TestClass = TestClass() + test.set(\.testString, to: original) + + let values: [CChar] = [ + test.testString.0, test.testString.1, test.testString.2, test.testString.3, + test.testString.4, test.testString.5, test.testString.6, test.testString.7, + test.testString.8, test.testString.9, test.testString.10, test.testString.11, + test.testString.12, test.testString.13, test.testString.14, test.testString.15, + test.testString.16, test.testString.17, test.testString.18, test.testString.19, + test.testString.20, test.testString.21, test.testString.22, test.testString.23, + test.testString.24, test.testString.25, test.testString.26, test.testString.27, + test.testString.28, test.testString.29, test.testString.30, test.testString.31, + test.testString.32, test.testString.33, test.testString.34, test.testString.35, + test.testString.36, test.testString.37, test.testString.38, test.testString.39, + test.testString.40, test.testString.41, test.testString.42, test.testString.43, + test.testString.44, test.testString.45, test.testString.46, test.testString.47, + test.testString.48, test.testString.49, test.testString.50, test.testString.51, + test.testString.52, test.testString.53, test.testString.54, test.testString.55, + test.testString.56, test.testString.57, test.testString.58, test.testString.59, + test.testString.60, test.testString.61, test.testString.62, test.testString.63 + ] + let expectedValue: [CChar] = [ + 84, 104, 105, 115, 83, 116, 114, 105, 110, 103, 73, 115, 95, 51, 48, 95, + 67, 104, 97, 114, 97, 99, 116, 101, 114, 115, 76, 111, 110, 103, 84, 104, + 105, 115, 83, 116, 114, 105, 110, 103, 73, 115, 95, 51, 48, 95, 67, 104, + 97, 114, 97, 99, 116, 101, 114, 115, 76, 111, 110, 103, 84, 104, 105, 115 + ] + expect(values).to(equal(expectedValue)) + expect(test.testString.64).to(equal(0)) // Last character will always be a null termination + } + + // MARK: ------ returns empty when null + context("returns empty when null") { + var test: TestClass = TestClass() + test.set(\.testString, to: nil) + + let values: [CChar] = [ + test.testString.0, test.testString.1, test.testString.2, test.testString.3, + test.testString.4, test.testString.5, test.testString.6, test.testString.7, + test.testString.8, test.testString.9, test.testString.10, test.testString.11, + test.testString.12, test.testString.13, test.testString.14, test.testString.15, + test.testString.16, test.testString.17, test.testString.18, test.testString.19, + test.testString.20, test.testString.21, test.testString.22, test.testString.23, + test.testString.24, test.testString.25, test.testString.26, test.testString.27, + test.testString.28, test.testString.29, test.testString.30, test.testString.31, + test.testString.32, test.testString.33, test.testString.34, test.testString.35, + test.testString.36, test.testString.37, test.testString.38, test.testString.39, + test.testString.40, test.testString.41, test.testString.42, test.testString.43, + test.testString.44, test.testString.45, test.testString.46, test.testString.47, + test.testString.48, test.testString.49, test.testString.50, test.testString.51, + test.testString.52, test.testString.53, test.testString.54, test.testString.55, + test.testString.56, test.testString.57, test.testString.58, test.testString.59, + test.testString.60, test.testString.61, test.testString.62, test.testString.63, + test.testString.64 + ] + + expect(Set(values)).to(equal([0])) // All values should be 0 + } + + // MARK: ---- retrieves a value correctly + it("retrieves a value correctly") { + var test: TestClass = TestClass() + test.set(\.testString, to: "TestT") + let result: String? = test.get(\.testString) + + expect(result).to(equal("TestT")) + } + + // MARK: ---- truncates at the first null termination character + it("truncates at the first null termination character") { + var test: TestClass = TestClass() + test.set(\.testString, to: "TestT") + test.testString.2 = 0 + let result: String? = test.get(\.testString) + + expect(result).to(equal("Te")) + } + + // MARK: ---- returns an empty string getting a non nullable string with only null termination characters + it("returns an empty string getting a non nullable string with only null termination characters") { + var test: TestClass = TestClass() + test.set(\.testString, to: "TestT") + test.testString.0 = 0 + test.testString.1 = 0 + test.testString.2 = 0 + test.testString.3 = 0 + test.testString.4 = 0 + let result: String = test.get(\.testString) + + expect(result).to(equal("")) + } + + // MARK: ---- returns an empty string when null and not set to return null + it("returns an empty string when null and not set to return null") { + var test: TestClass = TestClass() + let result: String? = test.get(\.testString, nullIfEmpty: false) + + expect(result).to(equal("")) + } + + // MARK: ---- returns null when specified and empty + it("returns null when specified and empty") { + var test: TestClass = TestClass() + let result: String? = test.get(\.testString, nullIfEmpty: true) + + expect(result).to(beNil()) + } + + // MARK: ---- defaults the null if empty flag to false + it("defaults the null if empty flag to false") { + var test: TestClass = TestClass() + let result: String? = test.get(\.testString) + + expect(result).to(equal("")) + } + } + } + + // MARK: - Data + describe("Data") { + // MARK: -- when initialised with a libSession value + context("when initialised with a libSession value") { + // MARK: ---- stores a value correctly + it("stores a value correctly") { + var test: TestClass = TestClass() + test.set(\.testData, to: Data([1, 2, 3, 4, 5])) + expect(test.testData.0).to(equal(1)) + expect(test.testData.1).to(equal(2)) + expect(test.testData.2).to(equal(3)) + expect(test.testData.3).to(equal(4)) + expect(test.testData.4).to(equal(5)) + expect(test.testData.5).to(equal(0)) + } + + // MARK: ---- truncates when too long + it("truncates when too long") { + let data20: Data = Data([1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5]) + let original: Data = (data20 + data20 + data20) + var test: TestClass = TestClass() + test.set(\.testData, to: original) + + let values: [UInt8] = [ + test.testData.0, test.testData.1, test.testData.2, test.testData.3, + test.testData.4, test.testData.5, test.testData.6, test.testData.7, + test.testData.8, test.testData.9, test.testData.10, test.testData.11, + test.testData.12, test.testData.13, test.testData.14, test.testData.15, + test.testData.16, test.testData.17, test.testData.18, test.testData.19, + test.testData.20, test.testData.21, test.testData.22, test.testData.23, + test.testData.24, test.testData.25, test.testData.26, test.testData.27, + test.testData.28, test.testData.29, test.testData.30, test.testData.31 + ] + let expectedValue: [UInt8] = [ + 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, + 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2 + ] + expect(values).to(equal(expectedValue)) + } + + // MARK: ---- fills with empty data when too short + it("fills with empty data when too short") { + var test: TestClass = TestClass() + test.set(\.testData, to: Data([1, 2, 3])) + let result: Data = test.get(\.testData) + + expect(result.count).to(equal(32)) + expect(result[0]).to(equal(1)) + expect(result[1]).to(equal(2)) + expect(result[2]).to(equal(3)) + expect(result.filter { $0 != 0 }.count).to(equal(3)) // Only the first 3 values are not zero + } + + // MARK: ------ returns empty when null + context("returns empty when null") { + let original: Data? = nil + var test: TestClass = TestClass() + test.set(\.testData, to: original) + + let values: [UInt8] = [ + test.testData.0, test.testData.1, test.testData.2, test.testData.3, + test.testData.4, test.testData.5, test.testData.6, test.testData.7, + test.testData.8, test.testData.9, test.testData.10, test.testData.11, + test.testData.12, test.testData.13, test.testData.14, test.testData.15, + test.testData.16, test.testData.17, test.testData.18, test.testData.19, + test.testData.20, test.testData.21, test.testData.22, test.testData.23, + test.testData.24, test.testData.25, test.testData.26, test.testData.27, + test.testData.28, test.testData.29, test.testData.30, test.testData.31 + ] + + expect(Set(values)).to(equal([0])) // All values should be 0 + } + + // MARK: ---- retrieves a value correctly + it("retrieves a value correctly") { + var test: TestClass = TestClass() + test.set(\.testData, to: Data([1, 2, 3, 4, 5])) + let result: Data? = test.get(\.testData) + + expect(result?.prefix(5)).to(equal(Data([1, 2, 3, 4, 5]))) + expect(Set((result ?? Data()).suffix(from: 5))).to(equal([0])) + } + + // MARK: ---- returns empty data when null and not set to return null + it("returns empty data when null and not set to return null") { + let test: TestClass = TestClass() + let result: Data? = test.get(\.testData, nullIfEmpty: false) + + expect(result).to(equal(Data(repeating: 0, count: 32))) + } + + // MARK: ---- returns null when specified and empty + it("returns null when specified and empty") { + let test: TestClass = TestClass() + let result: Data? = test.get(\.testData, nullIfEmpty: true) + + expect(result).to(beNil()) + } + + // MARK: ---- defaults the null if empty flag to false + it("defaults the null if empty flag to false") { + let test: TestClass = TestClass() + let result: Data? = test.get(\.testData) + + expect(result).to(equal(Data(repeating: 0, count: 32))) + } + } + } + + // MARK: - an Array + describe("an Array") { + // MARK: -- when initialised with a 2D C array + context("when initialised with a 2D C array") { + // MARK: ---- returns the correct array + it("returns the correct array") { + var test: [CChar] = ( + "Test1".cString(using: .utf8)! + + "Test2".cString(using: .utf8)! + + "Test3AndExtra".cString(using: .utf8)! + ) + let result = test.withUnsafeMutableBufferPointer { ptr in + var mutablePtr = UnsafePointer(ptr.baseAddress) + + return [String](pointer: &mutablePtr, count: 3) + } + + expect(result).to(equal(["Test1", "Test2", "Test3AndExtra"])) + } + + // MARK: ---- returns an empty array if given one + it("returns an empty array if given one") { + var test = [CChar]() + let result = test.withUnsafeMutableBufferPointer { ptr in + var mutablePtr = UnsafePointer(ptr.baseAddress) + + return [String](pointer: &mutablePtr, count: 0) + } + + expect(result).to(equal([])) + } + + // MARK: ---- handles empty strings without issues + it("handles empty strings without issues") { + var test: [CChar] = ( + "Test1".cString(using: .utf8)! + + "".cString(using: .utf8)! + + "Test2".cString(using: .utf8)! + ) + let result = test.withUnsafeMutableBufferPointer { ptr in + var mutablePtr = UnsafePointer(ptr.baseAddress) + + return [String](pointer: &mutablePtr, count: 3) + } + + expect(result).to(equal(["Test1", "", "Test2"])) + } + + // MARK: ---- returns null when given a null pointer + it("returns null when given a null pointer") { + expect([String](pointee: nil, count: 5)).to(beNil()) + } + + // MARK: ---- returns null when given a null count + it("returns null when given a null count") { + var test: [CChar] = "Test1".cString(using: .utf8)! + let result = test.withUnsafeMutableBufferPointer { ptr in + var mutablePtr = UnsafeMutablePointer(ptr.baseAddress) + + return [String](pointer: &mutablePtr, count: nil) + } + + expect(result).to(beNil()) + } + + // MARK: ---- returns the default value if given null values + it("returns the default value if given null values") { + let ptr: UnsafeMutablePointer?>? = nil + expect([String](pointer: ptr, count: 5, defaultValue: ["Test"])) + .to(equal(["Test"])) + } + } + } + } +} + +private extension TypeConversionUtilitiesSpec { + struct TestClass { + var testString: CChar65 + var testData: CUChar32 + + init() { + self.testString = ( + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ) + self.testData = ( + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ) + } + + init(testString: CChar65, testData: CUChar32) { + self.testString = testString + self.testData = testData + } + } +} + +extension TypeConversionUtilitiesSpec.TestClass: CAccessible & CMutable {} diff --git a/SessionUtilitiesKitTests/Networking/BatchRequestSpec.swift b/SessionUtilitiesKitTests/Networking/BatchRequestSpec.swift index 10cf74e905..d581d61c94 100644 --- a/SessionUtilitiesKitTests/Networking/BatchRequestSpec.swift +++ b/SessionUtilitiesKitTests/Networking/BatchRequestSpec.swift @@ -39,7 +39,7 @@ class BatchRequestSpec: QuickSpec { request: httpRequest, urlRequest: try! httpRequest.generateUrlRequest(using: dependencies), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -73,7 +73,7 @@ class BatchRequestSpec: QuickSpec { request: httpRequest, urlRequest: try! httpRequest.generateUrlRequest(using: dependencies), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -104,7 +104,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -133,7 +133,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -162,7 +162,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -195,7 +195,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -225,7 +225,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) @@ -255,7 +255,7 @@ class BatchRequestSpec: QuickSpec { ), urlRequest: URLRequest(url: URL(string: "https://www.oxen.io")!), responseType: NoResponse.self, - timeout: 0 + requestTimeout: 0 ) ] ) diff --git a/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift b/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift index 519ed89261..9d3ad04324 100644 --- a/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift +++ b/SessionUtilitiesKitTests/Networking/PreparedRequestSpec.swift @@ -41,7 +41,7 @@ class PreparedRequestSpec: QuickSpec { request: request, urlRequest: try! request.generateUrlRequest(using: dependencies), responseType: TestType.self, - timeout: 10 + requestTimeout: 10 ) expect(preparedRequest.request.url?.absoluteString).to(equal("testServer/endpoint")) @@ -70,7 +70,7 @@ class PreparedRequestSpec: QuickSpec { request: request, urlRequest: try! request.generateUrlRequest(using: dependencies), responseType: TestType.self, - timeout: 10 + requestTimeout: 10 ) expect(TestEndpoint.excludedSubRequestHeaders).to(equal([HTTPHeader.testHeader])) diff --git a/SignalUtilitiesKit/Utilities/AppSetup.swift b/SignalUtilitiesKit/Utilities/AppSetup.swift index c1bf1829c4..e5227a9680 100644 --- a/SignalUtilitiesKit/Utilities/AppSetup.swift +++ b/SignalUtilitiesKit/Utilities/AppSetup.swift @@ -8,7 +8,8 @@ import SessionUIKit import SessionSnodeKit public enum AppSetup { - private static let hasRun: Atomic = Atomic(false) + private static let _hasRun: Atomic = Atomic(false) + public static var hasRun: Bool { _hasRun.wrappedValue } public static func setupEnvironment( retrySetupIfDatabaseInvalid: Bool = false, @@ -18,13 +19,13 @@ public enum AppSetup { using dependencies: Dependencies ) { // If we've already run the app setup then only continue under certain circumstances - guard !AppSetup.hasRun.wrappedValue else { - let storageIsValid: Bool = Storage.shared.isValid + guard !AppSetup._hasRun.wrappedValue else { + let storageIsValid: Bool = dependencies.storage.isValid switch (retrySetupIfDatabaseInvalid, storageIsValid) { case (true, false): - Storage.reconfigureDatabase() - AppSetup.hasRun.mutate { $0 = false } + dependencies.storage.reconfigureDatabase() + AppSetup._hasRun.mutate { $0 = false } AppSetup.setupEnvironment( retrySetupIfDatabaseInvalid: false, // Don't want to get stuck in a loop appSpecificBlock: appSpecificBlock, @@ -42,7 +43,7 @@ public enum AppSetup { return } - AppSetup.hasRun.mutate { $0 = true } + AppSetup._hasRun.mutate { $0 = true } var backgroundTask: SessionBackgroundTask? = SessionBackgroundTask(label: #function) @@ -91,7 +92,7 @@ public enum AppSetup { ) { var backgroundTask: SessionBackgroundTask? = (backgroundTask ?? SessionBackgroundTask(label: #function)) - Storage.shared.perform( + dependencies.storage.perform( migrationTargets: [ SNUtilitiesKit.self, SNSnodeKit.self, diff --git a/_SharedTestUtilities/GRDBExtensions.swift b/_SharedTestUtilities/GRDBExtensions.swift index 1a84f88b1c..aae01cf3c7 100644 --- a/_SharedTestUtilities/GRDBExtensions.swift +++ b/_SharedTestUtilities/GRDBExtensions.swift @@ -1,14 +1,14 @@ // Copyright © 2023 Rangeproof Pty Ltd. All rights reserved. import Foundation +import GRDB -@testable import GRDB +@testable import SessionUtilitiesKit -public extension MutablePersistableRecord { - /// This is a test method which allows for inserting with a pre-defined id (it triggers the `didInsert` function directly before inserting which - /// is likely to cause problems with other tests if we ever use it for anything other than assigning the `id`) - mutating func insert(_ db: Database, withRowId rowID: Int64) throws { - didInsert(InsertionSuccess(rowID: rowID, rowIDColumn: nil, persistenceContainer: PersistenceContainer())) +public extension MutablePersistableRecord where Self: MutableIdentifiable { + /// This is a test method which allows for inserting with a pre-defined id + mutating func insert(_ db: Database, withRowId rowId: ID) throws { + self.setId(rowId) try insert(db) } }