Newer
Older
import Retry
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")
self.client = client
dbManager = GRDBDatabaseManager()
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)
dbManager = GRDBDatabaseManager()
try continueInitialization()
}
private func continueInitialization() throws {
try dbManager.setup()
setupBindings()
networkMonitor.start()
networkMonitor.statusPublisher
.filter { $0 == .available }.first()
.sink { [unowned self] _ in client.bindings.replayRequests() }
.store(in: &cancellables)
registerUnfinishedTransfers()
if let pendingVerificationUsers: [Contact] = try? dbManager.fetch(.verificationInProgress) {
pendingVerificationUsers.forEach {
var contact = $0
contact.status = .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") }
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
}
public func hideRequestOf(group: Group) {
var group = group
group.status = .hidden
_ = try? dbManager.save(group)
}
public func hideRequestOf(contact: Contact) {
var contact = contact
contact.status = .hidden
_ = try? dbManager.save(contact)
}
public func forceFailMessages() {
if let pendingE2E: [Message] = try? dbManager.fetch(.sending) {
pendingE2E.forEach {
var message = $0
message.status = .failedToSend
}
}
if let pendingGroupMessages: [GroupMessage] = try? dbManager.fetch(.sending) {
pendingGroupMessages.forEach {
var message = $0
message.status = .failed
}
}
}
private func registerUnfinishedTransfers() {
guard let unfinisheds: [Message] = try? dbManager.fetch(.sendingAttachment), !unfinisheds.isEmpty else { return }
for var message in unfinisheds {
guard let tid = message.payload.attachment?.transferId else { return }
do {
try client.transferManager?.listenUploadFromTransfer(with: tid) { completed, sent, arrived, total, error in
if completed {
message.status = .sent
message.payload.attachment?.progress = 1.0
if let transfer: FileTransfer = try? self.dbManager.fetch(.withTID(tid)).first {
}
} else {
if let error = error {
log(string: error.localizedDescription, type: .error)
message.status = .failedToSend
} else {
let progress = Float(arrived)/Float(total)
message.payload.attachment?.progress = progress
return
}
}
_ = try? self.dbManager.save(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] request in
if let _: Contact = try? dbManager.fetch(.withUserId(request.userId)).first { return }
if self.inappnotifications {
DeviceFeedback.sound(.contactAdded)
DeviceFeedback.shake(.notification)
}
verify(contact: request)
}.store(in: &cancellables)
client.requestsSent
.sink { [unowned self] in _ = try? dbManager.save($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
contact.status = .friend
_ = try? dbManager.save(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.groupMessages
.sink { [unowned self] in _ = try? dbManager.save($0) }
.store(in: &cancellables)
client.messages
.sink { [unowned self] in
if var contact: Contact = try? dbManager.fetch(.withUserId($0.sender)).first {
contact.isRecent = false
_ = try? dbManager.save(contact)
}
_ = try? dbManager.save($0)
}.store(in: &cancellables)
client.network
.sink { [unowned self] in networkMonitor.update($0) }
.store(in: &cancellables)
client.incomingTransfers
.sink { [unowned self] in handle(incomingTransfer: $0) }
.store(in: &cancellables)
client.groupRequests
.sink { [unowned self] request in
if let _: Group = try? dbManager.fetch(.withGroupId(request.0.groupId)).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: Contact = try? dbManager.fetch(.withUserId($0.userId)).first {
contact.status = .friend
toastController.enqueueToast(model: .init(
title: contact.nickname ?? contact.username,
subtitle: Localized.Requests.Confirmations.toaster,
leftImage: Asset.sharedSuccess.image
))
}
}.store(in: &cancellables)
}
public func getTextFromMessage(messageId: Data) -> String? {
guard let message: Message = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil }
return message.payload.text
}
public func getTextFromGroupMessage(messageId: Data) -> String? {
guard let message: GroupMessage = try? dbManager.fetch(.withUniqueId(messageId)).first else { return nil }
return message.payload.text
}
public func getContactWith(userId: Data) -> Contact? {
let contact: Contact? = try? dbManager.fetch(.withUserId(userId)).first
return contact
}
public func getGroupChatInfoWith(groupId: Data) -> GroupChatInfo? {
let info: GroupChatInfo? = try? dbManager.fetch(.fromGroup(groupId)).first
return info
}