session-ios/Session/Signal/SessionResetJob.swift
2020-11-12 08:48:41 +11:00

209 lines
7.8 KiB
Swift

//
// Copyright (c) 2018 Open Whisper Systems. All rights reserved.
//
import Foundation
import PromiseKit
import SignalUtilitiesKit
@objc(OWSSessionResetJobQueue)
public class SessionResetJobQueue: NSObject, JobQueue {
@objc(addContactThread:transaction:)
public func add(contactThread: TSContactThread, transaction: YapDatabaseReadWriteTransaction) {
let jobRecord = OWSSessionResetJobRecord(contactThread: contactThread, label: self.jobRecordLabel)
self.add(jobRecord: jobRecord, transaction: transaction)
}
// MARK: JobQueue
public typealias DurableOperationType = SessionResetOperation
public let jobRecordLabel: String = "SessionReset"
public static let maxRetries: UInt = 10
public let requiresInternet: Bool = true
public var runningOperations: [SessionResetOperation] = []
@objc
public override init() {
super.init()
AppReadiness.runNowOrWhenAppWillBecomeReady {
self.setup()
}
}
@objc
public func setup() {
defaultSetup()
}
public var isSetup: Bool = false
public func didMarkAsReady(oldJobRecord: JobRecordType, transaction: YapDatabaseReadWriteTransaction) {
// no special handling
}
let operationQueue: OperationQueue = {
// no need to serialize the operation queuing, since sending will ultimately be serialized by MessageSender
let operationQueue = OperationQueue()
operationQueue.name = "SessionReset.OperationQueue"
return operationQueue
}()
public func operationQueue(jobRecord: OWSSessionResetJobRecord) -> OperationQueue {
return self.operationQueue
}
public func buildOperation(jobRecord: OWSSessionResetJobRecord, transaction: YapDatabaseReadTransaction) throws -> SessionResetOperation {
guard let contactThread = TSThread.fetch(uniqueId: jobRecord.contactThreadId, transaction: transaction) as? TSContactThread else {
throw JobError.obsolete(description: "thread for session reset no longer exists")
}
return SessionResetOperation(contactThread: contactThread, jobRecord: jobRecord)
}
}
public class SessionResetOperation: OWSOperation, DurableOperation {
// MARK: DurableOperation
public let jobRecord: OWSSessionResetJobRecord
weak public var durableOperationDelegate: SessionResetJobQueue?
public var operation: OWSOperation {
return self
}
// MARK:
let contactThread: TSContactThread
var recipientId: String {
return contactThread.contactIdentifier()
}
@objc public required init(contactThread: TSContactThread, jobRecord: OWSSessionResetJobRecord) {
self.contactThread = contactThread
self.jobRecord = jobRecord
}
// MARK: Dependencies
var dbConnection: YapDatabaseConnection {
return SSKEnvironment.shared.primaryStorage.dbReadWriteConnection
}
var primaryStorage: OWSPrimaryStorage {
return SSKEnvironment.shared.primaryStorage
}
var messageSender: MessageSender {
return SSKEnvironment.shared.messageSender
}
// MARK:
var firstAttempt = true
override public func run() {
assert(self.durableOperationDelegate != nil)
/* Loki: Original code
* We don't want to delete the session. Ref: SignalServiceKit/Loki/Docs/SessionReset.md
* ================
if firstAttempt {
Storage.writeSync { transaction in
Logger.info("deleting sessions for recipient: \(self.recipientId)")
self.primaryStorage.deleteAllSessions(forContact: self.recipientId, protocolContext: transaction)
}
firstAttempt = false
}
* ================
*/
let endSessionMessage = EndSessionMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread)
firstly {
return self.messageSender.sendPromise(message: endSessionMessage)
}.done {
Logger.info("successfully sent EndSessionMessage.")
Storage.writeSync { transaction in
// Archive the just-created session since the recipient should delete their corresponding
// session upon receiving and decrypting our EndSession message.
// Otherwise if we send another message before them, they wont have the session to decrypt it.
self.primaryStorage.archiveAllSessions(forContact: self.recipientId, protocolContext: transaction)
/* Loki: Original code
* ================
let message = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(),
in: self.contactThread,
messageType: TSInfoMessageType.typeSessionDidEnd)
message.save(with: transaction)
* ================
*/
if (self.contactThread.sessionResetStatus != .requestReceived) {
let message = TSInfoMessage(timestamp: NSDate.ows_millisecondTimeStamp(), in: self.contactThread, messageType: .typeLokiSessionResetInProgress)
message.save(with: transaction)
// Loki: We have initiated a session reset
print("[Loki] Session reset initiated.")
self.contactThread.sessionResetStatus = .initiated
self.contactThread.save(with: transaction)
}
}
self.reportSuccess()
}.catch { error in
Logger.error("sending error: \(error.localizedDescription)")
self.reportError(error)
}.retainUntilComplete()
}
override public func didSucceed() {
Storage.writeSync { transaction in
self.durableOperationDelegate?.durableOperationDidSucceed(self, transaction: transaction)
}
}
override public func didReportError(_ error: Error) {
Logger.debug("remainingRetries: \(self.remainingRetries)")
Storage.writeSync { transaction in
self.durableOperationDelegate?.durableOperation(self, didReportError: error, transaction: transaction)
}
}
override public func retryInterval() -> TimeInterval {
// Arbitrary backoff factor...
// With backOffFactor of 1.9
// try 1 delay: 0.00s
// try 2 delay: 0.19s
// ...
// try 5 delay: 1.30s
// ...
// try 11 delay: 61.31s
let backoffFactor = 1.9
let maxBackoff = kHourInterval
let seconds = 0.1 * min(maxBackoff, pow(backoffFactor, Double(self.jobRecord.failureCount)))
return seconds
}
override public func didFail(error: Error) {
Logger.error("failed to send EndSessionMessage with error: \(error.localizedDescription)")
Storage.writeSync { transaction in
self.durableOperationDelegate?.durableOperation(self, didFailWithError: error, transaction: transaction)
// Even though this is the failure handler - which means probably the recipient didn't receive the message
// there's a chance that our send did succeed and the server just timed out our repsonse or something.
// Since the cost of sending a future message using a session the recipient doesn't have is so high,
// we archive the session just in case.
//
// Archive the just-created session since the recipient should delete their corresponding
// session upon receiving and decrypting our EndSession message.
// Otherwise if we send another message before them, they wont have the session to decrypt it.
self.primaryStorage.archiveAllSessions(forContact: self.recipientId, protocolContext: transaction)
}
}
}