diff --git a/.drone.jsonnet b/.drone.jsonnet index d07435c81..bd06b45c3 100644 --- a/.drone.jsonnet +++ b/.drone.jsonnet @@ -1,43 +1,35 @@ // This build configuration requires the following to be installed: -// Git, Xcode, XCode Command-line Tools, Cocoapods, Xcodebuild, Xcresultparser, pip +// Git, Xcode, XCode Command-line Tools, Cocoapods, Xcbeautify, Xcresultparser, pip // Log a bunch of version information to make it easier for debugging local version_info = { name: 'Version Information', commands: [ 'git --version', - 'pod --version', - 'xcodebuild -version' + 'LANG=en_US.UTF-8 pod --version', + 'xcodebuild -version', + 'xcbeautify --version' ] }; // Intentionally doing a depth of 2 as libSession-util has it's own submodules (and libLokinet likely will as well) local clone_submodules = { name: 'Clone Submodules', - commands: ['git fetch --tags', 'git submodule update --init --recursive --depth=2 --jobs=4'] + commands: [ 'git submodule update --init --recursive --depth=2 --jobs=4' ] }; // cmake options for static deps mirror local ci_dep_mirror(want_mirror) = (if want_mirror then ' -DLOCAL_MIRROR=https://oxen.rocks/deps ' else ''); -// Output some information about the built tools in case specific combinations break the build -local machine_info = { - name: 'Machine info', - commands: [ - 'xcodebuild -version', - 'LANG=en_US.UTF-8 pod --version' - ] -}; - // Cocoapods // // Unfortunately Cocoapods has a dumb restriction which requires you to use UTF-8 for the // 'LANG' env var so we need to work around the with https://github.com/CocoaPods/CocoaPods/issues/6333 local install_cocoapods = { name: 'Install CocoaPods', - commands: [' - LANG=en_US.UTF-8 pod install || rm -rf ./Pods && LANG=en_US.UTF-8 pod install - '], + commands: [ + 'LANG=en_US.UTF-8 pod install || (rm -rf ./Pods && LANG=en_US.UTF-8 pod install)' + ], depends_on: [ 'Load CocoaPods Cache' ] @@ -89,8 +81,7 @@ local update_cocoapods_cache(depends_on) = { 'touch /Users/drone/.cocoapods_cache.lock', ||| if [[ -d ./Pods ]]; then - rm -rf /Users/drone/.cocoapods_cache - cp -r ./Pods /Users/drone/.cocoapods_cache + rsync -a --delete ./Pods/ /Users/drone/.cocoapods_cache fi |||, 'rm -f /Users/drone/.cocoapods_cache.lock' @@ -104,28 +95,34 @@ local update_cocoapods_cache(depends_on) = { kind: 'pipeline', type: 'exec', name: 'Unit Tests', - platform: { os: 'darwin', arch: 'amd64' }, + platform: { os: 'darwin', arch: 'arm64' }, trigger: { event: { exclude: [ 'push' ] } }, steps: [ version_info, clone_submodules, load_cocoapods_cache, install_cocoapods, + { + name: 'Clean Up Old Test Simulators', + commands: [ + './Scripts/clean-up-old-test-simulators.sh' + ] + }, { name: 'Pre-Boot Test Simulator', commands: [ 'mkdir -p build/artifacts', 'echo "Test-iPhone14-${DRONE_COMMIT:0:9}-${DRONE_BUILD_EVENT}" > ./build/artifacts/device_name', - 'xcrun simctl create "$(cat ./build/artifacts/device_name)" com.apple.CoreSimulator.SimDeviceType.iPhone-14', - 'echo $(xcrun simctl list devices | grep -m 1 $(cat ./build/artifacts/device_name) | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})") > ./build/artifacts/sim_uuid', - 'xcrun simctl boot $(cat ./build/artifacts/sim_uuid)', - 'echo "Pre-booting simulator complete: $(xcrun simctl list | sed "s/^[[:space:]]*//" | grep -o ".*$(cat ./build/artifacts/sim_uuid).*")"', + 'xcrun simctl create "$(<./build/artifacts/device_name)" com.apple.CoreSimulator.SimDeviceType.iPhone-14', + 'echo $(xcrun simctl list devices | grep -m 1 $(<./build/artifacts/device_name) | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})") > ./build/artifacts/sim_uuid', + 'xcrun simctl boot $(<./build/artifacts/sim_uuid)', + 'echo "Pre-booting simulator complete: $(xcrun simctl list | sed "s/^[[:space:]]*//" | grep -o ".*$(<./build/artifacts/sim_uuid).*")"', ] }, { name: 'Build and Run Tests', commands: [ - 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -destination "platform=iOS Simulator,id=$(cat ./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never 2>&1 | xcbeautify --is-ci', + 'NSUnbufferedIO=YES set -o pipefail && xcodebuild test -workspace Session.xcworkspace -scheme Session -derivedDataPath ./build/derivedData -resultBundlePath ./build/artifacts/testResults.xcresult -parallelizeTargets -destination "platform=iOS Simulator,id=$(<./build/artifacts/sim_uuid)" -parallel-testing-enabled NO -test-timeouts-enabled YES -maximum-test-execution-time-allowance 10 -collect-test-diagnostics never 2>&1 | xcbeautify --is-ci', ], depends_on: [ 'Pre-Boot Test Simulator', @@ -135,31 +132,32 @@ local update_cocoapods_cache(depends_on) = { { name: 'Unit Test Summary', commands: [ - 'xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult', + ||| + if [[ -d ./build/artifacts/testResults.xcresult ]]; then + xcresultparser --output-format cli --failed-tests-only ./build/artifacts/testResults.xcresult + else + echo -e "\n\n\n\e[31;1mUnit test results not found\e[0m" + fi + |||, ], depends_on: ['Build and Run Tests'], when: { status: ['failure', 'success'] } }, - { - name: 'Delete Test Simulator', - commands: [ - 'xcrun simctl delete $(cat ./build/artifacts/sim_uuid)' - ], - depends_on: [ - 'Build and Run Tests', - ], - when: { - status: ['failure', 'success'] - } - }, update_cocoapods_cache(['Build and Run Tests']), { name: 'Install Codecov CLI', commands: [ + 'mkdir -p build/artifacts', 'pip3 install codecov-cli', - '~/Library/Python/3.9/bin/codecovcli --version' + 'find $HOME/Library/Python -name codecovcli -print -quit > ./build/artifacts/codecov_path', + ||| + if [[ ! -s ./build/artifacts/codecov_path ]]; then + which codecovcli > ./build/artifacts/codecov_path + fi + |||, + '$(<./build/artifacts/codecov_path) --version' ], }, { @@ -170,9 +168,10 @@ local update_cocoapods_cache(depends_on) = { depends_on: ['Build and Run Tests'] }, { + // No token needed for public repos name: 'Upload coverage to Codecov', commands: [ - '~/Library/Python/3.9/bin/codecovcli upload-process --fail-on-error -f ./build/artifacts/coverage.xml', + '$(<./build/artifacts/codecov_path) upload-process --fail-on-error -f ./build/artifacts/coverage.xml', ], depends_on: [ 'Convert xcresult to xml', @@ -186,7 +185,7 @@ local update_cocoapods_cache(depends_on) = { kind: 'pipeline', type: 'exec', name: 'Check Build Artifact Existence', - platform: { os: 'darwin', arch: 'amd64' }, + platform: { os: 'darwin', arch: 'arm64' }, trigger: { event: { exclude: [ 'push' ] } }, steps: [ { @@ -202,7 +201,7 @@ local update_cocoapods_cache(depends_on) = { kind: 'pipeline', type: 'exec', name: 'Simulator Build', - platform: { os: 'darwin', arch: 'amd64' }, + platform: { os: 'darwin', arch: 'arm64' }, trigger: { event: { exclude: [ 'pull_request' ] } }, steps: [ version_info, diff --git a/Podfile.lock b/Podfile.lock index 3ee64ac5a..c5d96e7e0 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -133,4 +133,4 @@ SPEC CHECKSUMS: PODFILE CHECKSUM: 2c877a533db6e82eaa94407c95be114d80c2f893 -COCOAPODS: 1.15.0 +COCOAPODS: 1.15.2 diff --git a/Scripts/clean-up-old-test-simulators.sh b/Scripts/clean-up-old-test-simulators.sh new file mode 100755 index 000000000..2437a2dcf --- /dev/null +++ b/Scripts/clean-up-old-test-simulators.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# +# Script used with Drone CI to delete any test simulators created by the pipeline that are older than 1 +# hour (the timeout for iOS builds) to ensure we don't waste too much HDD space with test simulators. + +dir="$HOME/Library/Developer/CoreSimulator/Devices" + +# The $HOME directory for a drone pipeline won't be the directory the simulators are stored in so +# check if it exists and if not, fallback to a hard-coded directory +if [[ ! -d $dir ]]; then + dir="/Users/drone/Library/Developer/CoreSimulator/Devices" +fi + +# Plist file +plist="${dir}/device_set.plist" + +if [[ ! -f ${plist} ]]; then + echo -e "\e[31;1mXCode Simulator list not found.\e[0m" + exit 1 +fi + +# Delete any unavailable simulators +xcrun simctl delete unavailable + +# Extract all UUIDs from the device_set +uuids=$(grep -Eo '[A-F0-9]{8}-([A-F0-9]{4}-){3}[A-F0-9]{12}' "$plist") + +# Create empty arrays to store the outputs +uuids_to_keep=() +uuids_to_ignore=() +uuids_to_remove=() + +# Find directories older than an hour +while read -r child_dir; do + # Get the last component of the directory path + dir_name=$(basename "$child_dir") + + # If the folder is not in the uuids array then add it to the uuids_to_remove + # array, otherwise add it to uuids_to_ignore + if ! echo "$uuids" | grep -q "$dir_name"; then + uuids_to_remove+=("$dir_name") + else + uuids_to_ignore+=("$dir_name") + fi +done < <(find "$dir" -maxdepth 1 -type d -not -path "$dir" -mmin +60) + +# Find directories newer than an hour +while read -r child_dir; do + # Get the last component of the directory path + dir_name=$(basename "$child_dir") + + # If the folder is not in the uuids array then add it to the uuids_to_keep array + if ! echo "$uuids" | grep -q "$dir_name"; then + uuids_to_keep+=("$dir_name") + fi +done < <(find "$dir" -maxdepth 1 -type d -not -path "$dir" -mmin -60) + +# Delete the simulators +if [ ${#uuids_to_remove[@]} -eq 0 ]; then + echo -e "\e[31mNo simulators to delete\e[0m" +else + echo -e "\e[31mDeleting ${#uuids_to_remove[@]} old test simulators:\e[0m" + for uuid in "${uuids_to_remove[@]}"; do + echo -e "\e[31m $uuid\e[0m" + # xcrun simctl delete "$uuid" + done +fi + +# Output the pipeline simulators we are leaving +if [ ${#uuids_to_keep[@]} -gt 0 ]; then + echo -e "\e[33m\nIgnoring ${#uuids_to_keep[@]} test simulators (might be in use):\e[0m" + for uuid in "${uuids_to_keep[@]}"; do + echo -e "\e[33m $uuid\e[0m" + done +fi + +# Output the remaining Xcode Simulators +echo -e "\e[32m\nIgnoring ${#uuids_to_ignore[@]} Xcode simulators:\e[0m" +for uuid in "${uuids_to_ignore[@]}"; do + echo -e "\e[32m $uuid\e[0m" +done \ No newline at end of file diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 0431415cf..2b357ab48 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -2810,11 +2810,11 @@ C300A5BB2554AFFB00555489 /* Messages */ = { isa = PBXGroup; children = ( + C300A5C62554B02D00555489 /* Visible Messages */, + C300A5C72554B03900555489 /* Control Messages */, C3C2A74325539EB700C340D1 /* Message.swift */, C352A30825574D8400338F3E /* Message+Destination.swift */, 943C6D812B75E061004ACE64 /* Message+DisappearingMessages.swift */, - C300A5C62554B02D00555489 /* Visible Messages */, - C300A5C72554B03900555489 /* Control Messages */, ); path = Messages; sourceTree = ""; @@ -7151,6 +7151,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SignalUtilitiesKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -7295,6 +7296,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionSnodeKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -7596,6 +7598,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.SessionMessagingKit"; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = NO; SUPPORTS_MACCATALYST = NO; SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7701,6 +7704,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SDKROOT = iphoneos; SKIP_INSTALL = YES; + STRIP_INSTALLED_PRODUCT = YES; SUPPORTS_MACCATALYST = NO; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_INCLUDE_PATHS = "$(inherited) \"${PODS_XCFRAMEWORKS_BUILD_DIR}/Clibsodium\" \"$(TARGET_BUILD_DIR)/libSessionUtil\""; @@ -7925,6 +7929,7 @@ PROVISIONING_PROFILE_SPECIFIER = ""; RUN_CLANG_STATIC_ANALYZER = YES; SDKROOT = iphoneos; + STRIP_INSTALLED_PRODUCT = NO; SWIFT_OBJC_BRIDGING_HEADER = "Session/Meta/Signal-Bridging-Header.h"; SWIFT_OBJC_INTERFACE_HEADER_NAME = "Session-Swift.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -8232,6 +8237,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; COPY_PHASE_STRIP = NO; @@ -8281,7 +8287,7 @@ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = "$(inherited)"; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; diff --git a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme index ec9633662..ea85c66b2 100644 --- a/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme +++ b/Session.xcodeproj/xcshareddata/xcschemes/Session.xcscheme @@ -41,6 +41,7 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "NO" + codeCoverageEnabled = "YES" onlyGenerateCoverageForSpecifiedTargets = "YES"> String { switch destination { - case .contact(let publicKey): + case .contact(let publicKey), .syncMessage(let publicKey): // Extract the 'syncTarget' value if there is one let maybeSyncTarget: String? @@ -661,29 +661,24 @@ public extension Message { internal static func getSpecifiedTTL( message: Message, - isGroupMessage: Bool, - isSyncMessage: Bool + destination: Message.Destination ) -> UInt64 { guard Features.useNewDisappearingMessagesConfig else { return message.ttl } - // Not disappearing messages - guard let expiresInSeconds = message.expiresInSeconds else { return message.ttl } - // Sync message should be read already, it is the same for disappear after read and disappear after sent - guard !isSyncMessage else { return UInt64(expiresInSeconds * 1000) } - - // Disappear after read messages that have not be read - guard let expiresStartedAtMs = message.expiresStartedAtMs else { return message.ttl } - - // Disappear after read messages that have already be read - guard message.sentTimestamp == UInt64(expiresStartedAtMs) else { return message.ttl } - - // Disappear after sent messages with exceptions - switch message { - case is ClosedGroupControlMessage, is UnsendRequest: + switch (destination, message) { + // Disappear after sent messages with exceptions + case (_, is UnsendRequest): return message.ttl + case (.closedGroup, is ClosedGroupControlMessage), (.closedGroup, is ExpirationTimerUpdate): return message.ttl - case is ExpirationTimerUpdate: - return isGroupMessage ? message.ttl : UInt64(expiresInSeconds * 1000) + default: + guard + let expiresInSeconds = message.expiresInSeconds, // Not disappearing messages + expiresInSeconds > 0, // Not disappearing messages (0 == disabled) + let expiresStartedAtMs = message.expiresStartedAtMs, // Unread disappear after read message + message.sentTimestamp == UInt64(expiresStartedAtMs) // Already read disappearing messages + else { return message.ttl } + return UInt64(expiresInSeconds * 1000) } } diff --git a/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift b/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift index 38c499f36..67acc770d 100644 --- a/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift +++ b/SessionMessagingKit/Sending & Receiving/Errors/MessageSenderError.swift @@ -4,7 +4,7 @@ import Foundation -public enum MessageSenderError: LocalizedError, Equatable { +public enum MessageSenderError: Error, CustomStringConvertible, Equatable { case invalidMessage case protoConversionFailed case noUserX25519KeyPair @@ -33,24 +33,28 @@ public enum MessageSenderError: LocalizedError, Equatable { } } - public var errorDescription: String? { + public var description: String { switch self { - case .invalidMessage: return "Invalid message." - case .protoConversionFailed: return "Couldn't convert message to proto." - case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair." - case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair." - case .signingFailed: return "Couldn't sign message." - case .encryptionFailed: return "Couldn't encrypt message." - case .noUsername: return "Missing username." - case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded." - case .blindingFailed: return "Couldn't blind the sender" - case .sendJobTimeout: return "Send job timeout (likely due to path building taking too long)." + case .invalidMessage: return "Invalid message (MessageSenderError.invalidMessage)." + case .protoConversionFailed: return "Couldn't convert message to proto (MessageSenderError.protoConversionFailed)." + case .noUserX25519KeyPair: return "Couldn't find user X25519 key pair (MessageSenderError.noUserX25519KeyPair)." + case .noUserED25519KeyPair: return "Couldn't find user ED25519 key pair (MessageSenderError.noUserED25519KeyPair)." + case .signingFailed: return "Couldn't sign message (MessageSenderError.signingFailed)." + case .encryptionFailed: return "Couldn't encrypt message (MessageSenderError.encryptionFailed)." + case .noUsername: return "Missing username (MessageSenderError.noUsername)." + case .attachmentsNotUploaded: return "Attachments for this message have not been uploaded (MessageSenderError.attachmentsNotUploaded)." + case .blindingFailed: return "Couldn't blind the sender (MessageSenderError.blindingFailed)." + case .sendJobTimeout: return "Send job timeout (likely due to path building taking too long - MessageSenderError.sendJobTimeout)." // Closed groups - case .noThread: return "Couldn't find a thread associated with the given group public key." - case .noKeyPair: return "Couldn't find a private key associated with the given group public key." - case .invalidClosedGroupUpdate: return "Invalid group update." - case .other(let error): return error.localizedDescription + case .noThread: return "Couldn't find a thread associated with the given group public key (MessageSenderError.noThread)." + case .noKeyPair: return "Couldn't find a private key associated with the given group public key (MessageSenderError.noKeyPair)." + case .invalidClosedGroupUpdate: return "Invalid group update (MessageSenderError.invalidClosedGroupUpdate)." + case .other(let error): + switch error { + case is CustomStringConvertible: return "\(error)" + default: return error.localizedDescription + } } } diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift index 5ca3e6333..8b1a35e0e 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender+Convenience.swift @@ -70,7 +70,6 @@ extension MessageSender { destination: destination, threadId: threadId, interactionId: interactionId, - isAlreadySyncMessage: false, using: dependencies ) return @@ -84,8 +83,7 @@ extension MessageSender { interactionId: interactionId, details: MessageSendJob.Details( destination: destination, - message: message, - isSyncMessage: isSyncMessage + message: message ) ), canStartJob: true, @@ -132,6 +130,7 @@ extension MessageSender { let threadId: String = { switch preparedSendData.destination { case .contact(let publicKey): return publicKey + case .syncMessage(let originalRecipientPublicKey): return originalRecipientPublicKey case .closedGroup(let groupPublicKey): return groupPublicKey case .openGroup(let roomToken, let server, _, _, _): return OpenGroup.idFor(roomToken: roomToken, server: server) diff --git a/SessionMessagingKit/Sending & Receiving/MessageSender.swift b/SessionMessagingKit/Sending & Receiving/MessageSender.swift index 430bbdda8..b80882804 100644 --- a/SessionMessagingKit/Sending & Receiving/MessageSender.swift +++ b/SessionMessagingKit/Sending & Receiving/MessageSender.swift @@ -17,7 +17,6 @@ public final class MessageSender { let message: Message? let interactionId: Int64? - let isSyncMessage: Bool? let totalAttachmentsUploaded: Int let snodeMessage: SnodeMessage? @@ -30,7 +29,6 @@ public final class MessageSender { destination: Message.Destination, namespace: SnodeAPI.Namespace?, interactionId: Int64?, - isSyncMessage: Bool?, totalAttachmentsUploaded: Int = 0, snodeMessage: SnodeMessage?, plaintext: Data?, @@ -42,7 +40,6 @@ public final class MessageSender { self.destination = destination self.namespace = namespace self.interactionId = interactionId - self.isSyncMessage = isSyncMessage self.totalAttachmentsUploaded = totalAttachmentsUploaded self.snodeMessage = snodeMessage @@ -56,7 +53,6 @@ public final class MessageSender { destination: Message.Destination, namespace: SnodeAPI.Namespace, interactionId: Int64?, - isSyncMessage: Bool?, snodeMessage: SnodeMessage ) { self.shouldSend = true @@ -65,7 +61,6 @@ public final class MessageSender { self.destination = destination self.namespace = namespace self.interactionId = interactionId - self.isSyncMessage = isSyncMessage self.totalAttachmentsUploaded = 0 self.snodeMessage = snodeMessage @@ -86,7 +81,6 @@ public final class MessageSender { self.destination = destination self.namespace = nil self.interactionId = interactionId - self.isSyncMessage = false self.totalAttachmentsUploaded = 0 self.snodeMessage = nil @@ -107,7 +101,6 @@ public final class MessageSender { self.destination = destination self.namespace = nil self.interactionId = interactionId - self.isSyncMessage = false self.totalAttachmentsUploaded = 0 self.snodeMessage = nil @@ -124,7 +117,6 @@ public final class MessageSender { destination: destination.with(fileIds: fileIds), namespace: namespace, interactionId: interactionId, - isSyncMessage: isSyncMessage, totalAttachmentsUploaded: fileIds.count, snodeMessage: snodeMessage, plaintext: plaintext, @@ -139,7 +131,6 @@ public final class MessageSender { to destination: Message.Destination, namespace: SnodeAPI.Namespace?, interactionId: Int64?, - isSyncMessage: Bool = false, using dependencies: Dependencies = Dependencies() ) throws -> PreparedSendData { // Common logic for all destinations @@ -154,7 +145,7 @@ public final class MessageSender { ) switch destination { - case .contact, .closedGroup: + case .contact, .syncMessage, .closedGroup: return try prepareSendToSnodeDestination( db, message: updatedMessage, @@ -163,7 +154,6 @@ public final class MessageSender { interactionId: interactionId, userPublicKey: currentUserPublicKey, messageSendTimestamp: messageSendTimestamp, - isSyncMessage: isSyncMessage, using: dependencies ) @@ -198,13 +188,13 @@ public final class MessageSender { interactionId: Int64?, userPublicKey: String, messageSendTimestamp: Int64, - isSyncMessage: Bool = false, using dependencies: Dependencies ) throws -> PreparedSendData { message.sender = userPublicKey message.recipient = { switch destination { case .contact(let publicKey): return publicKey + case .syncMessage: return userPublicKey case .closedGroup(let groupPublicKey): return groupPublicKey case .openGroup, .openGroupInbox: preconditionFailure() } @@ -215,6 +205,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .invalidMessage, interactionId: interactionId, using: dependencies @@ -223,25 +214,25 @@ public final class MessageSender { // Attach the user's profile if needed (no need to do so for 'Note to Self' or sync // messages as they will be managed by the user config handling - let isSelfSend: Bool = (message.recipient == userPublicKey) - - if !isSelfSend, !isSyncMessage, var messageWithProfile: MessageWithProfile = message as? MessageWithProfile { - let profile: Profile = Profile.fetchOrCreateCurrentUser(db) - - if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl { - messageWithProfile.profile = VisibleMessage.VMProfile( - displayName: profile.name, - profileKey: profileKey, - profilePictureUrl: profilePictureUrl - ) - } - else { - messageWithProfile.profile = VisibleMessage.VMProfile(displayName: profile.name) - } + switch (destination, (message.recipient == userPublicKey), message as? MessageWithProfile) { + case (.syncMessage, _, _), (_, true, _), (_, _, .none): break + case (_, _, .some(var messageWithProfile)): + let profile: Profile = Profile.fetchOrCreateCurrentUser(db) + + if let profileKey: Data = profile.profileEncryptionKey, let profilePictureUrl: String = profile.profilePictureUrl { + messageWithProfile.profile = VisibleMessage.VMProfile( + displayName: profile.name, + profileKey: profileKey, + profilePictureUrl: profilePictureUrl + ) + } + else { + messageWithProfile.profile = VisibleMessage.VMProfile(displayName: profile.name) + } } // Perform any pre-send actions - handleMessageWillSend(db, message: message, interactionId: interactionId, isSyncMessage: isSyncMessage) + handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId) // Convert it to protobuf let threadId: String = Message.threadId(forMessage: message, destination: destination) @@ -250,6 +241,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .protoConversionFailed, interactionId: interactionId, using: dependencies @@ -267,6 +259,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -280,6 +273,9 @@ public final class MessageSender { case .contact(let publicKey): ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: publicKey, using: dependencies) + case .syncMessage: + ciphertext = try encryptWithSessionProtocol(db, plaintext: plaintext, for: userPublicKey, using: dependencies) + case .closedGroup(let groupPublicKey): guard let encryptionKeyPair: ClosedGroupKeyPair = try? ClosedGroupKeyPair.fetchLatestKeyPair(db, threadId: groupPublicKey) else { throw MessageSenderError.noKeyPair @@ -300,6 +296,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -311,7 +308,7 @@ public final class MessageSender { let senderPublicKey: String switch destination { - case .contact: + case .contact, .syncMessage: kind = .sessionMessage senderPublicKey = "" @@ -336,6 +333,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -350,13 +348,7 @@ public final class MessageSender { data: base64EncodedData, ttl: Message.getSpecifiedTTL( message: message, - isGroupMessage: { - switch destination { - case .closedGroup: return true - default: return false - } - }(), - isSyncMessage: isSyncMessage + destination: destination ), timestampMs: UInt64(messageSendTimestamp) ) @@ -366,7 +358,6 @@ public final class MessageSender { destination: destination, namespace: namespace, interactionId: interactionId, - isSyncMessage: isSyncMessage, snodeMessage: snodeMessage ) } @@ -382,7 +373,7 @@ public final class MessageSender { let threadId: String switch destination { - case .contact, .closedGroup, .openGroupInbox: preconditionFailure() + case .contact, .syncMessage, .closedGroup, .openGroupInbox: preconditionFailure() case .openGroup(let roomToken, let server, let whisperTo, let whisperMods, _): threadId = OpenGroup.idFor(roomToken: roomToken, server: server) message.recipient = [ @@ -439,6 +430,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .invalidMessage, interactionId: interactionId, using: dependencies @@ -455,6 +447,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .noUsername, interactionId: interactionId, using: dependencies @@ -462,13 +455,14 @@ public final class MessageSender { } // Perform any pre-send actions - handleMessageWillSend(db, message: message, interactionId: interactionId) + handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId) // Convert it to protobuf guard let proto = message.toProto(db, threadId: threadId) else { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .protoConversionFailed, interactionId: interactionId, using: dependencies @@ -486,6 +480,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -533,13 +528,14 @@ public final class MessageSender { } // Perform any pre-send actions - handleMessageWillSend(db, message: message, interactionId: interactionId) + handleMessageWillSend(db, message: message, destination: destination, interactionId: interactionId) // Convert it to protobuf guard let proto = message.toProto(db, threadId: recipientBlindedPublicKey) else { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .protoConversionFailed, interactionId: interactionId, using: dependencies @@ -557,6 +553,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -580,6 +577,7 @@ public final class MessageSender { throw MessageSender.handleFailedMessageSend( db, message: message, + destination: destination, with: .other(error), interactionId: interactionId, using: dependencies @@ -628,9 +626,9 @@ public final class MessageSender { MessageSender.handleFailedMessageSend( db, message: message, + destination: data.destination, with: .attachmentsNotUploaded, interactionId: data.interactionId, - isSyncMessage: (data.isSyncMessage == true), using: dependencies ) } @@ -646,7 +644,7 @@ public final class MessageSender { } switch data.destination { - case .contact, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies) + case .contact, .syncMessage, .closedGroup: return sendToSnodeDestination(data: data, using: dependencies) case .openGroup: return sendToOpenGroupDestination(data: data, using: dependencies) case .openGroupInbox: return sendToOpenGroupInbox(data: data, using: dependencies) } @@ -661,7 +659,6 @@ public final class MessageSender { guard let message: Message = data.message, let namespace: SnodeAPI.Namespace = data.namespace, - let isSyncMessage: Bool = data.isSyncMessage, let snodeMessage: SnodeMessage = data.snodeMessage else { return Fail(error: MessageSenderError.invalidMessage) @@ -680,9 +677,10 @@ public final class MessageSender { details: NotifyPushServerJob.Details(message: snodeMessage) ) let shouldNotify: Bool = { - switch updatedMessage { - case is VisibleMessage, is UnsendRequest: return !isSyncMessage - case let callMessage as CallMessage: + switch (updatedMessage, data.destination) { + case (is VisibleMessage, .syncMessage), (is UnsendRequest, .syncMessage): return false + case (is VisibleMessage, _), (is UnsendRequest, _): return true + case (let callMessage as CallMessage, _): // Note: Other 'CallMessage' types are too big to send as push notifications // so only send the 'preOffer' message as a notification switch callMessage.kind { @@ -701,7 +699,6 @@ public final class MessageSender { message: updatedMessage, to: data.destination, interactionId: data.interactionId, - isSyncMessage: isSyncMessage, using: dependencies ) @@ -758,6 +755,7 @@ public final class MessageSender { MessageSender.handleFailedMessageSend( db, message: message, + destination: data.destination, with: .other(error), interactionId: data.interactionId, using: dependencies @@ -829,6 +827,7 @@ public final class MessageSender { MessageSender.handleFailedMessageSend( db, message: message, + destination: data.destination, with: .other(error), interactionId: data.interactionId, using: dependencies @@ -893,6 +892,7 @@ public final class MessageSender { MessageSender.handleFailedMessageSend( db, message: message, + destination: data.destination, with: .other(error), interactionId: data.interactionId, using: dependencies @@ -909,27 +909,33 @@ public final class MessageSender { public static func handleMessageWillSend( _ db: Database, message: Message, - interactionId: Int64?, - isSyncMessage: Bool = false + destination: Message.Destination, + interactionId: Int64? ) { // If the message was a reaction then we don't want to do anything to the original // interaction (which the 'interactionId' is pointing to guard (message as? VisibleMessage)?.reaction == nil else { return } // Mark messages as "sending"/"syncing" if needed (this is for retries) - _ = try? RecipientState - .filter(RecipientState.Columns.interactionId == interactionId) - .filter(isSyncMessage ? - RecipientState.Columns.state == RecipientState.State.failedToSync : - RecipientState.Columns.state == RecipientState.State.failed - ) - .updateAll( - db, - RecipientState.Columns.state.set(to: isSyncMessage ? - RecipientState.State.syncing : - RecipientState.State.sending - ) - ) + switch destination { + case .syncMessage: + _ = try? RecipientState + .filter(RecipientState.Columns.interactionId == interactionId) + .filter(RecipientState.Columns.state == RecipientState.State.failedToSync) + .updateAll( + db, + RecipientState.Columns.state.set(to: RecipientState.State.syncing) + ) + + default: + _ = try? RecipientState + .filter(RecipientState.Columns.interactionId == interactionId) + .filter(RecipientState.Columns.state == RecipientState.State.failed) + .updateAll( + db, + RecipientState.Columns.state.set(to: RecipientState.State.sending) + ) + } } private static func handleSuccessfulMessageSend( @@ -938,7 +944,6 @@ public final class MessageSender { to destination: Message.Destination, interactionId: Int64?, serverTimestampMs: UInt64? = nil, - isSyncMessage: Bool = false, using dependencies: Dependencies ) throws { // If the message was a reaction then we want to update the reaction instead of the original @@ -957,59 +962,61 @@ public final class MessageSender { // Get the visible message if possible if let interaction: Interaction = interaction { // Only store the server hash of a sync message if the message is self send valid - if (message.isSelfSendValid && isSyncMessage || !isSyncMessage) { - try interaction.with( - serverHash: message.serverHash, - // Track the open group server message ID and update server timestamp (use server - // timestamp for open group messages otherwise the quote messages may not be able - // to be found by the timestamp on other devices - timestampMs: (message.openGroupServerMessageId == nil ? - nil : - serverTimestampMs.map { Int64($0) } - ), - openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) } - ).update(db) - - if interaction.isExpiringMessage { - // Start disappearing messages job after a message is successfully sent. - // For DAR and DAS outgoing messages, the expiration start time are the - // same as message sentTimestamp. So do this once, DAR and DAS messages - // should all be covered. - dependencies.jobRunner.upsert( - db, - job: DisappearingMessagesJob.updateNextRunIfNeeded( - db, - interaction: interaction, - startedAtMs: Double(interaction.timestampMs) + switch (message.isSelfSendValid, destination) { + case (false, .syncMessage): break + case (true, .syncMessage), (_, .contact), (_, .closedGroup), (_, .openGroup), (_, .openGroupInbox): + try interaction.with( + serverHash: message.serverHash, + // Track the open group server message ID and update server timestamp (use server + // timestamp for open group messages otherwise the quote messages may not be able + // to be found by the timestamp on other devices + timestampMs: (message.openGroupServerMessageId == nil ? + nil : + serverTimestampMs.map { Int64($0) } ), - canStartJob: true, - using: dependencies - ) + openGroupServerMessageId: message.openGroupServerMessageId.map { Int64($0) } + ).update(db) - if - isSyncMessage, - let startedAtMs: Double = interaction.expiresStartedAtMs, - let expiresInSeconds: TimeInterval = interaction.expiresInSeconds, - let serverHash: String = message.serverHash - { - let expirationTimestampMs: Int64 = Int64(startedAtMs + expiresInSeconds * 1000) - dependencies.jobRunner.add( + if interaction.isExpiringMessage { + // Start disappearing messages job after a message is successfully sent. + // For DAR and DAS outgoing messages, the expiration start time are the + // same as message sentTimestamp. So do this once, DAR and DAS messages + // should all be covered. + dependencies.jobRunner.upsert( db, - job: Job( - variant: .expirationUpdate, - behaviour: .runOnce, - threadId: interaction.threadId, - details: ExpirationUpdateJob.Details( - serverHashes: [serverHash], - expirationTimestampMs: expirationTimestampMs - ) + job: DisappearingMessagesJob.updateNextRunIfNeeded( + db, + interaction: interaction, + startedAtMs: Double(interaction.timestampMs) ), canStartJob: true, using: dependencies ) + + if + case .syncMessage = destination, + let startedAtMs: Double = interaction.expiresStartedAtMs, + let expiresInSeconds: TimeInterval = interaction.expiresInSeconds, + let serverHash: String = message.serverHash + { + let expirationTimestampMs: Int64 = Int64(startedAtMs + expiresInSeconds * 1000) + dependencies.jobRunner.add( + db, + job: Job( + variant: .expirationUpdate, + behaviour: .runOnce, + threadId: interaction.threadId, + details: ExpirationUpdateJob.Details( + serverHashes: [serverHash], + expirationTimestampMs: expirationTimestampMs + ) + ), + canStartJob: true, + using: dependencies + ) + } } } - } // Mark the message as sent try interaction.recipientStates @@ -1038,7 +1045,6 @@ public final class MessageSender { destination: destination, threadId: threadId, interactionId: interactionId, - isAlreadySyncMessage: isSyncMessage, using: dependencies ) } @@ -1046,9 +1052,9 @@ public final class MessageSender { @discardableResult internal static func handleFailedMessageSend( _ db: Database, message: Message, + destination: Message.Destination?, with error: MessageSenderError, interactionId: Int64?, - isSyncMessage: Bool = false, using dependencies: Dependencies ) -> Error { // If the message was a reaction then we don't want to do anything to the original @@ -1060,18 +1066,27 @@ public final class MessageSender { // Note: The 'db' could be either read-only or writeable so we determine // if a change is required, and if so dispatch to a separate queue for the // actual write - let rowIds: [Int64] = (try? RecipientState - .select(Column.rowID) - .filter(RecipientState.Columns.interactionId == interactionId) - .filter(!isSyncMessage ? - RecipientState.Columns.state == RecipientState.State.sending : ( - RecipientState.Columns.state == RecipientState.State.syncing || - RecipientState.Columns.state == RecipientState.State.sent - ) - ) - .asRequest(of: Int64.self) - .fetchAll(db)) - .defaulting(to: []) + let rowIds: [Int64] = (try? { + switch destination { + case .syncMessage: + return RecipientState + .select(Column.rowID) + .filter(RecipientState.Columns.interactionId == interactionId) + .filter( + RecipientState.Columns.state == RecipientState.State.syncing || + RecipientState.Columns.state == RecipientState.State.sent + ) + + default: + return RecipientState + .select(Column.rowID) + .filter(RecipientState.Columns.interactionId == interactionId) + .filter(RecipientState.Columns.state == RecipientState.State.sending) + } + }() + .asRequest(of: Int64.self) + .fetchAll(db)) + .defaulting(to: []) guard !rowIds.isEmpty else { return error } @@ -1079,15 +1094,25 @@ public final class MessageSender { // issue from occuring in some cases DispatchQueue.global(qos: .background).async { dependencies.storage.write { db in - try RecipientState - .filter(rowIds.contains(Column.rowID)) - .updateAll( - db, - RecipientState.Columns.state.set( - to: (isSyncMessage ? RecipientState.State.failedToSync : RecipientState.State.failed) - ), - RecipientState.Columns.mostRecentFailureText.set(to: error.localizedDescription) - ) + switch destination { + case .syncMessage: + try RecipientState + .filter(rowIds.contains(Column.rowID)) + .updateAll( + db, + RecipientState.Columns.state.set(to: RecipientState.State.failedToSync), + RecipientState.Columns.mostRecentFailureText.set(to: "\(error)") + ) + + default: + try RecipientState + .filter(rowIds.contains(Column.rowID)) + .updateAll( + db, + RecipientState.Columns.state.set(to: RecipientState.State.failed), + RecipientState.Columns.mostRecentFailureText.set(to: "\(error)") + ) + } } } @@ -1116,7 +1141,6 @@ public final class MessageSender { destination: Message.Destination, threadId: String?, interactionId: Int64?, - isAlreadySyncMessage: Bool, using dependencies: Dependencies ) { // Sync the message if it's not a sync message, wasn't already sent to the current user and @@ -1125,7 +1149,6 @@ public final class MessageSender { if case .contact(let publicKey) = destination, - !isAlreadySyncMessage, publicKey != currentUserPublicKey, Message.shouldSync(message: message) { @@ -1139,9 +1162,8 @@ public final class MessageSender { threadId: threadId, interactionId: interactionId, details: MessageSendJob.Details( - destination: .contact(publicKey: currentUserPublicKey), - message: message, - isSyncMessage: true + destination: .syncMessage(originalRecipientPublicKey: publicKey), + message: message ) ), canStartJob: true, diff --git a/SessionSnodeKit/Networking/SnodeAPI.swift b/SessionSnodeKit/Networking/SnodeAPI.swift index 45a71652b..c7c6292ee 100644 --- a/SessionSnodeKit/Networking/SnodeAPI.swift +++ b/SessionSnodeKit/Networking/SnodeAPI.swift @@ -1430,15 +1430,27 @@ public extension Publisher where Output == Set { .mapError { $0 } .flatMap(maxPublishers: maxPublishers) { swarm -> AnyPublisher in var remainingSnodes: Set = swarm + var lastError: Error? return Just(()) .setFailureType(to: Error.self) .tryFlatMap(maxPublishers: maxPublishers) { _ -> AnyPublisher in - let snode: Snode = try remainingSnodes.popRandomElement() ?? { throw SnodeAPIError.generic }() + let snode: Snode = try remainingSnodes.popRandomElement() ?? { + throw SnodeAPIError.ranOutOfRandomSnodes(lastError) + }() return try transform(snode) .eraseToAnyPublisher() } + .mapError { error in + // Prevent nesting the 'ranOutOfRandomSnodes' errors + switch error { + case SnodeAPIError.ranOutOfRandomSnodes: break + default: lastError = error + } + + return error + } .retry(retries) .eraseToAnyPublisher() } diff --git a/SessionSnodeKit/Types/OnionRequestAPIError.swift b/SessionSnodeKit/Types/OnionRequestAPIError.swift index 2d18bc7d2..c6d4b0587 100644 --- a/SessionSnodeKit/Types/OnionRequestAPIError.swift +++ b/SessionSnodeKit/Types/OnionRequestAPIError.swift @@ -5,7 +5,7 @@ import Foundation import SessionUtilitiesKit -public enum OnionRequestAPIError: LocalizedError { +public enum OnionRequestAPIError: Error, CustomStringConvertible { case httpRequestFailedAtDestination(statusCode: UInt, data: Data, destination: OnionRequestAPIDestination) case insufficientSnodes case invalidURL @@ -14,25 +14,25 @@ public enum OnionRequestAPIError: LocalizedError { case unsupportedSnodeVersion(String) case invalidRequestInfo - public var errorDescription: String? { + public var description: String { switch self { case .httpRequestFailedAtDestination(let statusCode, let data, let destination): - if statusCode == 429 { return "Rate limited." } + if statusCode == 429 { return "Rate limited (OnionRequestAPIError.httpRequestFailedAtDestination)." } if let processedResponseBodyData: Data = OnionRequestAPI.process(bencodedData: data)?.body, let errorResponse: String = String(data: processedResponseBodyData, encoding: .utf8) { - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." + return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse) (OnionRequestAPIError.httpRequestFailedAtDestination)." } if let errorResponse: String = String(data: data, encoding: .utf8) { - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse)." + return "HTTP request failed at destination (\(destination)) with status code: \(statusCode), error body: \(errorResponse) (OnionRequestAPIError.httpRequestFailedAtDestination)." } - return "HTTP request failed at destination (\(destination)) with status code: \(statusCode)." + return "HTTP request failed at destination (\(destination)) with status code: \(statusCode) (OnionRequestAPIError.httpRequestFailedAtDestination)." - case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path." - case .invalidURL: return "Invalid URL" - case .missingSnodeVersion: return "Missing Service Node version." - case .snodePublicKeySetMissing: return "Missing Service Node public key set." - case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version)." - case .invalidRequestInfo: return "Invalid Request Info" + case .insufficientSnodes: return "Couldn't find enough Service Nodes to build a path (OnionRequestAPIError.insufficientSnodes)." + case .invalidURL: return "Invalid URL (OnionRequestAPIError.invalidURL)." + case .missingSnodeVersion: return "Missing Service Node version (OnionRequestAPIError.missingSnodeVersion)." + case .snodePublicKeySetMissing: return "Missing Service Node public key set (OnionRequestAPIError.snodePublicKeySetMissing)." + case .unsupportedSnodeVersion(let version): return "Unsupported Service Node version: \(version) (OnionRequestAPIError.unsupportedSnodeVersion)." + case .invalidRequestInfo: return "Invalid Request Info (OnionRequestAPIError.invalidRequestInfo)." } } } diff --git a/SessionSnodeKit/Types/SnodeAPIError.swift b/SessionSnodeKit/Types/SnodeAPIError.swift index ef132736b..da3cae60b 100644 --- a/SessionSnodeKit/Types/SnodeAPIError.swift +++ b/SessionSnodeKit/Types/SnodeAPIError.swift @@ -4,7 +4,7 @@ import Foundation -public enum SnodeAPIError: LocalizedError { +public enum SnodeAPIError: Error, CustomStringConvertible { case generic case clockOutOfSync case snodePoolUpdatingFailed @@ -15,29 +15,37 @@ public enum SnodeAPIError: LocalizedError { case invalidIP case emptySnodePool case responseFailedValidation + case ranOutOfRandomSnodes(Error?) // ONS case decryptionFailed case hashingFailed case validationFailed - public var errorDescription: String? { + public var description: String { switch self { - case .generic: return "An error occurred." - case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time." - case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool." - case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network." - case .noKeyPair: return "Missing user key pair." - case .signingFailed: return "Couldn't sign message." - case .signatureVerificationFailed: return "Failed to verify the signature." - case .invalidIP: return "Invalid IP." - case .emptySnodePool: return "Service Node pool is empty." - case .responseFailedValidation: return "Response failed validation." + case .generic: return "An error occurred (SnodeAPIError.generic)." + case .clockOutOfSync: return "Your clock is out of sync with the Service Node network. Please check that your device's clock is set to automatic time (SnodeAPIError.clockOutOfSync)." + case .snodePoolUpdatingFailed: return "Failed to update the Service Node pool (SnodeAPIError.snodePoolUpdatingFailed)." + case .inconsistentSnodePools: return "Received inconsistent Service Node pool information from the Service Node network (SnodeAPIError.inconsistentSnodePools)." + case .noKeyPair: return "Missing user key pair (SnodeAPIError.noKeyPair)." + case .signingFailed: return "Couldn't sign message (SnodeAPIError.signingFailed)." + case .signatureVerificationFailed: return "Failed to verify the signature (SnodeAPIError.signatureVerificationFailed)." + case .invalidIP: return "Invalid IP (SnodeAPIError.invalidIP)." + case .emptySnodePool: return "Service Node pool is empty (SnodeAPIError.emptySnodePool)." + case .responseFailedValidation: return "Response failed validation (SnodeAPIError.responseFailedValidation)." + case .ranOutOfRandomSnodes(let maybeError): + switch maybeError { + case .none: return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(nil))." + case .some(let error): + let errorDesc = "\(error)".trimmingCharacters(in: CharacterSet(["."])) + return "Ran out of random snodes (SnodeAPIError.ranOutOfRandomSnodes(\(errorDesc))." + } // ONS - case .decryptionFailed: return "Couldn't decrypt ONS name." - case .hashingFailed: return "Couldn't compute ONS name hash." - case .validationFailed: return "ONS name validation failed." + case .decryptionFailed: return "Couldn't decrypt ONS name (SnodeAPIError.decryptionFailed)." + case .hashingFailed: return "Couldn't compute ONS name hash (SnodeAPIError.hashingFailed)." + case .validationFailed: return "ONS name validation failed (SnodeAPIError.validationFailed)." } } }