session-ios/Signal/src/ProfileFetcherJob.swift

151 lines
5.8 KiB
Swift

//
// Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//
import Foundation
@objc
class ProfileFetcherJob: NSObject {
let TAG = "[ProfileFetcherJob]"
let networkManager: TSNetworkManager
let storageManager: TSStorageManager
let thread: TSThread
// This property is only accessed on the default global queue.
static var fetchDateMap = [String: Date]()
public class func run(thread: TSThread, networkManager: TSNetworkManager) {
ProfileFetcherJob(thread: thread, networkManager: networkManager).run()
}
init(thread: TSThread, networkManager: TSNetworkManager) {
self.networkManager = networkManager
self.storageManager = TSStorageManager.shared()
self.thread = thread
}
public func run() {
AssertIsOnMainThread()
DispatchQueue.global().async {
for recipientId in self.thread.recipientIdentifiers {
self.getProfile(recipientId: recipientId)
}
}
}
public func getProfile(recipientId: String, remainingRetries: Int = 3) {
// Only throttle profile fetch in production builds in order to
// facilitate debugging.
if !_isDebugAssertConfiguration() {
if let lastDate = ProfileFetcherJob.fetchDateMap[recipientId] {
let lastTimeInterval = fabs(lastDate.timeIntervalSinceNow)
// Don't check a profile more often than every N minutes.
let kGetProfileMaxFrequencySeconds = 60.0 * 5.0
if lastTimeInterval < kGetProfileMaxFrequencySeconds {
Logger.info("\(self.TAG) skipping getProfile: \(recipientId), lastTimeInterval: \(lastTimeInterval)")
return
}
}
ProfileFetcherJob.fetchDateMap[recipientId] = Date()
}
Logger.error("\(self.TAG) getProfile: \(recipientId)")
let request = OWSGetProfileRequest(recipientId: recipientId)
// We don't need to retainUntilComplete() since the success and failure
// handlers both close over a strong reference to self.
self.networkManager.makeRequest(
request,
success: { (_: URLSessionDataTask?, responseObject: Any?) -> Void in
guard let profileResponse = SignalServiceProfile(recipientId: recipientId, rawResponse: responseObject) else {
Logger.error("\(self.TAG) response object had unexpected content")
assertionFailure("\(self.TAG) response object had unexpected content")
return
}
self.processResponse(signalServiceProfile: profileResponse)
},
failure: { (_: URLSessionDataTask?, error: Error?) in
guard let error = error else {
Logger.error("\(self.TAG) error in \(#function) was surpringly nil. sheesh rough day.")
assertionFailure("\(self.TAG) error in \(#function) was surpringly nil. sheesh rough day.")
return
}
Logger.error("\(self.TAG) failed to fetch profile for recipient: \(recipientId) with error: \(error)")
if remainingRetries > 1 {
DispatchQueue.global().async {
self.getProfile(recipientId: recipientId, remainingRetries:remainingRetries - 1)
}
}
})
}
private func processResponse(signalServiceProfile: SignalServiceProfile) {
Logger.debug("\(TAG) in \(#function) for \(signalServiceProfile)")
verifyIdentityUpToDateAsync(recipientId: signalServiceProfile.recipientId, latestIdentityKey: signalServiceProfile.identityKey)
// Eventually we'll want to do more things with new SignalServiceProfile fields here.
}
private func verifyIdentityUpToDateAsync(recipientId: String, latestIdentityKey: Data) {
OWSDispatch.sessionStoreQueue().async {
if OWSIdentityManager.shared().identityKey(forRecipientId: recipientId) == nil {
// first time use, do nothing, since there's no change.
return
}
if OWSIdentityManager.shared().saveRemoteIdentity(latestIdentityKey, recipientId: recipientId) {
Logger.info("\(self.TAG) updated identity key with fetched profile for recipient: \(recipientId)")
self.storageManager.archiveAllSessions(forContact: recipientId)
} else {
// no change in identity.
}
}
}
}
struct SignalServiceProfile {
let TAG = "[SignalServiceProfile]"
public let recipientId: String
public let identityKey: Data
init?(recipientId: String, rawResponse: Any?) {
self.recipientId = recipientId
guard let responseDict = rawResponse as? [String: Any?] else {
Logger.error("\(TAG) unexpected type: \(String(describing: rawResponse))")
return nil
}
guard let identityKeyString = responseDict["identityKey"] as? String else {
Logger.error("\(TAG) missing identity key: \(String(describing: rawResponse))")
return nil
}
guard let identityKeyWithType = Data(base64Encoded: identityKeyString) else {
Logger.error("\(TAG) unable to parse identity key: \(identityKeyString)")
return nil
}
let kIdentityKeyLength = 33
guard identityKeyWithType.count == kIdentityKeyLength else {
Logger.error("\(TAG) malformed key \(identityKeyString) with decoded length: \(identityKeyWithType.count)")
return nil
}
// `removeKeyType` is an objc category method only on NSData, so temporarily cast.
self.identityKey = (identityKeyWithType as NSData).removeKeyType() as Data
}
}