diff --git a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist index 6faa9959a..c08f88276 100644 --- a/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist +++ b/Mastodon.xcodeproj/xcuserdata/mainasuk.xcuserdatad/xcschemes/xcschememanagement.plist @@ -7,12 +7,12 @@ AppShared.xcscheme_^#shared#^_ orderHint - 30 + 32 CoreDataStack.xcscheme_^#shared#^_ orderHint - 34 + 31 Mastodon - ASDK.xcscheme_^#shared#^_ @@ -77,7 +77,7 @@ MastodonIntent.xcscheme_^#shared#^_ orderHint - 32 + 33 MastodonIntents.xcscheme_^#shared#^_ @@ -97,7 +97,7 @@ ShareActionExtension.xcscheme_^#shared#^_ orderHint - 31 + 30 SuppressBuildableAutocreation diff --git a/Mastodon/Scene/Compose/ComposeViewModel.swift b/Mastodon/Scene/Compose/ComposeViewModel.swift index 601aeae04..7a6ed8bdd 100644 --- a/Mastodon/Scene/Compose/ComposeViewModel.swift +++ b/Mastodon/Scene/Compose/ComposeViewModel.swift @@ -338,6 +338,9 @@ final class ComposeViewModel: NSObject { if currentState is MastodonAttachmentService.UploadState.Finish { continue } + if currentState is MastodonAttachmentService.UploadState.Processing { + continue + } if currentState is MastodonAttachmentService.UploadState.Uploading { break } diff --git a/Mastodon/Service/APIService/APIService+Media.swift b/Mastodon/Service/APIService/APIService+Media.swift index d6b1d6c21..0c7822c9d 100644 --- a/Mastodon/Service/APIService/APIService+Media.swift +++ b/Mastodon/Service/APIService/APIService+Media.swift @@ -57,6 +57,25 @@ extension APIService { } +extension APIService { + + func getMedia( + attachmentID: Mastodon.Entity.Attachment.ID, + mastodonAuthenticationBox: MastodonAuthenticationBox + ) -> AnyPublisher, Error> { + let authorization = mastodonAuthenticationBox.userAuthorization + + return Mastodon.API.Media.getMedia( + session: session, + domain: mastodonAuthenticationBox.domain, + attachmentID: attachmentID, + authorization: authorization + ) + .eraseToAnyPublisher() + } + +} + extension APIService { func updateMedia( diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift index 8474ac4dd..8ff076dc1 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService+UploadState.swift @@ -47,7 +47,10 @@ extension MastodonAttachmentService.UploadState { var needsFallback = false override func isValidNextState(_ stateClass: AnyClass) -> Bool { - return stateClass == Fail.self || stateClass == Finish.self || stateClass == Uploading.self + return stateClass == Fail.self + || stateClass == Finish.self + || stateClass == Uploading.self + || stateClass == Processing.self } override func didEnter(from previousState: GKState?) { @@ -96,11 +99,70 @@ extension MastodonAttachmentService.UploadState { } receiveValue: { response in os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: upload attachment %s success: %s", ((#file as NSString).lastPathComponent), #line, #function, response.value.id, response.value.url ?? "") service.attachment.value = response.value + if response.statusCode == 202 { + // check if still processing + stateMachine.enter(Processing.self) + } else { + stateMachine.enter(Finish.self) + } + } + .store(in: &service.disposeBag) + } + } + + class Processing: MastodonAttachmentService.UploadState { + + static let retryLimit = 10 + var retryCount = 0 + + override func isValidNextState(_ stateClass: AnyClass) -> Bool { + return stateClass == Fail.self || stateClass == Finish.self || stateClass == Processing.self + } + + override func didEnter(from previousState: GKState?) { + super.didEnter(from: previousState) + + guard let service = service, let stateMachine = stateMachine else { return } + guard let authenticationBox = service.authenticationBox else { return } + guard let attachment = service.attachment.value else { return } + + retryCount += 1 + guard retryCount < Processing.retryLimit else { + stateMachine.enter(Fail.self) + return + } + + service.context.apiService.getMedia( + attachmentID: attachment.id, + mastodonAuthenticationBox: authenticationBox + ) + .retry(3) + .receive(on: DispatchQueue.main) + .sink { [weak self] completion in + guard let _ = self else { return } + switch completion { + case .failure(let error): + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: get attachment fail: %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription) + service.error.send(error) + stateMachine.enter(Fail.self) + case .finished: + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: get attachment success", ((#file as NSString).lastPathComponent), #line, #function) + break + } + } receiveValue: { [weak self] response in + guard let self = self else { return } + guard let _ = response.value.url else { + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in + os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: processing, retry in 2s", ((#file as NSString).lastPathComponent), #line, #function) + self?.stateMachine?.enter(Processing.self) + } + return + } + stateMachine.enter(Finish.self) } .store(in: &service.disposeBag) } - } class Fail: MastodonAttachmentService.UploadState { diff --git a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift index 2b08b0db0..e42c9bf2e 100644 --- a/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift +++ b/Mastodon/Service/MastodonAttachmentService/MastodonAttachmentService.swift @@ -41,6 +41,7 @@ final class MastodonAttachmentService { let stateMachine = GKStateMachine(states: [ UploadState.Initial(service: self), UploadState.Uploading(service: self), + UploadState.Processing(service: self), UploadState.Fail(service: self), UploadState.Finish(service: self), ]) diff --git a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift index d05cac01a..da77c65a1 100644 --- a/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift +++ b/MastodonSDK/Sources/MastodonSDK/API/Mastodon+API+Media.swift @@ -113,6 +113,49 @@ extension Mastodon.API.Media { } +extension Mastodon.API.Media { + static func getMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL { + Mastodon.API.endpointURL(domain: domain).appendingPathComponent("media").appendingPathComponent(attachmentID) + } + + /// Get media attachment + /// + /// Get an Attachment, before it is attached to a status and posted, but after it is accepted for processing. + /// + /// - Since: 0.0.0 + /// - Version: 3.4.1 + /// # Last Update + /// 2021/8/9 + /// # Reference + /// [Document](https://docs.joinmastodon.org/methods/statuses/media/) + /// - Parameters: + /// - session: `URLSession` + /// - domain: Mastodon instance domain. e.g. "example.com" + /// - mediaID: The ID of attachment + /// - authorization: User token + /// - Returns: `AnyPublisher` contains `Attachment` nested in the response + public static func getMedia( + session: URLSession, + domain: String, + attachmentID: Mastodon.Entity.Attachment.ID, + authorization: Mastodon.API.OAuth.Authorization? + ) -> AnyPublisher, Error> { + var request = Mastodon.API.get( + url: getMediaEndpointURL(domain: domain, attachmentID: attachmentID), + query: nil, + authorization: authorization + ) + request.timeoutInterval = 10 // short timeout for quick retry + return session.dataTaskPublisher(for: request) + .tryMap { data, response in + let value = try Mastodon.API.decode(type: Mastodon.Entity.Attachment.self, from: data, response: response) + return Mastodon.Response.Content(value: value, response: response) + } + .eraseToAnyPublisher() + } +} + + extension Mastodon.API.Media { static func updateMediaEndpointURL(domain: String, attachmentID: Mastodon.Entity.Attachment.ID) -> URL { diff --git a/MastodonSDK/Sources/MastodonSDK/Response/Mastodon+Response+Content.swift b/MastodonSDK/Sources/MastodonSDK/Response/Mastodon+Response+Content.swift index 9c39615f9..db42169d8 100644 --- a/MastodonSDK/Sources/MastodonSDK/Response/Mastodon+Response+Content.swift +++ b/MastodonSDK/Sources/MastodonSDK/Response/Mastodon+Response+Content.swift @@ -14,6 +14,7 @@ extension Mastodon.Response { public let value: T // standard fields + public let statusCode: Int? ///< HTTP Code public let date: Date? // application fields @@ -28,6 +29,8 @@ extension Mastodon.Response { public init(value: T, response: URLResponse) { self.value = value + self.statusCode = (response as? HTTPURLResponse)?.statusCode + self.date = { guard let string = (response as? HTTPURLResponse)?.value(forHTTPHeaderField: "date") else { return nil } return Mastodon.API.httpHeaderDateFormatter.date(from: string) @@ -47,6 +50,7 @@ extension Mastodon.Response { init(value: T, old: Mastodon.Response.Content) { self.value = value + self.statusCode = old.statusCode self.date = old.date self.rateLimit = old.rateLimit self.link = old.link