// // APIService+Follow.swift // Mastodon // // Created by MainasuK Cirno on 2021-4-2. // import UIKit import Combine import CoreData import CoreDataStack import CommonOSLog import MastodonSDK extension APIService { private struct MastodonFollowContext { let sourceUserID: MastodonUser.ID let targetUserID: MastodonUser.ID let isFollowing: Bool let isPending: Bool let needsUnfollow: Bool } /// Toggle friendship between target MastodonUser and current MastodonUser /// /// Following / Following pending <-> Unfollow /// /// - Parameters: /// - mastodonUser: target MastodonUser /// - activeMastodonAuthenticationBox: `AuthenticationService.MastodonAuthenticationBox` /// - Returns: publisher for `Relationship` func toggleFollow( user: ManagedObjectRecord, authenticationBox: MastodonAuthenticationBox ) async throws -> Mastodon.Response.Content { let logger = Logger(subsystem: "APIService", category: "Follow") let managedObjectContext = backgroundManagedObjectContext let _followContext: MastodonFollowContext? = try await managedObjectContext.performChanges { guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user else { return nil } guard let user = user.object(in: managedObjectContext) else { return nil } let isFollowing = user.followingBy.contains(me) let isPending = user.followRequestedBy.contains(me) let needsUnfollow = isFollowing || isPending if needsUnfollow { // unfollow user.update(isFollowing: false, by: me) user.update(isFollowRequested: false, by: me) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Local] update user friendship: undo follow") } else { // follow if user.locked { user.update(isFollowing: false, by: me) user.update(isFollowRequested: true, by: me) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Local] update user friendship: pending follow") } else { user.update(isFollowing: true, by: me) user.update(isFollowRequested: false, by: me) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Local] update user friendship: following") } } let context = MastodonFollowContext( sourceUserID: me.id, targetUserID: user.id, isFollowing: isFollowing, isPending: isPending, needsUnfollow: needsUnfollow ) return context } guard let followContext = _followContext else { throw APIError.implicit(.badRequest) } // request follow or unfollow let result: Result, Error> do { let response = try await Mastodon.API.Account.follow( session: session, domain: authenticationBox.domain, accountID: followContext.targetUserID, followQueryType: followContext.needsUnfollow ? .unfollow : .follow(query: .init()), authorization: authenticationBox.userAuthorization ).singleOutput() result = .success(response) } catch { logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Remote] update friendship failure: \(error.localizedDescription)") result = .failure(error) } // update friendship state try await managedObjectContext.performChanges { guard let me = authenticationBox.authenticationRecord.object(in: managedObjectContext)?.user, let user = user.object(in: managedObjectContext) else { return } switch result { case .success(let response): Persistence.MastodonUser.update( mastodonUser: user, context: Persistence.MastodonUser.RelationshipContext( entity: response.value, me: me, networkDate: response.networkDate ) ) let following = response.value.following logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Remote] update user friendship: following \(following)") case .failure: // rollback user.update(isFollowing: followContext.isFollowing, by: me) user.update(isFollowRequested: followContext.isPending, by: me) logger.log(level: .debug, "\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): [Remote] rollback user friendship") } } let response = try result.get() return response } }