// // APIService+Poll.swift // Mastodon // // Created by MainasuK Cirno on 2021-3-3. // import Foundation import Combine import CoreData import CoreDataStack import CommonOSLog import DateToolsSwift import MastodonSDK extension APIService { func poll( domain: String, pollID: Mastodon.Entity.Poll.ID, pollObjectID: NSManagedObjectID, mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID return Mastodon.API.Polls.poll( session: session, domain: domain, pollID: pollID, authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in let entity = response.value let managedObjectContext = self.backgroundManagedObjectContext return managedObjectContext.performChanges { let _requestMastodonUser: MastodonUser? = { let request = MastodonUser.sortedFetchRequest request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { return try managedObjectContext.fetch(request).first } catch { assertionFailure(error.localizedDescription) return nil } }() guard let requestMastodonUser = _requestMastodonUser else { assertionFailure() return } guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return } APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) } .setFailureType(to: Error.self) .tryMap { result -> Mastodon.Response.Content in switch result { case .success: return response case .failure(let error): throw error } } .eraseToAnyPublisher() } .eraseToAnyPublisher() } } extension APIService { /// vote local /// # Note /// Not mark the poll voted so that view model could know when to reveal the results func vote( pollObjectID: NSManagedObjectID, mastodonUserObjectID: NSManagedObjectID, choices: [Int] ) -> AnyPublisher { var _targetPollID: Mastodon.Entity.Poll.ID? var isPollExpired = false var didVotedLocal = false let managedObjectContext = backgroundManagedObjectContext return managedObjectContext.performChanges { let poll = managedObjectContext.object(with: pollObjectID) as! Poll let mastodonUser = managedObjectContext.object(with: mastodonUserObjectID) as! MastodonUser _targetPollID = poll.id if let expiresAt = poll.expiresAt, Date().timeIntervalSince(expiresAt) > 0 { isPollExpired = true poll.update(expired: true) return } let options = poll.options.sorted(by: { $0.index.intValue < $1.index.intValue }) let votedOptions = poll.options.filter { option in (option.votedBy ?? Set()).map { $0.id }.contains(mastodonUser.id) } if !poll.multiple, !votedOptions.isEmpty { // if did voted for single poll. Do not allow vote again didVotedLocal = true return } for option in options { let voted = choices.contains(option.index.intValue) option.update(voted: voted, by: mastodonUser) option.didUpdate(at: option.updatedAt) // trigger update without change anything } poll.didUpdate(at: poll.updatedAt) // trigger update without change anything } .tryMap { result in guard !isPollExpired else { throw APIError.explicit(APIError.ErrorReason.voteExpiredPoll) } guard !didVotedLocal else { throw APIError.implicit(APIError.ErrorReason.badRequest) } switch result { case .success: guard let targetPollID = _targetPollID else { throw APIError.implicit(.badRequest) } return targetPollID case .failure(let error): assertionFailure(error.localizedDescription) throw error } } .eraseToAnyPublisher() } /// send vote request to remote func vote( domain: String, pollID: Mastodon.Entity.Poll.ID, pollObjectID: NSManagedObjectID, choices: [Int], mastodonAuthenticationBox: MastodonAuthenticationBox ) -> AnyPublisher, Error> { let authorization = mastodonAuthenticationBox.userAuthorization let requestMastodonUserID = mastodonAuthenticationBox.userID let query = Mastodon.API.Polls.VoteQuery(choices: choices) return Mastodon.API.Polls.vote( session: session, domain: domain, pollID: pollID, query: query, authorization: authorization ) .flatMap { response -> AnyPublisher, Error> in let entity = response.value let managedObjectContext = self.backgroundManagedObjectContext return managedObjectContext.performChanges { let _requestMastodonUser: MastodonUser? = { let request = MastodonUser.sortedFetchRequest request.predicate = MastodonUser.predicate(domain: mastodonAuthenticationBox.domain, id: requestMastodonUserID) request.fetchLimit = 1 request.returnsObjectsAsFaults = false do { return try managedObjectContext.fetch(request).first } catch { assertionFailure(error.localizedDescription) return nil } }() guard let requestMastodonUser = _requestMastodonUser else { assertionFailure() return } guard let poll = managedObjectContext.object(with: pollObjectID) as? Poll else { return } APIService.CoreData.merge(poll: poll, entity: entity, requestMastodonUser: requestMastodonUser, domain: domain, networkDate: response.networkDate) } .setFailureType(to: Error.self) .tryMap { result -> Mastodon.Response.Content in switch result { case .success: return response case .failure(let error): throw error } } .eraseToAnyPublisher() } .eraseToAnyPublisher() } }