Signal-iOS/SignalServiceKit/Jobs/LocalUserLeaveGroupJob.swift
Harry 003cc19e62
Update SignalCoreKit. Use explicit DispatchQueue instead of relying on the implicit type on promise 'on' params
* Use explicit DispatchQueue instead of relying on the implicit type on promise 'on' params

* Update SignalCoreKit
2023-02-07 16:14:39 -08:00

215 lines
7.2 KiB
Swift

//
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
public final class LocalUserLeaveGroupOperation: OWSOperation, DurableOperation {
public typealias JobRecordType = OWSLocalUserLeaveGroupJobRecord
public typealias DurableOperationDelegateType = LocalUserLeaveGroupJobQueue
public let jobRecord: OWSLocalUserLeaveGroupJobRecord
public weak var durableOperationDelegate: LocalUserLeaveGroupJobQueue?
public var operation: OWSOperation { self }
private var groupThreadAfterSuccessfulUpdate: TSGroupThread?
private let future: Future<TSGroupThread>?
fileprivate init(
jobRecord: OWSLocalUserLeaveGroupJobRecord,
future: Future<TSGroupThread>?
) {
self.jobRecord = jobRecord
self.future = future
}
public override func run() {
firstly(on: DispatchQueue.global()) { () throws -> Promise<Void> in
guard self.jobRecord.waitForMessageProcessing else {
return Promise.value(())
}
let groupModel = try self.getGroupModelWithSneakyTransaction()
return GroupManager.messageProcessingPromise(
for: groupModel,
description: "Leave group or decline invite"
)
}.then { () throws -> Promise<TSGroupThread> in
// Read the group model again from the DB to ensure we have the
// latest before we try and update the group
let groupModel = try self.getGroupModelWithSneakyTransaction()
let replacementAdminUuid: UUID? = try self.jobRecord.replacementAdminUuid.map { uuidString in
guard let uuid = UUID(uuidString: uuidString) else {
throw OWSAssertionError("Unable to convert replacement admin UUID string to UUID")
}
return uuid
}
return GroupManager.updateGroupV2(
groupModel: groupModel,
description: "Leave group or decline invite"
) { groupChangeSet in
groupChangeSet.setShouldLeaveGroupDeclineInvite()
// Sometimes when we leave a group we take care to assign a new admin.
if let replacementAdminUuid = replacementAdminUuid {
groupChangeSet.changeRoleForMember(replacementAdminUuid, role: .administrator)
}
}
}.map { groupThread in
self.groupThreadAfterSuccessfulUpdate = groupThread
self.reportSuccess()
}.catch { error in
self.reportError(withUndefinedRetry: error)
}
}
public override func didSucceed() {
Logger.info("Succeeded!")
databaseStorage.write { transaction in
self.durableOperationDelegate?.durableOperationDidSucceed(self, transaction: transaction)
}
guard let groupThread = groupThreadAfterSuccessfulUpdate else {
owsFailDebug("Unexpectedly missing group thread")
future?.reject(OWSAssertionError("Unexpectedly missing group thread"))
return
}
future?.resolve(groupThread)
}
public override func didReportError(_ error: Error) {
Logger.warn("Reported error: \(error). Remaining retries: \(remainingRetries)")
databaseStorage.write { transaction in
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction)
}
}
public override func didFail(error: Error) {
Logger.error("Failed with error: \(error)")
databaseStorage.write { transaction in
self.durableOperationDelegate?.durableOperation(self, didFailWithError: error, transaction: transaction)
}
future?.reject(error)
}
public override func retryInterval() -> TimeInterval {
OWSOperation.retryIntervalForExponentialBackoff(failureCount: jobRecord.failureCount)
}
private func getGroupModelWithSneakyTransaction() throws -> TSGroupModelV2 {
guard
let groupThread = self.databaseStorage.read(block: { transaction in
TSGroupThread.anyFetchGroupThread(
uniqueId: self.jobRecord.threadId,
transaction: transaction
)
}),
let groupModel = groupThread.groupModel as? TSGroupModelV2
else {
throw OWSAssertionError("Missing V2 group thread for operation")
}
return groupModel
}
}
@objc
public class LocalUserLeaveGroupJobQueue: NSObject, JobQueue {
public typealias DurableOperationType = LocalUserLeaveGroupOperation
/// 110 retries corresponds to approximately ~24hr of retry when using
/// `OWSOperation.retryIntervalForExponentialBackoff(failureCount:)`.
public static var maxRetries: UInt { 110 }
public var runningOperations = AtomicArray<LocalUserLeaveGroupOperation>()
public var isSetup = AtomicBool(false)
public var requiresInternet: Bool { true }
public var isEnabled: Bool { true }
public var jobRecordLabel: String { "LocalUserLeaveGroup" }
private let operationQueue: OperationQueue = {
let queue = OperationQueue()
queue.name = "LocalUserLeaveGroupJobQueue"
queue.maxConcurrentOperationCount = 1
return queue
}()
private var jobFutures = AtomicDictionary<String, Future<TSGroupThread>>()
public override init() {
super.init()
AppReadiness.runNowOrWhenAppDidBecomeReadyAsync {
self.setup()
}
}
public func setup() {
defaultSetup()
}
public func buildOperation(jobRecord: JobRecordType, transaction _: SDSAnyReadTransaction) throws -> DurableOperationType {
LocalUserLeaveGroupOperation(
jobRecord: jobRecord,
future: jobFutures.pop(jobRecord.uniqueId)
)
}
public func operationQueue(jobRecord: JobRecordType) -> OperationQueue {
operationQueue
}
public func didMarkAsReady(oldJobRecord: JobRecordType, transaction: SDSAnyWriteTransaction) {
// No special handling
}
// MARK: - Promises
public func add(
threadId: String,
replacementAdminUuid: UUID?,
waitForMessageProcessing: Bool,
transaction: SDSAnyWriteTransaction
) -> Promise<TSGroupThread> {
Promise { future in
self.add(
threadId: threadId,
replacementAdminUuid: replacementAdminUuid,
waitForMessageProcessing: waitForMessageProcessing,
future: future,
transaction: transaction
)
}
}
private func add(
threadId: String,
replacementAdminUuid: UUID?,
waitForMessageProcessing: Bool,
future: Future<TSGroupThread>,
transaction: SDSAnyWriteTransaction
) {
let jobRecord = OWSLocalUserLeaveGroupJobRecord(
threadId: threadId,
replacementAdminUuid: replacementAdminUuid?.uuidString,
waitForMessageProcessing: waitForMessageProcessing,
label: self.jobRecordLabel
)
self.add(jobRecord: jobRecord, transaction: transaction)
jobFutures[jobRecord.uniqueId] = future
}
}