From d3c77ee6cf13c0f03f34f8aa377472a2bb6da38c Mon Sep 17 00:00:00 2001 From: CMK Date: Mon, 9 Aug 2021 17:54:11 +0800 Subject: [PATCH] feat: add Idempotency-Key` header for status --- .../Item/ComposeStatusPollItem.swift | 2 +- .../ComposeViewModel+PublishState.swift | 3 + Mastodon/Scene/Compose/ComposeViewModel.swift | 55 ++++++++++++++++++- .../APIService+Status+Publish.swift | 2 + MastodonIntent/SendPostIntentHandler.swift | 3 + .../API/Mastodon+API+Statuses.swift | 6 +- .../Scene/ShareViewModel.swift | 1 + 7 files changed, 69 insertions(+), 3 deletions(-) diff --git a/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift b/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift index 0b4abf23..2e45484c 100644 --- a/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift +++ b/Mastodon/Diffiable/Item/ComposeStatusPollItem.swift @@ -70,7 +70,7 @@ extension ComposeStatusPollItem { hasher.combine(id) } - enum ExpiresOption: Equatable, Hashable, CaseIterable { + enum ExpiresOption: String, Equatable, Hashable, CaseIterable { case thirtyMinutes case oneHour case sixHours diff --git a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift index fd3f5bce..8f739315 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel+PublishState.swift @@ -107,6 +107,8 @@ extension ComposeViewModel.PublishState { return subscriptions }() + let idempotencyKey = viewModel.idempotencyKey.value + publishingSubscription = Publishers.MergeMany(updateMediaQuerySubscriptions) .collect() .flatMap { attachments -> AnyPublisher, Error> in @@ -122,6 +124,7 @@ extension ComposeViewModel.PublishState { ) return viewModel.context.apiService.publishStatus( domain: domain, + idempotencyKey: idempotencyKey, query: query, mastodonAuthenticationBox: mastodonAuthenticationBox ) diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 7a6ed8bd..f91565d3 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -58,7 +58,10 @@ final class ComposeViewModel: NSObject { }() private(set) lazy var publishStateMachinePublisher = CurrentValueSubject(nil) private(set) var publishDate = Date() // update it when enter Publishing state - + + // TODO: group post material into Hashable class + var idempotencyKey = CurrentValueSubject(UUID().uuidString) + // UI & UX let title: CurrentValueSubject let shouldDismiss = CurrentValueSubject(true) @@ -383,6 +386,56 @@ final class ComposeViewModel: NSObject { self.isPollToolbarButtonEnabled.value = !shouldPollDisable }) .store(in: &disposeBag) + + // calculate `Idempotency-Key` + let content = Publishers.CombineLatest3( + composeStatusAttribute.isContentWarningComposing, + composeStatusAttribute.contentWarningContent, + composeStatusAttribute.composeContent + ) + .map { isContentWarningComposing, contentWarningContent, composeContent -> String in + if isContentWarningComposing { + return contentWarningContent + (composeContent ?? "") + } else { + return composeContent ?? "" + } + } + let attachmentIDs = attachmentServices.map { attachments -> String in + let attachmentIDs = attachments.compactMap { $0.attachment.value?.id } + return attachmentIDs.joined(separator: ",") + } + let pollOptionsAndDuration = Publishers.CombineLatest3( + isPollComposing, + pollOptionAttributes, + pollExpiresOptionAttribute.expiresOption + ) + .map { isPollComposing, pollOptionAttributes, expiresOption -> String in + guard isPollComposing else { + return "" + } + + let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",") + return pollOptions + expiresOption.rawValue + } + + Publishers.CombineLatest4( + content, + attachmentIDs, + pollOptionsAndDuration, + selectedStatusVisibility + ) + .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in + var hasher = Hasher() + hasher.combine(content) + hasher.combine(attachmentIDs) + hasher.combine(pollOptionsAndDuration) + hasher.combine(selectedStatusVisibility.visibility.rawValue) + let hashValue = hasher.finalize() + return "\(hashValue)" + } + .assign(to: \.value, on: idempotencyKey) + .store(in: &disposeBag) + } deinit { diff --git a/Mastodon/Service/APIService/APIService+Status+Publish.swift b/Mastodon/Service/APIService/APIService+Status+Publish.swift index 45964602..1bd3363c 100644 --- a/Mastodon/Service/APIService/APIService+Status+Publish.swift +++ b/Mastodon/Service/APIService/APIService+Status+Publish.swift @@ -16,6 +16,7 @@ extension APIService { func publishStatus( domain: String, + idempotencyKey: String?, query: Mastodon.API.Statuses.PublishStatusQuery, mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { @@ -24,6 +25,7 @@ extension APIService { return Mastodon.API.Statuses.publishStatus( session: session, domain: domain, + idempotencyKey: idempotencyKey, query: query, authorization: authorization ) diff --git a/MastodonIntent/SendPostIntentHandler.swift b/MastodonIntent/SendPostIntentHandler.swift index 6d5f739f..75e7049a 100644 --- a/MastodonIntent/SendPostIntentHandler.swift +++ b/MastodonIntent/SendPostIntentHandler.swift @@ -55,9 +55,12 @@ final class SendPostIntentHandler: NSObject, SendPostIntentHandling { spoilerText: nil, visibility: visibility ) + + let idempotencyKey = UUID().uuidString APIService.shared.publishStatus( domain: box.domain, + idempotencyKey: idempotencyKey, query: query, mastodonAuthenticationBox: box ) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift index e6c8b19d..0fa1a0d6 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Statuses.swift @@ -77,14 +77,18 @@ extension Mastodon.API.Statuses { public static func publishStatus( session: URLSession, domain: String, + idempotencyKey: String?, query: PublishStatusQuery, authorization: Mastodon.API.OAuth.Authorization? ) -> AnyPublisher, Error> { - let request = Mastodon.API.post( + var request = Mastodon.API.post( url: publishNewStatusEndpointURL(domain: domain), query: query, authorization: authorization ) + if let idempotencyKey = idempotencyKey { + request.setValue(idempotencyKey, forHTTPHeaderField: "Idempotency-Key") + } return session.dataTaskPublisher(for: request) .tryMap { data, response in let value = try Mastodon.API.decode(type: Mastodon.Entity.Status.self, from: data, response: response) diff --git a/ShareActionExtension/Scene/ShareViewModel.swift b/ShareActionExtension/Scene/ShareViewModel.swift index 62102e66..1aec81bd 100644 --- a/ShareActionExtension/Scene/ShareViewModel.swift +++ b/ShareActionExtension/Scene/ShareViewModel.swift @@ -346,6 +346,7 @@ extension ShareViewModel { ) return APIService.shared.publishStatus( domain: domain, + idempotencyKey: nil, // FIXME: query: query, mastodonAuthenticationBox: mastodonAuthenticationBox )