// // Copyright (c) 2018 Open Whisper Systems. All rights reserved. // import Foundation import PromiseKit import SignalUtilitiesKit @objc(OWSSessionResetJobQueue) public class SessionResetJobQueue: NSObject, SignalUtilitiesKit.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 } // MARK: var firstAttempt = true override public func run() { assert(self.durableOperationDelegate != nil) /* 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 SNLog("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) } } }