Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
@@ -0,0 +1,204 @@
import Foundation
import TelegramApi
import Postbox
import SwiftSignalKit
import CryptoUtils
private func md5(_ data: Data) -> Data {
return data.withUnsafeBytes { rawBytes -> Data in
let bytes = rawBytes.baseAddress!
return CryptoMD5(bytes, Int32(data.count))
}
}
private func updatedRemoteContactPeers(network: Network, hash: Int64) -> Signal<(AccumulatedPeers, Int32)?, NoError> {
return network.request(Api.functions.contacts.getContacts(hash: hash), automaticFloodWait: false)
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.Contacts?, NoError> in
return .single(nil)
}
|> map { result -> (AccumulatedPeers, Int32)? in
guard let result = result else {
return nil
}
switch result {
case .contactsNotModified:
return nil
case let .contacts(_, savedCount, users):
return (AccumulatedPeers(users: users), savedCount)
}
}
}
private func hashForCountAndIds(count: Int32, ids: [Int64]) -> Int64 {
var acc: UInt64 = 0
combineInt64Hash(&acc, with: UInt64(count))
for id in ids {
combineInt64Hash(&acc, with: UInt64(bitPattern: id))
}
return finalizeInt64Hash(acc)
}
func syncContactsOnce(network: Network, postbox: Postbox, accountPeerId: PeerId) -> Signal<Never, NoError> {
let initialContactPeerIdsHash = postbox.transaction { transaction -> Int64 in
let contactPeerIds = transaction.getContactPeerIds()
let totalCount = transaction.getRemoteContactCount()
let peerIds = Set(contactPeerIds.filter({ $0.namespace == Namespaces.Peer.CloudUser }))
return hashForCountAndIds(count: totalCount, ids: peerIds.map({ $0.id._internalGetInt64Value() }).sorted())
}
let updatedPeers = initialContactPeerIdsHash
|> mapToSignal { hash -> Signal<(AccumulatedPeers, Int32)?, NoError> in
return updatedRemoteContactPeers(network: network, hash: hash)
}
let appliedUpdatedPeers = updatedPeers
|> mapToSignal { peersAndPresences -> Signal<Never, NoError> in
if let (peers, totalCount) = peersAndPresences {
return postbox.transaction { transaction -> Signal<Void, NoError> in
let previousIds = transaction.getContactPeerIds()
let wasEmpty = previousIds.isEmpty
transaction.replaceRemoteContactCount(totalCount)
if wasEmpty {
let users = Array(peers.users.values)
var insertSignal: Signal<Void, NoError> = .complete()
for s in stride(from: 0, to: users.count, by: 500) {
let partUsers = Array(users[s ..< min(s + 500, users.count)])
let partSignal = postbox.transaction { transaction -> Void in
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: partUsers))
var updatedIds = transaction.getContactPeerIds()
updatedIds.formUnion(partUsers.map { $0.peerId })
transaction.replaceContactPeerIds(updatedIds)
}
|> delay(0.1, queue: Queue.concurrentDefaultQueue())
insertSignal = insertSignal |> then(partSignal)
}
return insertSignal
} else {
transaction.replaceContactPeerIds(Set(peers.users.keys))
return .complete()
}
}
|> switchToLatest
|> ignoreValues
} else {
return .complete()
}
}
return appliedUpdatedPeers
}
func _internal_deleteContactPeerInteractively(account: Account, peerId: PeerId) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
if let peer = transaction.getPeer(peerId), let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.deleteContacts(id: [inputUser]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return account.postbox.transaction { transaction -> Void in
if let user = peer as? TelegramUser {
_internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false)
}
var peerIds = transaction.getContactPeerIds()
if peerIds.contains(peerId) {
peerIds.remove(peerId)
transaction.replaceContactPeerIds(peerIds)
}
}
}
|> ignoreValues
} else {
return .complete()
}
}
|> switchToLatest
}
func _internal_deleteContacts(account: Account, peerIds: [PeerId]) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> Signal<Never, NoError> in
let users = peerIds.compactMap { transaction.getPeer($0) }
let inputUsers: [Api.InputUser] = users.compactMap { apiInputUser($0) }
if !inputUsers.isEmpty {
return account.network.request(Api.functions.contacts.deleteContacts(id: inputUsers))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
|> mapToSignal { updates -> Signal<Void, NoError> in
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return account.postbox.transaction { transaction -> Void in
for user in users {
if let user = user as? TelegramUser {
_internal_updatePeerIsContact(transaction: transaction, user: user, isContact: false)
}
}
let updatedContactPeerIds = transaction.getContactPeerIds().filter { !peerIds.contains($0) }
transaction.replaceContactPeerIds(updatedContactPeerIds)
}
}
|> ignoreValues
} else {
return .complete()
}
}
|> switchToLatest
}
func _internal_deleteAllContacts(account: Account) -> Signal<Never, NoError> {
return account.postbox.transaction { transaction -> [Api.InputUser] in
return transaction.getContactPeerIds().compactMap(transaction.getPeer).compactMap({ apiInputUser($0) }).compactMap({ $0 })
}
|> mapToSignal { users -> Signal<Never, NoError> in
let deleteContacts = account.network.request(Api.functions.contacts.deleteContacts(id: users))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.Updates?, NoError> in
return .single(nil)
}
let deleteImported = account.network.request(Api.functions.contacts.resetSaved())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
return combineLatest(deleteContacts, deleteImported)
|> mapToSignal { updates, _ -> Signal<Never, NoError> in
return account.postbox.transaction { transaction -> Void in
transaction.replaceContactPeerIds(Set())
transaction.clearDeviceContactImportInfoIdentifiers()
}
|> mapToSignal { _ -> Signal<Void, NoError> in
account.restartContactManagement()
if let updates = updates {
account.stateManager.addUpdates(updates)
}
return .complete()
}
|> ignoreValues
}
}
}
func _internal_resetSavedContacts(network: Network) -> Signal<Void, NoError> {
return network.request(Api.functions.contacts.resetSaved())
|> `catch` { _ -> Signal<Api.Bool, NoError> in
return .single(.boolFalse)
}
|> mapToSignal { _ -> Signal<Void, NoError> in
return .complete()
}
}
@@ -0,0 +1,75 @@
import Foundation
import Postbox
public struct DeviceContactNormalizedPhoneNumber: Hashable, RawRepresentable {
public let rawValue: String
public init(rawValue: String) {
self.rawValue = rawValue
}
}
public final class DeviceContactPhoneNumberValue: Equatable {
public let plain: String
public let normalized: DeviceContactNormalizedPhoneNumber
public init(plain: String, normalized: DeviceContactNormalizedPhoneNumber) {
self.plain = plain
self.normalized = normalized
}
public static func ==(lhs: DeviceContactPhoneNumberValue, rhs: DeviceContactPhoneNumberValue) -> Bool {
if lhs.plain != rhs.plain {
return false
}
if lhs.normalized != rhs.normalized {
return false
}
return true
}
}
public final class DeviceContactPhoneNumber: Equatable {
public let label: String
public let number: DeviceContactPhoneNumberValue
public init(label: String, number: DeviceContactPhoneNumberValue) {
self.label = label
self.number = number
}
public static func ==(lhs: DeviceContactPhoneNumber, rhs: DeviceContactPhoneNumber) -> Bool {
return lhs.label == rhs.label && lhs.number == rhs.number
}
}
public final class DeviceContact: Equatable {
public let id: String
public let firstName: String
public let lastName: String
public let phoneNumbers: [DeviceContactPhoneNumber]
public init(id: String, firstName: String, lastName: String, phoneNumbers: [DeviceContactPhoneNumber]) {
self.id = id
self.firstName = firstName
self.lastName = lastName
self.phoneNumbers = phoneNumbers
}
public static func ==(lhs: DeviceContact, rhs: DeviceContact) -> Bool {
if lhs.id != rhs.id {
return false
}
if lhs.firstName != rhs.firstName {
return false
}
if lhs.lastName != rhs.lastName {
return false
}
if lhs.phoneNumbers != rhs.phoneNumbers {
return false
}
return true
}
}
@@ -0,0 +1,137 @@
import Postbox
import TelegramApi
import SwiftSignalKit
func _internal_importContact(account: Account, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity]) -> Signal<PeerId?, NoError> {
let accountPeerId = account.peerId
var flags: Int32 = 0
var note: Api.TextWithEntities?
if !noteText.isEmpty {
flags |= (1 << 1)
note = .textWithEntities(text: noteText, entities: apiEntitiesFromMessageTextEntities(noteEntities, associatedPeers: SimpleDictionary()))
}
let input = Api.InputContact.inputPhoneContact(flags: 0, clientId: 1, phone: phoneNumber, firstName: firstName, lastName: lastName, note: note)
return account.network.request(Api.functions.contacts.importContacts(contacts: [input]))
|> map(Optional.init)
|> `catch` { _ -> Signal<Api.contacts.ImportedContacts?, NoError> in
return .single(nil)
}
|> mapToSignal { result -> Signal<PeerId?, NoError> in
return account.postbox.transaction { transaction -> PeerId? in
if let result = result {
switch result {
case let .importedContacts(_, _, _, users):
if let first = users.first {
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: AccumulatedPeers(users: users))
let peerId = first.peerId
var peerIds = transaction.getContactPeerIds()
if !peerIds.contains(peerId) {
peerIds.insert(peerId)
transaction.replaceContactPeerIds(peerIds)
}
if !noteText.isEmpty {
transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, cachedData in
(cachedData as? CachedUserData)?.withUpdatedNote(.init(text: noteText, entities: noteEntities))
})
}
return peerId
}
}
}
return nil
}
}
}
public enum AddContactError {
case generic
}
func _internal_addContactInteractively(account: Account, peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity], addToPrivacyExceptions: Bool) -> Signal<Never, AddContactError> {
let accountPeerId = account.peerId
return account.postbox.transaction { transaction -> (Api.InputUser, String)? in
if let user = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(user) {
return (inputUser, user.phone == nil ? phoneNumber : "")
} else {
return nil
}
}
|> castError(AddContactError.self)
|> mapToSignal { inputUserAndPhone in
guard let (inputUser, phone) = inputUserAndPhone else {
return .fail(.generic)
}
var flags: Int32 = 0
if addToPrivacyExceptions {
flags |= (1 << 0)
}
var note: Api.TextWithEntities?
if !noteText.isEmpty {
flags |= (1 << 1)
note = .textWithEntities(text: noteText, entities: apiEntitiesFromMessageTextEntities(noteEntities, associatedPeers: SimpleDictionary()))
}
return account.network.request(Api.functions.contacts.addContact(flags: flags, id: inputUser, firstName: firstName, lastName: lastName, phone: phone, note: note))
|> mapError { _ -> AddContactError in
return .generic
}
|> mapToSignal { result -> Signal<Never, AddContactError> in
return account.postbox.transaction { transaction -> Void in
var peers = AccumulatedPeers()
switch result {
case let .updates(_, users, _, _, _):
peers = AccumulatedPeers(users: users)
case let .updatesCombined(_, users, _, _, _, _):
peers = AccumulatedPeers(users: users)
default:
break
}
updatePeers(transaction: transaction, accountPeerId: accountPeerId, peers: peers)
var peerIds = transaction.getContactPeerIds()
if !peerIds.contains(peerId) {
peerIds.insert(peerId)
transaction.replaceContactPeerIds(peerIds)
}
if !noteText.isEmpty {
transaction.updatePeerCachedData(peerIds: [peerId], update: { peerId, cachedData in
(cachedData as? CachedUserData)?.withUpdatedNote(.init(text: noteText, entities: noteEntities))
})
}
account.stateManager.addUpdates(result)
}
|> castError(AddContactError.self)
|> ignoreValues
}
}
}
public enum AcceptAndShareContactError {
case generic
}
func _internal_acceptAndShareContact(account: Account, peerId: PeerId) -> Signal<Never, AcceptAndShareContactError> {
return account.postbox.transaction { transaction -> Api.InputUser? in
return transaction.getPeer(peerId).flatMap(apiInputUser)
}
|> castError(AcceptAndShareContactError.self)
|> mapToSignal { inputUser -> Signal<Never, AcceptAndShareContactError> in
guard let inputUser = inputUser else {
return .fail(.generic)
}
return account.network.request(Api.functions.contacts.acceptContact(id: inputUser))
|> mapError { _ -> AcceptAndShareContactError in
return .generic
}
|> mapToSignal { updates -> Signal<Never, AcceptAndShareContactError> in
account.stateManager.addUpdates(updates)
return .complete()
}
}
}
@@ -0,0 +1,14 @@
public struct PhoneNumberWithLabel: Equatable {
public let label: String
public let number: String
public init(label: String, number: String) {
self.label = label
self.number = number
}
public static func ==(lhs: PhoneNumberWithLabel, rhs: PhoneNumberWithLabel) -> Bool {
return lhs.label == rhs.label && lhs.number == rhs.number
}
}
@@ -0,0 +1,66 @@
import Foundation
import Postbox
import SwiftSignalKit
private let phoneNumberKeyPrefix: ValueBoxKey = {
let result = ValueBoxKey(length: 1)
result.setInt8(0, value: 0)
return result
}()
enum TelegramDeviceContactImportIdentifier: Hashable, Comparable, Equatable {
case phoneNumber(DeviceContactNormalizedPhoneNumber)
init?(key: ValueBoxKey) {
if key.length < 2 {
return nil
}
switch key.getInt8(0) {
case 0:
guard let string = key.substringValue(1 ..< key.length) else {
return nil
}
self = .phoneNumber(DeviceContactNormalizedPhoneNumber(rawValue: string))
default:
return nil
}
}
var key: ValueBoxKey {
switch self {
case let .phoneNumber(number):
let numberKey = ValueBoxKey(number.rawValue)
return phoneNumberKeyPrefix + numberKey
}
}
static func <(lhs: TelegramDeviceContactImportIdentifier, rhs: TelegramDeviceContactImportIdentifier) -> Bool {
switch lhs {
case let .phoneNumber(lhsNumber):
switch rhs {
case let .phoneNumber(rhsNumber):
return lhsNumber.rawValue < rhsNumber.rawValue
}
}
}
}
func _internal_deviceContactsImportedByCount(postbox: Postbox, contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> {
return postbox.transaction { transaction -> [String: Int32] in
var result: [String: Int32] = [:]
for (id, numbers) in contacts {
var maxCount: Int32 = 0
for number in numbers {
if let value = transaction.getDeviceContactImportInfo(TelegramDeviceContactImportIdentifier.phoneNumber(number).key) as? TelegramDeviceContactImportedData, case let .imported(_, importedByCount, _) = value {
maxCount = max(maxCount, importedByCount)
}
}
if maxCount != 0 {
result[id] = maxCount
}
}
return result
}
}
@@ -0,0 +1,144 @@
import Foundation
import SwiftSignalKit
import Postbox
public extension TelegramEngine {
final class Contacts {
private let account: Account
init(account: Account) {
self.account = account
}
public func deleteContactPeerInteractively(peerId: PeerId) -> Signal<Never, NoError> {
return _internal_deleteContactPeerInteractively(account: self.account, peerId: peerId)
}
public func deleteContacts(peerIds: [PeerId]) -> Signal<Never, NoError> {
return _internal_deleteContacts(account: self.account, peerIds: peerIds)
}
public func deleteAllContacts() -> Signal<Never, NoError> {
return _internal_deleteAllContacts(account: self.account)
}
public func resetSavedContacts() -> Signal<Void, NoError> {
return _internal_resetSavedContacts(network: self.account.network)
}
public func updateContactName(peerId: PeerId, firstName: String, lastName: String) -> Signal<Void, UpdateContactNameError> {
return _internal_updateContactName(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName)
}
public func updateContactPhoto(peerId: PeerId, resource: MediaResource?, videoResource: MediaResource?, videoStartTimestamp: Double?, markup: UploadPeerPhotoMarkup?, mode: SetCustomPeerPhotoMode, mapResourceToAvatarSizes: @escaping (MediaResource, [TelegramMediaImageRepresentation]) -> Signal<[Int: Data], NoError>) -> Signal<UpdatePeerPhotoStatus, UploadPeerPhotoError> {
return _internal_updateContactPhoto(account: self.account, peerId: peerId, resource: resource, videoResource: videoResource, videoStartTimestamp: videoStartTimestamp, markup: markup, mode: mode, mapResourceToAvatarSizes: mapResourceToAvatarSizes)
}
public func updateContactNote(peerId: PeerId, text: String, entities: [MessageTextEntity]) -> Signal<Never, UpdateContactNoteError> {
return _internal_updateContactNote(account: self.account, peerId: peerId, text: text, entities: entities)
}
public func deviceContactsImportedByCount(contacts: [(String, [DeviceContactNormalizedPhoneNumber])]) -> Signal<[String: Int32], NoError> {
return _internal_deviceContactsImportedByCount(postbox: self.account.postbox, contacts: contacts)
}
public func importContact(firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity]) -> Signal<PeerId?, NoError> {
return _internal_importContact(account: self.account, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, noteText: noteText, noteEntities: noteEntities)
}
public func addContactInteractively(peerId: PeerId, firstName: String, lastName: String, phoneNumber: String, noteText: String, noteEntities: [MessageTextEntity], addToPrivacyExceptions: Bool) -> Signal<Never, AddContactError> {
return _internal_addContactInteractively(account: self.account, peerId: peerId, firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, noteText: noteText, noteEntities: noteEntities, addToPrivacyExceptions: addToPrivacyExceptions)
}
public func acceptAndShareContact(peerId: PeerId) -> Signal<Never, AcceptAndShareContactError> {
return _internal_acceptAndShareContact(account: self.account, peerId: peerId)
}
public func searchRemotePeers(query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<([FoundPeer], [FoundPeer]), NoError> {
return _internal_searchPeers(accountPeerId: self.account.peerId, postbox: self.account.postbox, network: self.account.network, query: query, scope: scope)
}
public func searchLocalPeers(query: String, scope: TelegramSearchPeersScope = .everywhere) -> Signal<[EngineRenderedPeer], NoError> {
return self.account.postbox.searchPeers(query: query)
|> map { peers in
switch scope {
case .everywhere:
return peers.map(EngineRenderedPeer.init)
case .channels:
return peers.filter { peer in
if let channel = peer.peer as? TelegramChannel, case .broadcast = channel.info {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .groups:
return peers.filter { item in
if let channel = item.peer as? TelegramChannel, case .group = channel.info {
return true
} else if item.peer is TelegramGroup {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .privateChats:
return peers.filter { item in
if item.peer is TelegramUser {
return true
} else {
return false
}
}.map(EngineRenderedPeer.init)
case .globalPosts:
return []
}
}
}
public func searchContacts(query: String) -> Signal<([EnginePeer], [EnginePeer.Id: EnginePeer.Presence]), NoError> {
return self.account.postbox.searchContacts(query: query)
|> map { peers, presences in
return (peers.map(EnginePeer.init), presences.mapValues(EnginePeer.Presence.init))
}
}
public func updateIsContactSynchronizationEnabled(isContactSynchronizationEnabled: Bool) -> Signal<Never, NoError> {
return self.account.postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { current in
var settings = current?.get(ContactsSettings.self) ?? ContactsSettings.defaultSettings
settings.synchronizeContacts = isContactSynchronizationEnabled
return PreferencesEntry(settings)
})
}
|> ignoreValues
}
public func findPeerByLocalContactIdentifier(identifier: String) -> Signal<EnginePeer?, NoError> {
return self.account.postbox.transaction { transaction -> EnginePeer? in
var foundPeerId: PeerId?
transaction.enumerateDeviceContactImportInfoItems({ _, value in
if let value = value as? TelegramDeviceContactImportedData {
switch value {
case let .imported(data, _, peerId):
if data.localIdentifiers.contains(identifier) {
if let peerId = peerId {
foundPeerId = peerId
return false
}
}
default:
break
}
}
return true
})
if let foundPeerId = foundPeerId {
return transaction.getPeer(foundPeerId).flatMap(EnginePeer.init)
} else {
return nil
}
}
}
}
}
@@ -0,0 +1,58 @@
import Foundation
import Postbox
import SwiftSignalKit
import TelegramApi
import MtProtoKit
public enum UpdateContactNameError {
case generic
}
func _internal_updateContactName(account: Account, peerId: PeerId, firstName: String, lastName: String) -> Signal<Void, UpdateContactNameError> {
return account.postbox.transaction { transaction -> Signal<Void, UpdateContactNameError> in
if let peer = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.addContact(flags: 0, id: inputUser, firstName: firstName, lastName: lastName, phone: "", note: nil))
|> mapError { _ -> UpdateContactNameError in
return .generic
}
|> mapToSignal { result -> Signal<Void, UpdateContactNameError> in
account.stateManager.addUpdates(result)
return .complete()
}
} else {
return .fail(.generic)
}
}
|> mapError { _ -> UpdateContactNameError in }
|> switchToLatest
}
public enum UpdateContactNoteError {
case generic
}
func _internal_updateContactNote(account: Account, peerId: PeerId, text: String, entities: [MessageTextEntity]) -> Signal<Never, UpdateContactNoteError> {
return account.postbox.transaction { transaction -> Signal<Void, UpdateContactNoteError> in
if let peer = transaction.getPeer(peerId) as? TelegramUser, let inputUser = apiInputUser(peer) {
return account.network.request(Api.functions.contacts.updateContactNote(id: inputUser, note: .textWithEntities(text: text, entities: apiEntitiesFromMessageTextEntities(entities, associatedPeers: SimpleDictionary()))))
|> mapError { _ -> UpdateContactNoteError in
return .generic
}
|> mapToSignal { result -> Signal<Void, UpdateContactNoteError> in
return account.postbox.transaction { transaction in
transaction.updatePeerCachedData(peerIds: Set([peerId]), update: { peerId, cachedData in
let cachedData = cachedData as? CachedUserData ?? CachedUserData()
return cachedData.withUpdatedNote(!text.isEmpty ? CachedUserData.Note(text: text, entities: entities) : nil)
})
}
|> castError(UpdateContactNoteError.self)
}
} else {
return .fail(.generic)
}
}
|> mapError { _ -> UpdateContactNoteError in }
|> switchToLatest
|> ignoreValues
}