Newer
Older
import Models
import Shared
import Combine
import Defaults
let logHandler = OSLog(subsystem: "xx.network", category: "Performance debugging")
struct BackupParameters: Codable {
var email: String?
var phone: String?
var username: String
}
struct BackupReport: Codable {
var contactIds: [String]
var parameters: String
private enum CodingKeys: String, CodingKey {
case parameters = "Params"
case contactIds = "RestoredContacts"
}
}
public final class Session: SessionType {
@KeyObject(.theme, defaultValue: nil) var theme: String?
@KeyObject(.email, defaultValue: nil) var email: String?
@KeyObject(.phone, defaultValue: nil) var phone: String?
@KeyObject(.avatar, defaultValue: nil) var avatar: Data?
@KeyObject(.username, defaultValue: nil) var username: String?
@KeyObject(.biometrics, defaultValue: false) var biometrics: Bool
@KeyObject(.hideAppList, defaultValue: false) var hideAppList: Bool
@KeyObject(.requestCounter, defaultValue: 0) var requestCounter: Int
@KeyObject(.sharingEmail, defaultValue: false) var isSharingEmail: Bool
@KeyObject(.sharingPhone, defaultValue: false) var isSharingPhone: Bool
@KeyObject(.recordingLogs, defaultValue: true) var recordingLogs: Bool
@KeyObject(.crashReporting, defaultValue: true) var crashReporting: Bool
@KeyObject(.icognitoKeyboard, defaultValue: false) var icognitoKeyboard: Bool
@KeyObject(.pushNotifications, defaultValue: false) var pushNotifications: Bool
@KeyObject(.inappnotifications, defaultValue: true) var inappnotifications: Bool
@Dependency var toastController: ToastController
@Dependency var networkMonitor: NetworkMonitoring
public let client: Client
private var cancellables = Set<AnyCancellable>()
public var myId: Data { client.bindings.myId }
public var version: String { type(of: client.bindings).version }
public var myQR: Data {
client
.bindings
.meMarshalled(
username!,
email: isSharingEmail ? email : nil,
phone: isSharingPhone ? phone : nil
)
}
public var hasRunningTasks: Bool {
client.bindings.hasRunningTasks
}
public var isOnline: AnyPublisher<Bool, Never> {
networkMonitor.statusPublisher.map { $0 == .available }.eraseToAnyPublisher()
}
public init(
passphrase: String,
backupFile: Data,
ndf: String
) throws {
let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking
os_signpost(.begin, log: logHandler, name: "Decrypting", "Calling newClientFromBackup")
let (client, backupData) = try network.newClientFromBackup(passphrase: passphrase, data: backupFile, ndf: ndf)
os_signpost(.end, log: logHandler, name: "Decrypting", "Finished newClientFromBackup")
let oldPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
)[0].appending("/xxmessenger.sqlite")
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
.appendingPathComponent("database")
.appendingPathExtension("sqlite").path
try Migrator.live()(
try .init(path: oldPath),
to: try .onDisk(path: newPath),
myContactId: client.bindings.myId,
meMarshaled: client.bindings.meMarshalled
)
dbManager = try Database.onDisk(path: newPath)
let report = try! JSONDecoder().decode(BackupReport.self, from: backupData!)
if !report.parameters.isEmpty {
let params = try! JSONDecoder().decode(BackupParameters.self, from: Data(report.parameters.utf8))
username = params.username
phone = params.phone
email = params.email
}
try continueInitialization()
if !report.contactIds.isEmpty {
client.restoreContacts(fromBackup: try! JSONSerialization.data(withJSONObject: report.contactIds))
}
}
public init(ndf: String) throws {
let network = try! DependencyInjection.Container.shared.resolve() as XXNetworking
self.client = try network.newClient(ndf: ndf)
let oldPath = NSSearchPathForDirectoriesInDomains(
.documentDirectory, .userDomainMask, true
)[0].appending("/xxmessenger.sqlite")
let newPath = FileManager.default
.containerURL(forSecurityApplicationGroupIdentifier: "group.elixxir.messenger")!
.appendingPathComponent("database")
.appendingPathExtension("sqlite").path
try Migrator.live()(
try .init(path: oldPath),
to: try .onDisk(path: newPath),
myContactId: client.bindings.myId,
meMarshaled: client.bindings.meMarshalled
)
dbManager = try Database.onDisk(path: newPath)
try continueInitialization()
}
private func continueInitialization() throws {
setupBindings()
networkMonitor.start()
networkMonitor.statusPublisher
.filter { $0 == .available }.first()
.sink { [unowned self] _ in client.bindings.replayRequests() }
.store(in: &cancellables)
registerUnfinishedTransfers()
let query = Contact.Query(authStatus: [.verificationInProgress])
_ = try? dbManager.bulkUpdateContacts(query, .init(authStatus: .verificationFailed))
}
public func setDummyTraffic(status: Bool) {
client.dummyManager?.setStatus(status: status)
}
public func deleteMyself() throws {
guard let username = username, let ud = client.userDiscovery else { return }
try ud.deleteMyself(username)
stop()
cleanUp()
}
private func cleanUp() {
retry(max: 10, retryStrategy: .delay(seconds: 1)) { [unowned self] in
guard self.hasRunningTasks == false else { throw NSError.create("") }
}.finalCatch { _ in fatalError("Couldn't delete account because network is not stopping") }
try? dbManager.drop()
FileManager.xxCleanup()
email = nil
phone = nil
theme = nil
avatar = nil
self.username = nil
isSharingEmail = false
isSharingPhone = false
requestCounter = 0
biometrics = false
hideAppList = false
recordingLogs = true
crashReporting = true
icognitoKeyboard = false
pushNotifications = false
inappnotifications = true
}
private func registerUnfinishedTransfers() {
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
guard let unfinishedSendingMessages = try? dbManager.fetchMessages(.init(status: [.sending])),
let unfinishedSendingTransfers = try? dbManager.fetchFileTransfers(.init(
id: Set(unfinishedSendingMessages
.filter { $0.fileTransferId != nil }
.compactMap(\.fileTransferId))))
else { return }
// What would be a good way to do this?
let pairs = unfinishedSendingMessages.map { message -> (Message, FileTransfer) in
let transfer = unfinishedSendingTransfers.first { ft in
ft.id == message.fileTransferId
}
return (message, transfer!)
}
pairs.forEach { message, transfer in
var message = message
var transfer = transfer
do {
try client.transferManager?.listenUploadFromTransfer(with: transfer.id) { completed, sent, arrived, total, error in
if completed {
transfer.progress = 1.0
message.status = .sent
} else {
if error != nil {
message.status = .sendingFailed
} else {
transfer.progress = Float(arrived)/Float(total)
}
}
_ = try? self.dbManager.saveFileTransfer(transfer)
_ = try? self.dbManager.saveMessage(message)
}
} catch {
message.status = .sendingFailed
_ = try? self.dbManager.saveMessage(message)
}
}
func updateFactsOnBackup() {
struct BackupParameters: Codable {
var email: String?
var phone: String?
var username: String
var jsonFormat: String {
let data = try! JSONEncoder().encode(self)
let json = String(data: data, encoding: .utf8)
return json!
let params = BackupParameters(
email: email,
phone: phone,
username: username!
).jsonFormat
client.addJson(params)
backupService.performBackupIfAutomaticIsEnabled()
}
private func setupBindings() {
client.requests
.sink { [unowned self] in
if let _ = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
return
}
if self.inappnotifications {
DeviceFeedback.sound(.contactAdded)
DeviceFeedback.shake(.notification)
}
}.store(in: &cancellables)
client.requestsSent
.sink { [unowned self] in _ = try? dbManager.saveContact($0) }
client.backup
.throttle(for: .seconds(5), scheduler: DispatchQueue.main, latest: true)
.sink { [unowned self] in backupService.updateBackup(data: $0) }
.store(in: &cancellables)
client.resets
/// This will get called when my contact restore its contact.
/// TODO: Hold a record on the chat that this contact restored.
///
var contact = $0
_ = try? dbManager.saveContact(contact)
backupService.settingsPublisher
.map { $0.enabledService != nil }
.removeDuplicates()
guard let passphrase = backupService.passphrase else {
client.resumeBackup()
updateFactsOnBackup()
return
}
client.initializeBackup(passphrase: passphrase)
backupService.passphrase = nil
}
.store(in: &cancellables)
networkMonitor.statusPublisher
.sink { print($0) }
.store(in: &cancellables)
client.messages
if var contact = try? dbManager.fetchContacts(.init(id: [$0.senderId])).first {
_ = try? dbManager.saveContact(contact)
client.network
.sink { [unowned self] in networkMonitor.update($0) }
.store(in: &cancellables)
client.groupRequests
.sink { [unowned self] request in
if let _ = try? dbManager.fetchGroups(.init(id: [request.0.id])).first {
return
}
DispatchQueue.global().async { [weak self] in
self?.processGroupCreation(request.0, memberIds: request.1, welcome: request.2)
}
}.store(in: &cancellables)
client.confirmations
.sink { [unowned self] in
if var contact = try? dbManager.fetchContacts(.init(id: [$0.id])).first {
contact.authStatus = .friend
_ = try? dbManager.saveContact(contact)
toastController.enqueueToast(model: .init(
title: contact.nickname ?? contact.username!,
subtitle: Localized.Requests.Confirmations.toaster,
leftImage: Asset.sharedSuccess.image
))