Swiftgram/TelegramCore/ContactSyncManager.swift
Peter 0b96d69daa Fixed unread counter category for private channels
Added support for "contact joined" service messages
Updated password recovery API
Updated Localization APIs
Added limited contact presence polling after getDifference
2018-11-11 17:46:21 +04:00

426 lines
20 KiB
Swift

import Foundation
#if os(macOS)
import PostboxMac
import SwiftSignalKitMac
#else
import Postbox
import SwiftSignalKit
#endif
private final class ContactSyncOperation {
let id: Int32
var isRunning: Bool = false
let content: ContactSyncOperationContent
let disposable = DisposableSet()
init(id: Int32, content: ContactSyncOperationContent) {
self.id = id
self.content = content
}
}
private enum ContactSyncOperationContent {
case waitForUpdatedState
case updatePresences
case sync(importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData]?)
case updateIsContact([(PeerId, Bool)])
}
private final class ContactSyncManagerImpl {
private let queue: Queue
private let postbox: Postbox
private let network: Network
private let accountPeerId: PeerId
private let stateManager: AccountStateManager
private var nextId: Int32 = 0
private var operations: [ContactSyncOperation] = []
private var lastContactPresencesRequestTimestamp: Double?
private var reimportAttempts: [TelegramDeviceContactImportIdentifier: Double] = [:]
private let importableContactsDisposable = MetaDisposable()
private let significantStateUpdateCompletedDisposable = MetaDisposable()
init(queue: Queue, postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager) {
self.queue = queue
self.postbox = postbox
self.network = network
self.accountPeerId = accountPeerId
self.stateManager = stateManager
}
deinit {
self.importableContactsDisposable.dispose()
}
func beginSync(importableContacts: Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>) {
self.importableContactsDisposable.set((importableContacts
|> deliverOn(self.queue)).start(next: { [weak self] importableContacts in
guard let strongSelf = self else {
return
}
strongSelf.addOperation(.waitForUpdatedState)
strongSelf.addOperation(.updatePresences)
strongSelf.addOperation(.sync(importableContacts: importableContacts))
}))
self.significantStateUpdateCompletedDisposable.set((self.stateManager.significantStateUpdateCompleted
|> deliverOn(self.queue)).start(next: { [weak self] in
guard let strongSelf = self else {
return
}
let timestamp = CFAbsoluteTimeGetCurrent()
let shouldUpdate: Bool
if let lastContactPresencesRequestTimestamp = strongSelf.lastContactPresencesRequestTimestamp {
if timestamp > lastContactPresencesRequestTimestamp + 30.0 * 60.0 {
shouldUpdate = true
} else {
shouldUpdate = false
}
} else {
shouldUpdate = true
}
if shouldUpdate {
strongSelf.lastContactPresencesRequestTimestamp = timestamp
var found = false
for operation in strongSelf.operations {
if case .updatePresences = operation.content {
found = true
break
}
}
if !found {
strongSelf.addOperation(.updatePresences)
}
}
}))
}
func addIsContactUpdates(_ updates: [(PeerId, Bool)]) {
self.addOperation(.updateIsContact(updates))
}
func addOperation(_ content: ContactSyncOperationContent) {
let id = self.nextId
self.nextId += 1
let operation = ContactSyncOperation(id: id, content: content)
switch content {
case .waitForUpdatedState:
self.operations.append(operation)
case .updatePresences:
for i in (0 ..< self.operations.count).reversed() {
if case .updatePresences = self.operations[i].content {
if !self.operations[i].isRunning {
self.operations.remove(at: i)
}
}
}
self.operations.append(operation)
case .sync:
for i in (0 ..< self.operations.count).reversed() {
if case .sync = self.operations[i].content {
if !self.operations[i].isRunning {
self.operations.remove(at: i)
}
}
}
self.operations.append(operation)
case let .updateIsContact(updates):
var mergedUpdates: [(PeerId, Bool)] = []
var removeIndices: [Int] = []
for i in 0 ..< self.operations.count {
if case let .updateIsContact(operationUpdates) = self.operations[i].content {
if !self.operations[i].isRunning {
mergedUpdates.append(contentsOf: operationUpdates)
removeIndices.append(i)
}
}
}
mergedUpdates.append(contentsOf: updates)
for index in removeIndices.reversed() {
self.operations.remove(at: index)
}
if self.operations.isEmpty || !self.operations[0].isRunning {
self.operations.insert(operation, at: 0)
} else {
self.operations.insert(operation, at: 1)
}
}
self.updateOperations()
}
func updateOperations() {
if let first = self.operations.first, !first.isRunning {
first.isRunning = true
let id = first.id
let queue = self.queue
self.startOperation(first.content, disposable: first.disposable, completion: { [weak self] in
queue.async {
guard let strongSelf = self else {
return
}
if let currentFirst = strongSelf.operations.first, currentFirst.id == id {
strongSelf.operations.remove(at: 0)
strongSelf.updateOperations()
} else {
assertionFailure()
}
}
})
}
}
func startOperation(_ operation: ContactSyncOperationContent, disposable: DisposableSet, completion: @escaping () -> Void) {
switch operation {
case .waitForUpdatedState:
disposable.add((self.stateManager.pollStateUpdateCompletion()
|> deliverOn(self.queue)).start(next: { _ in
completion()
}))
case .updatePresences:
disposable.add(updateContactPresences(postbox: self.postbox, network: self.network, accountPeerId: self.accountPeerId).start(completed: {
completion()
}))
case let .sync(importableContacts):
let importSignal: Signal<PushDeviceContactsResult, NoError>
if let importableContacts = importableContacts {
importSignal = pushDeviceContacts(postbox: self.postbox, network: self.network, importableContacts: importableContacts, reimportAttempts: self.reimportAttempts)
} else {
importSignal = .single(PushDeviceContactsResult(addedReimportAttempts: [:]))
}
disposable.add(
(syncContactsOnce(network: self.network, postbox: self.postbox, accountPeerId: self.accountPeerId)
|> mapToSignal { _ -> Signal<PushDeviceContactsResult, NoError> in
return .complete()
}
|> then(importSignal)
|> deliverOn(self.queue)
).start(next: { [weak self] result in
guard let strongSelf = self else {
return
}
for (identifier, timestamp) in result.addedReimportAttempts {
strongSelf.reimportAttempts[identifier] = timestamp
}
completion()
}))
case let .updateIsContact(updates):
disposable.add((self.postbox.transaction { transaction -> Void in
var contactPeerIds = transaction.getContactPeerIds()
for (peerId, isContact) in updates {
if isContact {
contactPeerIds.insert(peerId)
} else {
contactPeerIds.remove(peerId)
}
}
transaction.replaceContactPeerIds(contactPeerIds)
}
|> deliverOnMainQueue).start(completed: {
completion()
}))
}
}
}
private struct PushDeviceContactsResult {
let addedReimportAttempts: [TelegramDeviceContactImportIdentifier: Double]
}
private func pushDeviceContacts(postbox: Postbox, network: Network, importableContacts: [DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], reimportAttempts: [TelegramDeviceContactImportIdentifier: Double]) -> Signal<PushDeviceContactsResult, NoError> {
return postbox.transaction { transaction -> Signal<PushDeviceContactsResult, NoError> in
var noLongerImportedIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var updatedDataIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var addedIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
var retryLaterIdentifiers = Set<TelegramDeviceContactImportIdentifier>()
addedIdentifiers.formUnion(importableContacts.keys.map(TelegramDeviceContactImportIdentifier.phoneNumber))
transaction.enumerateDeviceContactImportInfoItems({ key, value in
if let identifier = TelegramDeviceContactImportIdentifier(key: key) {
addedIdentifiers.remove(identifier)
switch identifier {
case let .phoneNumber(number):
if let updatedData = importableContacts[number] {
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(imported):
if imported.data != updatedData {
updatedDataIdentifiers.insert(identifier)
}
case .retryLater:
retryLaterIdentifiers.insert(identifier)
}
} else {
assertionFailure()
}
} else {
noLongerImportedIdentifiers.insert(identifier)
}
}
} else {
assertionFailure()
}
return true
})
for identifier in noLongerImportedIdentifiers {
transaction.setDeviceContactImportInfo(identifier.key, value: nil)
}
var orderedPushIdentifiers: [TelegramDeviceContactImportIdentifier] = []
orderedPushIdentifiers.append(contentsOf: addedIdentifiers.sorted())
orderedPushIdentifiers.append(contentsOf: updatedDataIdentifiers.sorted())
orderedPushIdentifiers.append(contentsOf: retryLaterIdentifiers.sorted())
var currentContactDetails: [TelegramDeviceContactImportIdentifier: TelegramUser] = [:]
for peerId in transaction.getContactPeerIds() {
if let user = transaction.getPeer(peerId) as? TelegramUser, let phone = user.phone, !phone.isEmpty {
currentContactDetails[.phoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: formatPhoneNumber(phone)))] = user
}
}
let timestamp = CFAbsoluteTimeGetCurrent()
outer: for i in (0 ..< orderedPushIdentifiers.count).reversed() {
if let user = currentContactDetails[orderedPushIdentifiers[i]], case let .phoneNumber(number) = orderedPushIdentifiers[i], let data = importableContacts[number] {
if (user.firstName ?? "") == data.firstName && (user.lastName ?? "") == data.lastName {
transaction.setDeviceContactImportInfo(orderedPushIdentifiers[i].key, value: TelegramDeviceContactImportedData.imported(data: data, importedByCount: 0))
orderedPushIdentifiers.remove(at: i)
continue outer
}
}
if let attemptTimestamp = reimportAttempts[orderedPushIdentifiers[i]], attemptTimestamp + 60.0 * 60.0 * 24.0 > timestamp {
orderedPushIdentifiers.remove(at: i)
}
}
var preparedContactData: [(DeviceContactNormalizedPhoneNumber, ImportableDeviceContactData)] = []
for identifier in orderedPushIdentifiers {
if case let .phoneNumber(number) = identifier, let value = importableContacts[number] {
preparedContactData.append((number, value))
}
}
return pushDeviceContactData(postbox: postbox, network: network, contacts: preparedContactData)
}
|> switchToLatest
}
private let importBatchCount: Int = 100
private func pushDeviceContactData(postbox: Postbox, network: Network, contacts: [(DeviceContactNormalizedPhoneNumber, ImportableDeviceContactData)]) -> Signal<PushDeviceContactsResult, NoError> {
var batches: Signal<PushDeviceContactsResult, NoError> = .single(PushDeviceContactsResult(addedReimportAttempts: [:]))
for s in stride(from: 0, to: contacts.count, by: importBatchCount) {
let batch = Array(contacts[s ..< min(s + importBatchCount, contacts.count)])
batches = batches
|> mapToSignal { intermediateResult -> Signal<PushDeviceContactsResult, NoError> in
return network.request(Api.functions.contacts.importContacts(contacts: zip(0 ..< batch.count, batch).map { index, item -> Api.InputContact in
return .inputPhoneContact(clientId: Int64(index), phone: item.0.rawValue, firstName: item.1.firstName, lastName: item.1.lastName)
}))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.ImportedContacts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PushDeviceContactsResult, NoError> in
return postbox.transaction { transaction -> PushDeviceContactsResult in
var addedReimportAttempts: [TelegramDeviceContactImportIdentifier: Double] = intermediateResult.addedReimportAttempts
if let result = result {
var addedContactPeerIds = Set<PeerId>()
var retryIndices = Set<Int>()
var importedCounts: [Int: Int32] = [:]
switch result {
case let .importedContacts(imported, popularInvites, retryContacts, users):
let peers = users.map { TelegramUser(user: $0) as Peer }
updatePeers(transaction: transaction, peers: peers, update: { _, updated in
return updated
})
for item in imported {
switch item {
case let .importedContact(userId, _):
addedContactPeerIds.insert(PeerId(namespace: Namespaces.Peer.CloudUser, id: userId))
}
}
for item in retryContacts {
retryIndices.insert(Int(item))
}
for item in popularInvites {
switch item {
case let .popularContact(clientId, importers):
importedCounts[Int(clientId)] = importers
}
}
}
let timestamp = CFAbsoluteTimeGetCurrent()
for i in 0 ..< batch.count {
let importedData: TelegramDeviceContactImportedData
if retryIndices.contains(i) {
importedData = .retryLater
addedReimportAttempts[.phoneNumber(batch[i].0)] = timestamp
} else {
importedData = .imported(data: batch[i].1, importedByCount: importedCounts[i] ?? 0)
}
transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(batch[i].0).key, value: importedData)
}
var contactPeerIds = transaction.getContactPeerIds()
contactPeerIds.formUnion(addedContactPeerIds)
transaction.replaceContactPeerIds(contactPeerIds)
} else {
let timestamp = CFAbsoluteTimeGetCurrent()
for (number, _) in batch {
addedReimportAttempts[.phoneNumber(number)] = timestamp
transaction.setDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key, value: TelegramDeviceContactImportedData.retryLater)
}
}
return PushDeviceContactsResult(addedReimportAttempts: addedReimportAttempts)
}
}
}
}
return batches
}
private func updateContactPresences(postbox: Postbox, network: Network, accountPeerId: PeerId) -> Signal<Never, NoError> {
return network.request(Api.functions.contacts.getStatuses())
|> `catch` { _ -> Signal<[Api.ContactStatus], NoError> in
return .single([])
}
|> mapToSignal { statuses -> Signal<Never, NoError> in
return postbox.transaction { transaction -> Void in
var peerPresences: [PeerId: PeerPresence] = [:]
for status in statuses {
switch status {
case let .contactStatus(userId, status):
peerPresences[PeerId(namespace: Namespaces.Peer.CloudUser, id: userId)] = TelegramUserPresence(apiStatus: status)
}
}
updatePeerPresences(transaction: transaction, accountPeerId: accountPeerId, peerPresences: peerPresences)
}
|> ignoreValues
}
}
final class ContactSyncManager {
private let queue = Queue()
private let impl: QueueLocalObject<ContactSyncManagerImpl>
init(postbox: Postbox, network: Network, accountPeerId: PeerId, stateManager: AccountStateManager) {
let queue = self.queue
self.impl = QueueLocalObject(queue: queue, generate: {
return ContactSyncManagerImpl(queue: queue, postbox: postbox, network: network, accountPeerId: accountPeerId, stateManager: stateManager)
})
}
func beginSync(importableContacts: Signal<[DeviceContactNormalizedPhoneNumber: ImportableDeviceContactData], NoError>) {
self.impl.with { impl in
impl.beginSync(importableContacts: importableContacts)
}
}
func addIsContactUpdates(_ updates: [(PeerId, Bool)]) {
self.impl.with { impl in
impl.addIsContactUpdates(updates)
}
}
}