diff --git a/.github/workflows/issue_closed.yml b/.github/workflows/issue_closed.yml index 822ba28996..d5c0340e7b 100644 --- a/.github/workflows/issue_closed.yml +++ b/.github/workflows/issue_closed.yml @@ -10,16 +10,16 @@ permissions: jobs: cleanup-labels: runs-on: ubuntu-latest - if: ${{ (contains(github.event.issue.labels.*.name, 'pending-response') || contains(github.event.issue.labels.*.name, 'closing soon') || contains(github.event.issue.labels.*.name, 'pending-release')) }} + if: ${{ (contains(github.event.issue.labels.*.name, 'pending-community-response') || contains(github.event.issue.labels.*.name, 'pending-maintainer-response') || contains(github.event.issue.labels.*.name, 'closing soon') || contains(github.event.issue.labels.*.name, 'pending-release')|| contains(github.event.issue.labels.*.name, 'pending-triage')) }} steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - name: remove unnecessary labels after closing shell: bash env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} run: | - gh issue edit $ISSUE_NUMBER --remove-label "closing soon" --remove-label "pending-response" --remove-label "pending-release" + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "closing soon" --remove-label "pending-community-response" --remove-label "pending-maintainer-response" --remove-label "pending-release" --remove-label "pending-triage" comment-visibility-warning: runs-on: ubuntu-latest @@ -30,4 +30,4 @@ jobs: message: | This issue is now closed. Comments on closed issues are hard for our team to see. If you need more assistance, please open a new issue that references this one. - If you wish to keep having a conversation with other community members under this issue feel free to do so. + If you wish to keep having a conversation with other community members under this issue feel free to do so. \ No newline at end of file diff --git a/.github/workflows/issue_comment.yml b/.github/workflows/issue_comment.yml index 55c66ea87e..822ff84a1c 100644 --- a/.github/workflows/issue_comment.yml +++ b/.github/workflows/issue_comment.yml @@ -19,17 +19,27 @@ jobs: shell: bash run: echo $COMMENT | sed "s/\\\n/. /g; s/\\\r//g; s/[^a-zA-Z0-9 &().,:]//g" | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"comment":"{}", "commentUrl":"'$COMMENT_URL'", "user":"'$USER'"}' - remove-pending-response-label: + adjust-labels: runs-on: ubuntu-latest permissions: issues: write - if: ${{ !github.event.issue.pull_request && contains(github.event.issue.labels.*.name, 'pending-response') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} steps: - - uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 #v4.1.1 - - name: remove unnecessary labels after closing + - name: remove pending-community-response when new comment received + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) && !github.event.issue.pull_request }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-community-response" + - name: add pending-maintainer-response when new community comment received + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response" + - name: remove pending-maintainer-response when new owner/member comment received + if: ${{ contains(fromJSON('["MEMBER", "OWNER"]'), github.event.comment.author_association) }} shell: bash - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - ISSUE_NUMBER: ${{ github.event.issue.number }} run: | - gh issue edit $ISSUE_NUMBER --remove-label "pending-response" \ No newline at end of file + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --remove-label "pending-maintainer-response" diff --git a/.github/workflows/issue_opened.yml b/.github/workflows/issue_opened.yml index 7ea510ba0e..9dc25e7ebd 100644 --- a/.github/workflows/issue_opened.yml +++ b/.github/workflows/issue_opened.yml @@ -18,6 +18,26 @@ jobs: shell: bash run: echo $ISSUE | sed 's/[^a-zA-Z0-9 &().,:]//g' | xargs -I {} curl -s POST "$WEBHOOK_URL" -H "Content-Type:application/json" --data '{"issue":"{}", "issueUrl":"'$ISSUE_URL'", "user":"'$USER'"}' + add-issue-opened-labels: + runs-on: ubuntu-latest + permissions: + issues: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} + steps: + - name: Add the pending-triage label + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-triage" + - name: Add the pending-maintainer-response label + if: ${{ !contains(fromJSON('["MEMBER", "OWNER"]'), github.event.issue.author_association) }} + shell: bash + run: | + gh issue edit $ISSUE_NUMBER --repo $REPOSITORY_NAME --add-label "pending-maintainer-response" + + maintainer-opened: runs-on: ubuntu-latest permissions: @@ -29,5 +49,6 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ISSUE_NUMBER: ${{ github.event.issue.number }} + REPOSITORY_NAME: ${{ github.event.repository.full_name }} run: | - gh issue comment $ISSUE_NUMBER --repo aws-amplify/amplify-swift -b "This issue was opened by a maintainer of this repository; updates will be posted here. If you are also experiencing this issue, please comment here with any relevant information so that we're aware and can prioritize accordingly." + gh issue comment $ISSUE_NUMBER --repo $REPOSITORY_NAME -b "This issue was opened by a maintainer of this repository; updates will be posted here. If you are also experiencing this issue, please comment here with any relevant information so that we're aware and can prioritize accordingly." \ No newline at end of file diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Dependency/AWSTranscribeStreamingAdapter.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Dependency/AWSTranscribeStreamingAdapter.swift index ec0cbeb219..5c70b79948 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Dependency/AWSTranscribeStreamingAdapter.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Dependency/AWSTranscribeStreamingAdapter.swift @@ -131,17 +131,17 @@ class AWSTranscribeStreamingAdapter: AWSTranscribeStreamingBehavior { continuation.yield(transcribedPayload) let isPartial = transcribedPayload.transcript?.results?.map(\.isPartial) ?? [] let shouldContinue = isPartial.allSatisfy { $0 } - return shouldContinue + return shouldContinue ? .continueToReceive : .stopAndInvalidateSession } catch { - return true + return .continueToReceive } case .success(.string): - return true + return .continueToReceive case .failure(let error): continuation.finish(throwing: error) - return false + return .stopAndInvalidateSession @unknown default: - return true + return .continueToReceive } } } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift index ffc2a9abac..092532d8e3 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/FaceLivenessSession.swift @@ -17,6 +17,14 @@ public final class FaceLivenessSession: LivenessService { let baseURL: URL var serverEventListeners: [LivenessEventKind.Server: (FaceLivenessSession.SessionConfiguration) -> Void] = [:] var onComplete: (ServerDisconnection) -> Void = { _ in } + var serverDate: Date? + var savedURLForReconnect: URL? + var connectingState: ConnectingState = .normal + + enum ConnectingState { + case normal + case reconnect + } private let livenessServiceDispatchQueue = DispatchQueue( label: "com.amazon.aws.amplify.liveness.service", @@ -35,12 +43,16 @@ public final class FaceLivenessSession: LivenessService { self.websocket = websocket websocket.onMessageReceived { [weak self] result in - self?.receive(result: result) ?? false + self?.receive(result: result) ?? .stopAndInvalidateSession } websocket.onSocketClosed { [weak self] closeCode in self?.onComplete(.unexpectedClosure(closeCode)) } + + websocket.onServerDateReceived { [weak self] serverDate in + self?.serverDate = serverDate + } } public var onServiceException: (FaceLivenessSessionError) -> Void = { _ in } @@ -75,6 +87,7 @@ public final class FaceLivenessSession: LivenessService { guard let url = components?.url else { throw FaceLivenessSessionError.invalidURL } + savedURLForReconnect = url let signedConnectionURL = signer.sign(url: url) websocket.open(url: signedConnectionURL) } @@ -93,17 +106,22 @@ public final class FaceLivenessSession: LivenessService { ] ) - let eventDate = eventDate() + let dateForSigning: Date + if let serverDate = serverDate { + dateForSigning = serverDate + } else { + dateForSigning = eventDate() + } let signedPayload = self.signer.signWithPreviousSignature( payload: encodedPayload, - dateHeader: (key: ":date", value: eventDate) + dateHeader: (key: ":date", value: dateForSigning) ) let encodedEvent = self.eventStreamEncoder.encode( payload: encodedPayload, headers: [ - ":date": .timestamp(eventDate), + ":date": .timestamp(dateForSigning), ":chunk-signature": .data(signedPayload) ] ) @@ -115,7 +133,7 @@ public final class FaceLivenessSession: LivenessService { } } - private func fallbackDecoding(_ message: EventStream.Message) -> Bool { + private func fallbackDecoding(_ message: EventStream.Message) -> WebSocketSession.WebSocketMessageResult { // We only care about two events above. // Just in case the header value changes (it shouldn't) // We'll try to decode each of these events @@ -124,12 +142,12 @@ public final class FaceLivenessSession: LivenessService { self.serverEventListeners[.challenge]?(sessionConfiguration) } else if (try? JSONDecoder().decode(DisconnectEvent.self, from: message.payload)) != nil { onComplete(.disconnectionEvent) - return false + return .stopAndInvalidateSession } - return true + return .continueToReceive } - private func receive(result: Result) -> Bool { + private func receive(result: Result) -> WebSocketSession.WebSocketMessageResult { switch result { case .success(.data(let data)): do { @@ -145,28 +163,41 @@ public final class FaceLivenessSession: LivenessService { ) let sessionConfiguration = sessionConfiguration(from: payload) serverEventListeners[.challenge]?(sessionConfiguration) - return true + return .continueToReceive case .disconnect: // :event-type DisconnectionEvent onComplete(.disconnectionEvent) - return false + return .stopAndInvalidateSession default: - return true + return .continueToReceive } } else if let exceptionType = message.headers.first(where: { $0.name == ":exception-type" }) { let exceptionEvent = LivenessEventKind.Exception(rawValue: exceptionType.value) - onServiceException(.init(event: exceptionEvent)) - return false + Amplify.log.verbose("\(#function): Received exception: \(exceptionEvent)") + guard exceptionEvent == .invalidSignature, + connectingState == .normal, + let savedURLForReconnect = savedURLForReconnect, + let serverDate = serverDate else { + onServiceException(.init(event: exceptionEvent)) + return .stopAndInvalidateSession + } + + connectingState = .reconnect + let signedConnectionURL = signer.sign( + url: savedURLForReconnect, + date: { serverDate } + ) + return .invalidateSessionAndRetry(url: signedConnectionURL) } else { return fallbackDecoding(message) } } catch { - return false + return .stopAndInvalidateSession } case .success: - return true + return .continueToReceive case .failure: - return false + return .stopAndInvalidateSession } } } diff --git a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/WebSocketSession.swift b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/WebSocketSession.swift index c0c96f3c17..f900f3dfc3 100644 --- a/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/WebSocketSession.swift +++ b/AmplifyPlugins/Predictions/AWSPredictionsPlugin/Liveness/Service/WebSocketSession.swift @@ -6,13 +6,15 @@ // import Foundation +import Amplify final class WebSocketSession { private let urlSessionWebSocketDelegate: Delegate private let session: URLSession private var task: URLSessionWebSocketTask? - private var receiveMessage: ((Result) -> Bool)? + private var receiveMessage: ((Result) -> WebSocketMessageResult)? private var onSocketClosed: ((URLSessionWebSocketTask.CloseCode) -> Void)? + private var onServerDateReceived: ((Date?) -> Void)? init() { self.urlSessionWebSocketDelegate = Delegate() @@ -23,7 +25,7 @@ final class WebSocketSession { ) } - func onMessageReceived(_ receive: @escaping (Result) -> Bool) { + func onMessageReceived(_ receive: @escaping (Result) -> WebSocketMessageResult) { self.receiveMessage = receive } @@ -34,25 +36,32 @@ final class WebSocketSession { func onSocketOpened(_ onOpen: @escaping () -> Void) { urlSessionWebSocketDelegate.onOpen = onOpen } + + func onServerDateReceived(_ onServerDateReceived: @escaping (Date?) -> Void) { + urlSessionWebSocketDelegate.onServerDateReceived = onServerDateReceived + } - func receive(shouldContinue: Bool) { - guard shouldContinue else { + func receive(result: WebSocketMessageResult) { + switch result { + case .continueToReceive: + task?.receive(completionHandler: { [weak self] result in + if let webSocketResult = self?.receiveMessage?(result) { + self?.receive(result: webSocketResult) + } + }) + case .stopAndInvalidateSession: + session.finishTasksAndInvalidate() + case .invalidateSessionAndRetry(let url): session.finishTasksAndInvalidate() - return + open(url: url) } - - task?.receive(completionHandler: { [weak self] result in - if let shouldContinue = self?.receiveMessage?(result) { - self?.receive(shouldContinue: shouldContinue) - } - }) } func open(url: URL) { var request = URLRequest(url: url) request.setValue("no-store", forHTTPHeaderField: "Cache-Control") task = session.webSocketTask(with: request) - receive(shouldContinue: true) + receive(result: .continueToReceive) task?.resume() } @@ -77,10 +86,12 @@ final class WebSocketSession { ) } - final class Delegate: NSObject, URLSessionWebSocketDelegate { + final class Delegate: NSObject, URLSessionWebSocketDelegate, URLSessionTaskDelegate { var onClose: (URLSessionWebSocketTask.CloseCode) -> Void = { _ in } var onOpen: () -> Void = {} + var onServerDateReceived: (Date?) -> Void = { _ in } + // MARK: - URLSessionWebSocketDelegate methods func urlSession( _ session: URLSession, webSocketTask: URLSessionWebSocketTask, @@ -97,5 +108,34 @@ final class WebSocketSession { ) { onClose(closeCode) } + + // MARK: - URLSessionTaskDelegate methods + func urlSession(_ session: URLSession, + task: URLSessionTask, + didFinishCollecting metrics: URLSessionTaskMetrics + ) { + guard let httpResponse = metrics.transactionMetrics.first?.response as? HTTPURLResponse, + let dateString = httpResponse.value(forHTTPHeaderField: "Date") else { + Amplify.log.verbose("\(#function): Couldn't find Date header in URLSession metrics") + onServerDateReceived(nil) + return + } + + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss z" + guard let serverDate = dateFormatter.date(from: dateString) else { + Amplify.log.verbose("\(#function): Error parsing Date header in expected format") + onServerDateReceived(nil) + return + } + + onServerDateReceived(serverDate) + } + } + + enum WebSocketMessageResult { + case continueToReceive + case stopAndInvalidateSession + case invalidateSessionAndRetry(url: URL) } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e64584cec..4ef5b8d73a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,10 +4,6 @@ ### Features -- **api**: add support for GraphQL filter attributeExists (#3484) - -### Bug Fixes - - **api**: propagate connectionLost error from websocket client to sync engine (#3800) ## 2.36.0 (2024-07-18) diff --git a/Gemfile b/Gemfile index 7943313c25..3b0fac1afc 100644 --- a/Gemfile +++ b/Gemfile @@ -4,6 +4,6 @@ source 'https://rubygems.org' gem 'xcpretty', '0.3.0' gem 'fastlane', '2.205.1' -gem 'jazzy', '0.14.2' +gem 'jazzy', '0.15.1' eval_gemfile('fastlane/Pluginfile') diff --git a/Gemfile.lock b/Gemfile.lock index c00707a60a..d949113527 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,13 +13,18 @@ GEM base64 nkf rexml - activesupport (7.0.7.2) + activesupport (7.1.3.4) + base64 + bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) + connection_pool (>= 2.2.5) + drb i18n (>= 1.6, < 2) minitest (>= 5.1) + mutex_m tzinfo (~> 2.0) - addressable (2.8.1) - public_suffix (>= 2.0.2, < 6.0) + addressable (2.8.7) + public_suffix (>= 2.0.2, < 7.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) @@ -43,13 +48,14 @@ GEM aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) base64 (0.2.0) + bigdecimal (3.1.8) claide (1.1.0) - cocoapods (1.12.0) + cocoapods (1.15.2) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.12.0) + cocoapods-core (= 1.15.2) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 1.6.0, < 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -61,8 +67,8 @@ GEM molinillo (~> 0.8.0) nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) - xcodeproj (>= 1.21.0, < 2.0) - cocoapods-core (1.12.0) + xcodeproj (>= 1.23.0, < 2.0) + cocoapods-core (1.15.2) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -73,7 +79,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (1.6.3) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -85,13 +91,15 @@ GEM colored2 (3.1.2) commander (4.6.0) highline (~> 2.0.0) - concurrent-ruby (1.2.2) + concurrent-ruby (1.3.4) + connection_pool (2.4.1) declarative (0.0.20) digest-crc (0.6.4) rake (>= 12.0.0, < 14.0.0) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.8.1) + drb (2.2.1) emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) @@ -165,7 +173,7 @@ GEM xcodeproj (>= 1.13.0, < 2.0.0) xcpretty (~> 0.3.0) xcpretty-travis-formatter (>= 0.0.3) - ffi (1.15.5) + ffi (1.17.0) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) @@ -211,15 +219,15 @@ GEM http-cookie (1.0.5) domain_name (~> 0.5) httpclient (2.8.3) - i18n (1.14.1) + i18n (1.14.5) concurrent-ruby (~> 1.0) - jazzy (0.14.2) + jazzy (0.15.1) cocoapods (~> 1.5) mustache (~> 1.1) open4 (~> 1.3) redcarpet (~> 3.4) - rexml (~> 3.2) - rouge (>= 2.0.6, < 4.0) + rexml (>= 3.2.7, < 4.0) + rouge (>= 2.0.6, < 5.0) sassc (~> 2.1) sqlite3 (~> 1.3) xcinvoke (~> 0.3.0) @@ -230,12 +238,13 @@ GEM memoist (0.16.2) mini_magick (4.12.0) mini_mime (1.1.2) - mini_portile2 (2.8.1) - minitest (5.19.0) + mini_portile2 (2.8.7) + minitest (5.25.1) molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.0.0) mustache (1.1.1) + mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) @@ -253,7 +262,7 @@ GEM trailblazer-option (>= 0.1.1, < 0.2.0) uber (< 0.2.0) retriable (3.1.2) - rexml (3.3.4) + rexml (3.3.5) strscan rouge (2.0.7) ruby-macho (2.5.1) @@ -270,7 +279,7 @@ GEM simctl (1.6.10) CFPropertyList naturally - sqlite3 (1.6.0) + sqlite3 (1.7.3) mini_portile2 (~> 2.8.0) strscan (3.1.0) terminal-notifier (2.0.0) @@ -281,7 +290,7 @@ GEM tty-screen (0.8.1) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) @@ -312,7 +321,7 @@ PLATFORMS DEPENDENCIES fastlane (= 2.205.1) fastlane-plugin-release_actions! - jazzy (= 0.14.2) + jazzy (= 0.15.1) xcpretty (= 0.3.0) BUNDLED WITH