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,194 @@
import Foundation
class LegacyBuffer: CustomStringConvertible {
var data: UnsafeMutableRawPointer?
var _size: UInt = 0
private var capacity: UInt = 0
private let freeWhenDone: Bool
var size: Int {
return Int(self._size)
}
deinit {
if self.freeWhenDone {
free(self.data)
}
}
init(memory: UnsafeMutableRawPointer?, size: Int, capacity: Int, freeWhenDone: Bool) {
self.data = memory
self._size = UInt(size)
self.capacity = UInt(capacity)
self.freeWhenDone = freeWhenDone
}
init() {
self.data = nil
self._size = 0
self.capacity = 0
self.freeWhenDone = true
}
convenience init(data: Data?) {
self.init()
if let data = data {
data.withUnsafeBytes { bytes in
self.appendBytes(bytes, length: UInt(data.count))
}
}
}
func makeData() -> Data {
return self.withUnsafeMutablePointer { pointer, size -> Data in
if let pointer = pointer {
return Data(bytes: pointer.assumingMemoryBound(to: UInt8.self), count: Int(size))
} else {
return Data()
}
}
}
var description: String {
get {
var string = ""
if let data = self.data {
var i: UInt = 0
let bytes = data.assumingMemoryBound(to: UInt8.self)
while i < _size && i < 8 {
string += String(format: "%02x", Int(bytes.advanced(by: Int(i)).pointee))
i += 1
}
if i < _size {
string += "...\(_size)b"
}
} else {
string += "<null>"
}
return string
}
}
func appendBytes(_ bytes: UnsafeRawPointer, length: UInt) {
if self.capacity < self._size + length {
self.capacity = self._size + length + 128
if self.data == nil {
self.data = malloc(Int(self.capacity))!
}
else {
self.data = realloc(self.data, Int(self.capacity))!
}
}
memcpy(self.data?.advanced(by: Int(self._size)), bytes, Int(length))
self._size += length
}
func appendBuffer(_ buffer: LegacyBuffer) {
if self.capacity < self._size + buffer._size {
self.capacity = self._size + buffer._size + 128
if self.data == nil {
self.data = malloc(Int(self.capacity))!
}
else {
self.data = realloc(self.data, Int(self.capacity))!
}
}
memcpy(self.data?.advanced(by: Int(self._size)), buffer.data, Int(buffer._size))
}
func appendInt32(_ value: Int32) {
var v = value
self.appendBytes(&v, length: 4)
}
func appendInt64(_ value: Int64) {
var v = value
self.appendBytes(&v, length: 8)
}
func appendDouble(_ value: Double) {
var v = value
self.appendBytes(&v, length: 8)
}
func withUnsafeMutablePointer<R>(_ f: (UnsafeMutableRawPointer?, UInt) -> R) -> R {
return f(self.data, self._size)
}
}
class LegacyBufferReader {
private let buffer: LegacyBuffer
private(set) var offset: UInt = 0
init(_ buffer: LegacyBuffer) {
self.buffer = buffer
}
func reset() {
self.offset = 0
}
func skip(_ count: Int) {
self.offset = min(self.buffer._size, self.offset + UInt(count))
}
func readInt32() -> Int32? {
if self.offset + 4 <= self.buffer._size {
let value: Int32 = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Int32.self).pointee
self.offset += 4
return value
}
return nil
}
func readInt64() -> Int64? {
if self.offset + 8 <= self.buffer._size {
let value: Int64 = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Int64.self).pointee
self.offset += 8
return value
}
return nil
}
func readDouble() -> Double? {
if self.offset + 8 <= self.buffer._size {
let value: Double = buffer.data!.advanced(by: Int(self.offset)).assumingMemoryBound(to: Double.self).pointee
self.offset += 8
return value
}
return nil
}
func readBytesAsInt32(_ count: Int) -> Int32? {
if count == 0 {
return 0
}
else if count > 0 && count <= 4 || self.offset + UInt(count) <= self.buffer._size {
var value: Int32 = 0
memcpy(&value, self.buffer.data?.advanced(by: Int(self.offset)), count)
self.offset += UInt(count)
return value
}
return nil
}
func readBuffer(_ count: Int) -> LegacyBuffer? {
if count >= 0 && self.offset + UInt(count) <= self.buffer._size {
let buffer = LegacyBuffer()
buffer.appendBytes((self.buffer.data?.advanced(by: Int(self.offset)))!, length: UInt(count))
self.offset += UInt(count)
return buffer
}
return nil
}
func withReadBufferNoCopy<T>(_ count: Int, _ f: (LegacyBuffer) -> T) -> T? {
if count >= 0 && self.offset + UInt(count) <= self.buffer._size {
return f(LegacyBuffer(memory: self.buffer.data!.advanced(by: Int(self.offset)), size: count, capacity: count, freeWhenDone: false))
}
return nil
}
}
@@ -0,0 +1,780 @@
import Foundation
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import LegacyComponents
private let reportedLayer_hash: Int32 = -717538193
private let layer_hash: Int32 = 849537378
private let seq_out_hash: Int32 = -737765753
private let seq_in_hash: Int32 = -7646011
private let defaultPrime: Data = {
let bytes: [UInt8] = [
0xc7, 0x1c, 0xae, 0xb9, 0xc6, 0xb1, 0xc9, 0x04, 0x8e, 0x6c, 0x52, 0x2f,
0x70, 0xf1, 0x3f, 0x73, 0x98, 0x0d, 0x40, 0x23, 0x8e, 0x3e, 0x21, 0xc1,
0x49, 0x34, 0xd0, 0x37, 0x56, 0x3d, 0x93, 0x0f, 0x48, 0x19, 0x8a, 0x0a,
0xa7, 0xc1, 0x40, 0x58, 0x22, 0x94, 0x93, 0xd2, 0x25, 0x30, 0xf4, 0xdb,
0xfa, 0x33, 0x6f, 0x6e, 0x0a, 0xc9, 0x25, 0x13, 0x95, 0x43, 0xae, 0xd4,
0x4c, 0xce, 0x7c, 0x37, 0x20, 0xfd, 0x51, 0xf6, 0x94, 0x58, 0x70, 0x5a,
0xc6, 0x8c, 0xd4, 0xfe, 0x6b, 0x6b, 0x13, 0xab, 0xdc, 0x97, 0x46, 0x51,
0x29, 0x69, 0x32, 0x84, 0x54, 0xf1, 0x8f, 0xaf, 0x8c, 0x59, 0x5f, 0x64,
0x24, 0x77, 0xfe, 0x96, 0xbb, 0x2a, 0x94, 0x1d, 0x5b, 0xcd, 0x1d, 0x4a,
0xc8, 0xcc, 0x49, 0x88, 0x07, 0x08, 0xfa, 0x9b, 0x37, 0x8e, 0x3c, 0x4f,
0x3a, 0x90, 0x60, 0xbe, 0xe6, 0x7c, 0xf9, 0xa4, 0xa4, 0xa6, 0x95, 0x81,
0x10, 0x51, 0x90, 0x7e, 0x16, 0x27, 0x53, 0xb5, 0x6b, 0x0f, 0x6b, 0x41,
0x0d, 0xba, 0x74, 0xd8, 0xa8, 0x4b, 0x2a, 0x14, 0xb3, 0x14, 0x4e, 0x0e,
0xf1, 0x28, 0x47, 0x54, 0xfd, 0x17, 0xed, 0x95, 0x0d, 0x59, 0x65, 0xb4,
0xb9, 0xdd, 0x46, 0x58, 0x2d, 0xb1, 0x17, 0x8d, 0x16, 0x9c, 0x6b, 0xc4,
0x65, 0xb0, 0xd6, 0xff, 0x9c, 0xa3, 0x92, 0x8f, 0xef, 0x5b, 0x9a, 0xe4,
0xe4, 0x18, 0xfc, 0x15, 0xe8, 0x3e, 0xbe, 0xa0, 0xf8, 0x7f, 0xa9, 0xff,
0x5e, 0xed, 0x70, 0x05, 0x0d, 0xed, 0x28, 0x49, 0xf4, 0x7b, 0xf9, 0x59,
0xd9, 0x56, 0x85, 0x0c, 0xe9, 0x29, 0x85, 0x1f, 0x0d, 0x81, 0x15, 0xf6,
0x35, 0xb1, 0x05, 0xee, 0x2e, 0x4e, 0x15, 0xd0, 0x4b, 0x24, 0x54, 0xbf,
0x6f, 0x4f, 0xad, 0xf0, 0x34, 0xb1, 0x04, 0x03, 0x11, 0x9c, 0xd8, 0xe3,
0xb9, 0x2f, 0xcc, 0x5b
]
var data = Data(count: bytes.count)
data.withUnsafeMutableBytes { (dst: UnsafeMutablePointer<UInt8>) -> Void in
for i in 0 ..< bytes.count {
dst.advanced(by: i).pointee = bytes[i]
}
}
return data
}()
@objc(TGEncryptionKeyData) private final class TGEncryptionKeyData: NSObject, NSCoding {
let keyId: Int64
let key: Data
let firstSeqOut: Int32
init?(coder aDecoder: NSCoder) {
self.keyId = aDecoder.decodeInt64(forKey: "keyId")
self.key = (aDecoder.decodeObject(forKey: "key") as? Data) ?? Data()
self.firstSeqOut = aDecoder.decodeInt32(forKey: "firstSeqOut")
}
func encode(with aCoder: NSCoder) {
assertionFailure()
}
}
private struct SecretChatData {
let accessHash: Int64
let handshakeState: Int32
let rekeyState: SecretChatRekeySessionState?
}
private func readSecretChatParticipantData(accountPeerId: PeerId, data: Data) -> (SecretChatRole, PeerId)? {
let reader = LegacyBufferReader(LegacyBuffer(data: data))
guard reader.readInt32() == Int32(bitPattern: 0xabcdef12) else {
return nil
}
guard let formatVersion = reader.readInt32(), formatVersion >= 2 else {
return nil
}
reader.skip(4)
guard let adminId = reader.readInt32() else {
return nil
}
guard let count = reader.readInt32() else {
return nil
}
var ids: [Int32] = []
for _ in 0 ..< Int(count) {
guard let id = reader.readInt32() else {
return nil
}
reader.skip(4)
reader.skip(4)
ids.append(id)
}
guard let otherPeerId = ids.first else {
return nil
}
return (adminId == accountPeerId.id ? .creator : .participant, PeerId(namespace: Namespaces.Peer.CloudUser, id: otherPeerId))
}
private func readSecretChatData(reader: LegacyBufferReader) -> SecretChatData? {
guard let version = reader.readBytesAsInt32(1) else {
return nil
}
if version != 3 {
return nil
}
reader.skip(8)
guard let accessHash = reader.readInt64() else {
return nil
}
guard let _ = reader.readInt64() else {
return nil
}
guard let handshakeState = reader.readInt32() else {
return nil
}
guard let currentRekeyExchangeId = reader.readInt64() else {
return nil
}
guard let currentRekeyIsInitiatedByLocalClient = reader.readBytesAsInt32(1) else {
return nil
}
guard let currentRekeyNumberLength = reader.readInt32() else {
return nil
}
var currentRekeyNumber: Data?
if currentRekeyNumberLength > 0 {
guard let value = reader.readBuffer(Int(currentRekeyNumberLength))?.makeData() else {
return nil
}
currentRekeyNumber = value
}
guard let currentRekeyKeyLength = reader.readInt32() else {
return nil
}
var currentRekeyKey: Data?
if currentRekeyKeyLength > 0 {
guard let value = reader.readBuffer(Int(currentRekeyKeyLength))?.makeData() else {
return nil
}
currentRekeyKey = value
}
guard let currentRekeyKeyId = reader.readInt64() else {
return nil
}
var rekeyState: SecretChatRekeySessionState?
if currentRekeyExchangeId != 0 {
let innerState: SecretChatRekeySessionData?
if currentRekeyIsInitiatedByLocalClient != 0, let currentRekeyNumber = currentRekeyNumber {
innerState = .requested(a: MemoryBuffer(data: currentRekeyNumber), config: SecretChatEncryptionConfig(g: 3, p: MemoryBuffer(data: defaultPrime), version: 0))
} else if currentRekeyIsInitiatedByLocalClient == 0, let currentRekeyKey = currentRekeyKey, currentRekeyKeyId != 0 {
innerState = .accepted(key: MemoryBuffer(data: currentRekeyKey), keyFingerprint: currentRekeyKeyId)
} else {
innerState = nil
}
if let innerState = innerState {
rekeyState = SecretChatRekeySessionState(id: currentRekeyExchangeId, data: innerState)
}
}
return SecretChatData(accessHash: accessHash, handshakeState: handshakeState, rekeyState: rekeyState)
}
let registeredAttachmentParsers: Bool = {
let parsers: [(Int32, TGMediaAttachmentParser)] = [
(TGActionMediaAttachmentType, TGActionMediaAttachment()),
(TGImageMediaAttachmentType, TGImageMediaAttachment()),
(TGLocationMediaAttachmentType, TGLocationMediaAttachment()),
(TGVideoMediaAttachmentType, TGVideoMediaAttachment()),
(Int32(bitPattern: 0xB90A5663), TGContactMediaAttachment()),
(Int32(bitPattern: 0xE6C64318), TGDocumentMediaAttachment()),
(TGAudioMediaAttachmentType, TGAudioMediaAttachment()),
(Int32(bitPattern: 0x8C2E3CCE), TGMessageEntitiesAttachment()),
(Int32(bitPattern: 0x944DE6B6), TGLocalMessageMetaMediaAttachment()),
(TGAuthorSignatureMediaAttachmentType, TGAuthorSignatureMediaAttachment()),
(TGInvoiceMediaAttachmentType, TGInvoiceMediaAttachment()),
(TGGameAttachmentType, TGGameMediaAttachment()),
(Int32(bitPattern: 0xA3F4C8F5), TGViaUserAttachment()),
(TGBotContextResultAttachmentType, TGBotContextResultAttachment()),
(TGReplyMarkupAttachmentType, TGReplyMarkupAttachment()),
(TGWebPageMediaAttachmentType, TGWebPageMediaAttachment()),
(TGReplyMessageMediaAttachmentType, TGReplyMessageMediaAttachment()),
(TGAudioMediaAttachmentType, TGAudioMediaAttachment()),
(Int32(bitPattern: 0xaa1050c1), TGForwardedMessageMediaAttachment())
]
for (id, parser) in parsers {
TGMessage.registerMediaAttachmentParser(id, parser: parser)
}
return true
}()
private func parseSecretChatData(peerId: PeerId, data: Data, unreadCount: Int32) -> (SecretChatData, [MessageId.Namespace: PeerReadState], Int32)? {
let reader = LegacyBufferReader(LegacyBuffer(data: data))
guard let magic = reader.readInt32() else {
return nil
}
var version: Int32 = 1
if magic == 0x7acde441 {
guard let value = reader.readInt32() else {
return nil
}
version = value
}
if version < 2 {
return nil
}
for _ in 0 ..< 3 {
guard let length = reader.readInt32() else {
return nil
}
reader.skip(Int(length))
}
guard let hasEncryptedData = reader.readBytesAsInt32(1), hasEncryptedData == 1 else {
return nil
}
guard let secretChatData = readSecretChatData(reader: reader) else {
return nil
}
reader.skip(4)
reader.skip(4)
reader.skip(8)
reader.skip(4)
reader.skip(4)
reader.skip(4)
reader.skip(4)
guard let maxReadDate = reader.readInt32() else {
return nil
}
guard let maxOutgoingReadDate = reader.readInt32() else {
return nil
}
guard let messageDate = reader.readInt32() else {
return nil
}
guard let minMessageDate = reader.readInt32() else {
return nil
}
let readStates: [MessageId.Namespace: PeerReadState] = [
Namespaces.Message.SecretIncoming: .indexBased(maxIncomingReadIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: 1), timestamp: maxReadDate), maxOutgoingReadIndex: MessageIndex.lowerBound(peerId: peerId), count: 0, markedUnread: false),
Namespaces.Message.Local: .indexBased(maxIncomingReadIndex: MessageIndex.lowerBound(peerId: peerId), maxOutgoingReadIndex: MessageIndex(id: MessageId(peerId: peerId, namespace: Namespaces.Message.Local, id: 1), timestamp: maxOutgoingReadDate), count: 0, markedUnread: false)
]
return (secretChatData, readStates, max(messageDate, minMessageDate))
}
private enum CustomPropertyKey {
case string(String)
case hash(Int32)
}
private func loadLegacyPeerCustomProperyData(database: SqliteInterface, peerId: Int64, key: CustomPropertyKey) -> Data? {
var propertiesData: Data?
database.select("SELECT custom_properties FROM peers_v29 WHERE pid=\(peerId)", { cursor in
propertiesData = cursor.getData(at: 0)
return false
})
if let propertiesData = propertiesData {
let keyHash: Int32
switch key {
case let .string(string):
keyHash = HashFunctions.murMurHash32(string)
case let .hash(hash):
keyHash = hash
}
let reader = LegacyBufferReader(LegacyBuffer(data: propertiesData))
guard let _ = reader.readInt32() else {
return nil
}
guard let count = reader.readInt32() else {
return nil
}
for _ in 0 ..< Int(count) {
guard let valueKey = reader.readInt32() else {
return nil
}
guard let valueLength = reader.readInt32() else {
return nil
}
if valueKey == keyHash {
return reader.readBuffer(Int(valueLength))?.makeData()
}
reader.skip(Int(valueLength))
}
}
return nil
}
private func loadLegacyPeerCustomProperyInt32(database: SqliteInterface, peerId: Int64, key: CustomPropertyKey) -> Int32? {
guard let data = loadLegacyPeerCustomProperyData(database: database, peerId: peerId, key: key), data.count == 4 else {
return nil
}
var result: Int32 = 0
withUnsafeMutablePointer(to: &result, { bytes -> Void in
data.copyBytes(to: UnsafeMutableRawPointer(bytes).assumingMemoryBound(to: UInt8.self), from: 0 ..< 4)
})
return result
}
private func loadLegacyMessages(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, peerId: PeerId, userPeerId: PeerId, database: SqliteInterface, conversationId: Int64, expectedTotalCount: Int32) -> Signal<Float, NoError> {
return Signal { subscriber in
subscriber.putNext(0.0)
var copyLocalFiles: [(MediaResource, String)] = []
var messages: [StoreMessage] = []
Logger.shared.log("loadLegacyMessages", "begin peerId \(peerId) conversationId \(conversationId) count \(expectedTotalCount)")
database.select("CREATE INDEX IF NOT EXISTS random_ids_mid ON random_ids_v29 (mid)", { _ in
return true
})
var messageIndex: Int32 = -1
let reportBase = max(1, expectedTotalCount / 100)
database.select("SELECT mid, message, media, from_id, dstate, date, flags, localMid, content_properties FROM messages_v29 WHERE cid=\(conversationId)", { cursor in
messageIndex += 1
#if DEBUG
//usleep(500000)
#endif
if messageIndex % reportBase == 0 {
subscriber.putNext(min(1.0, Float(messageIndex) / Float(expectedTotalCount)))
}
let messageId = cursor.getInt32(at: 0)
//Logger.shared.log("loadLegacyMessages", "import message \(messageId)")
var globallyUniqueId: Int64?
database.select("SELECT random_id FROM random_ids_v29 where mid=\(messageId)", { innerCursor in
globallyUniqueId = innerCursor.getInt64(at: 0)
return false
})
let text = cursor.getString(at: 1)
let fromId = cursor.getInt64(at: 3)
let deliveryState = cursor.getInt32(at: 4)
let timestamp = cursor.getInt32(at: 5)
let autoremoveTimeout = cursor.getInt32(at: 7)
let contentPropertiesData = cursor.getData(at: 8)
let parsedAuthorId: PeerId
let parsedId: StoreMessageId
var parsedFlags: StoreMessageFlags = []
var parsedAttributes: [MessageAttribute] = []
var parsedMedia: [Media] = []
var parsedGroupingKey: Int64?
if fromId == accountPeerId.id {
parsedAuthorId = accountPeerId
parsedId = .Partial(peerId, Namespaces.Message.Local)
} else {
parsedAuthorId = userPeerId
parsedId = .Partial(peerId, Namespaces.Message.SecretIncoming)
parsedFlags.insert(.Incoming)
}
if deliveryState != 0 {
return true
}
if !contentPropertiesData.isEmpty {
if let contentProperties = TGMessage.parseContentProperties(contentPropertiesData) {
for (_, value) in contentProperties {
if let value = value as? TGMessageGroupedIdContentProperty {
parsedGroupingKey = value.groupedId
}
}
}
}
//Logger.shared.log("loadLegacyMessages", "message \(messageId) read content properties")
let media = cursor.getData(at: 2)
if let mediaList = TGMessage.parseMediaAttachments(media) {
for item in mediaList {
if let item = item as? TGImageMediaAttachment {
let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64())
var representations: [TelegramMediaImageRepresentation] = []
if let allSizes = item.imageInfo?.allSizes() as? [String: NSValue] {
for (imageUrl, sizeValue) in allSizes {
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
var resourcePath: String?
if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) {
resource = updatedResource
copyLocalFiles.append((updatedResource, path))
resourcePath = path
} else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path {
copyLocalFiles.append((resource, path))
resourcePath = path
}
var dimensions = sizeValue.cgSizeValue
if let resourcePath = resourcePath, let image = UIImage(contentsOfFile: resourcePath) {
dimensions = image.size
}
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(dimensions), resource: resource, progressiveSizes: []))
}
}
if item.localImageId != 0 {
let fullSizePath = basePath + "/Documents/files/image-local-\(String(item.localImageId, radix: 16))/image.jpg"
if let image = UIImage(contentsOfFile: fullSizePath) {
let resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
copyLocalFiles.append((resource, fullSizePath))
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(image.size), resource: resource, progressiveSizes: []))
}
}
parsedMedia.append(TelegramMediaImage(imageId: mediaId, representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []))
} else if let item = item as? TGVideoMediaAttachment {
let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64())
var representations: [TelegramMediaImageRepresentation] = []
if let allSizes = item.thumbnailInfo?.allSizes() as? [String: NSValue] {
for (imageUrl, sizeValue) in allSizes {
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) {
resource = updatedResource
copyLocalFiles.append((updatedResource, path))
} else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path {
copyLocalFiles.append((resource, path))
}
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource, progressiveSizes: []))
}
}
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.Video(duration: Int(item.duration), size: PixelDimensions(item.dimensions), flags: item.roundMessage ? .instantRoundVideo : []))
var size: Int32 = 0
if let videoUrl = item.videoInfo?.url(withQuality: 1, actualQuality: nil, actualSize: &size) {
if let path = pathFromLegacyLocalVideoUrl(basePath: basePath, url: videoUrl) {
copyLocalFiles.append((resource, path))
} else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: videoUrl, type: .video) {
resource = updatedResource
copyLocalFiles.append((updatedResource, path))
} else if videoUrl.hasPrefix("file://"), let path = URL(string: videoUrl)?.path {
copyLocalFiles.append((resource, path))
}
}
parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size == 0 ? nil : Int(size), attributes: attributes))
} else if let item = item as? TGAudioMediaAttachment {
let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64())
let representations: [TelegramMediaImageRepresentation] = []
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
var attributes: [TelegramMediaFileAttribute] = []
attributes.append(.Audio(isVoice: true, duration: Int(item.duration), title: nil, performer: nil, waveform: nil))
let size: Int32 = item.fileSize
let audioUrl = item.audioUri ?? ""
if let path = pathFromLegacyLocalVideoUrl(basePath: basePath, url: audioUrl) {
copyLocalFiles.append((resource, path))
} else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: audioUrl, type: .audio) {
resource = updatedResource
copyLocalFiles.append((updatedResource, path))
} else if audioUrl.hasPrefix("file://"), let path = URL(string: audioUrl)?.path {
copyLocalFiles.append((resource, path))
}
parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: "audio/ogg", size: size == 0 ? nil : Int(size), attributes: attributes))
} else if let item = item as? TGDocumentMediaAttachment {
let mediaId = MediaId(namespace: Namespaces.Media.LocalImage, id: arc4random64())
var representations: [TelegramMediaImageRepresentation] = []
if let allSizes = (item.thumbnailInfo?.allSizes()) as? [String: NSValue] {
for (imageUrl, sizeValue) in allSizes {
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: imageUrl, type: .image) {
resource = updatedResource
copyLocalFiles.append((updatedResource, path))
} else if imageUrl.hasPrefix("file://"), let path = URL(string: imageUrl)?.path {
copyLocalFiles.append((resource, path))
} else if let updatedResource = resourceFromLegacyImageUrl(imageUrl) {
resource = updatedResource
copyLocalFiles.append((resource, pathFromLegacyImageUrl(basePath: basePath, url: imageUrl)))
}
representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(sizeValue.cgSizeValue), resource: resource, progressiveSizes: []))
}
}
var resource: TelegramMediaResource = LocalFileMediaResource(fileId: arc4random64())
var attributes: [TelegramMediaFileAttribute] = []
var fileName = "file"
if let itemAttributes = item.attributes {
for attribute in itemAttributes {
if let attribute = attribute as? TGDocumentAttributeFilename {
attributes.append(.FileName(fileName: attribute.filename ?? "file"))
fileName = attribute.filename ?? "file"
} else if let attribute = attribute as? TGDocumentAttributeAudio {
let title = attribute.title ?? ""
let performer = attribute.performer ?? ""
var waveform: MemoryBuffer?
if let data = attribute.waveform {
waveform = MemoryBuffer(data: data.bitstream()!)
}
attributes.append(.Audio(isVoice: attribute.isVoice, duration: Int(attribute.duration), title: title.isEmpty ? nil : title, performer: performer.isEmpty ? nil : performer, waveform: waveform))
} else if let _ = attribute as? TGDocumentAttributeAnimated {
attributes.append(.Animated)
} else if let attribute = attribute as? TGDocumentAttributeVideo {
attributes.append(.Video(duration: Int(attribute.duration), size: PixelDimensions(attribute.size), flags: attribute.isRoundMessage ? .instantRoundVideo : []))
} else if let attribute = attribute as? TGDocumentAttributeSticker {
var packReference: StickerPackReference?
if let reference = attribute.packReference as? TGStickerPackIdReference {
packReference = .id(id: reference.packId, accessHash: reference.packAccessHash)
} else if let reference = attribute.packReference as? TGStickerPackShortnameReference {
packReference = .name(reference.shortName ?? "")
}
attributes.append(.Sticker(displayText: attribute.alt ?? "", packReference: packReference, maskData: nil))
} else if let attribute = attribute as? TGDocumentAttributeImageSize {
attributes.append(.ImageSize(size: PixelDimensions(attribute.size)))
}
}
}
let documentUri = item.documentUri ?? ""
let size: Int32 = item.size
if documentUri.hasPrefix("file://"), let path = URL(string: documentUri)?.path {
copyLocalFiles.append((resource, path))
} else if let (path, updatedResource) = pathAndResourceFromEncryptedFileUrl(basePath: basePath, url: documentUri, type: .document(fileName: fileName)) {
resource = updatedResource
copyLocalFiles.append((resource, path))
} else if item.localDocumentId != 0 {
copyLocalFiles.append((resource, pathFromLegacyFile(basePath: basePath, fileId: item.localDocumentId, isLocal: true, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? "")))
} else if item.documentId != 0 {
copyLocalFiles.append((resource, pathFromLegacyFile(basePath: basePath, fileId: item.documentId, isLocal: false, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? "")))
}
parsedMedia.append(TelegramMediaFile(fileId: mediaId, partialReference: nil, resource: resource, previewRepresentations: representations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: item.mimeType ?? "application/octet-stream", size: size == 0 ? nil : Int(size), attributes: attributes))
} else if let item = item as? TGActionMediaAttachment {
if item.actionType == TGMessageActionEncryptedChatMessageLifetime, let actionData = item.actionData, let timeout = actionData["messageLifetime"] as? Int32 {
parsedMedia.append(TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(timeout)))
}
} else if let item = item as? TGContactMediaAttachment {
parsedMedia.append(TelegramMediaContact(firstName: item.firstName ?? "", lastName: item.lastName ?? "", phoneNumber: item.phoneNumber ?? "", peerId: nil, vCardData: nil))
} else if let item = item as? TGLocationMediaAttachment {
var venue: MapVenue?
if let v = item.venue {
venue = MapVenue(title: v.title ?? "", address: v.address ?? "", provider: v.provider == "" ? nil : v.provider, id: v.venueId == "" ? nil : v.venueId, type: v.type == "" ? nil : v.type)
}
parsedMedia.append(TelegramMediaMap(latitude: item.latitude, longitude: item.longitude, heading: nil, accuracyRadius: nil, geoPlace: nil, venue: venue, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil))
}
}
}
//Logger.shared.log("loadLegacyMessages", "message \(messageId) read media")
if autoremoveTimeout != 0 {
var countdownBeginTime: Int32?
database.select("SELECT date FROM selfdestruct_v29 where mid=\(messageId)", { innerCursor in
countdownBeginTime = innerCursor.getInt32(at: 0) - autoremoveTimeout
return false
})
parsedAttributes.append(AutoremoveTimeoutMessageAttribute(timeout: autoremoveTimeout, countdownBeginTime: countdownBeginTime))
}
let (parsedTags, parsedGlobalTags) = tagsForStoreMessage(incoming: parsedFlags.contains(.Incoming), attributes: parsedAttributes, media: parsedMedia, textEntities: nil, isPinned: false)
messages.append(StoreMessage(id: parsedId, globallyUniqueId: globallyUniqueId, groupingKey: parsedGroupingKey, threadId: nil, timestamp: timestamp, flags: parsedFlags, tags: parsedTags, globalTags: parsedGlobalTags, localTags: [], forwardInfo: nil, authorId: parsedAuthorId, text: text, attributes: parsedAttributes, media: parsedMedia))
//Logger.shared.log("loadLegacyMessages", "message \(messageId) completed")
return true
})
let disposable = (account.postbox.transaction { transaction -> Void in
//Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) storing messages")
let _ = transaction.addMessages(messages, location: .UpperHistoryBlock)
//Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) copying \(copyLocalFiles.count) files")
for (resource, path) in copyLocalFiles {
account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path)
}
Logger.shared.log("loadLegacyMessages", "conversation \(conversationId) done")
}).start(completed: {
subscriber.putCompletion()
})
return disposable
}
}
private func importChannelBroadcastPreferences(account: TemporaryAccount, basePath: String, database: SqliteInterface) -> Signal<Never, NoError> {
return deferred { () -> Signal<Never, NoError> in
var peerIds: [Int64] = []
database.select("SELECT cid FROM channel_conversations_v29", { cursor in
peerIds.append(cursor.getInt64(at: 0))
return true
})
var peerIdsWithMutedMessages: [Int64] = []
for peerId in peerIds {
if let data = loadLegacyPeerCustomProperyData(database: database, peerId: peerId, key: .hash(0x374BF349)), !data.isEmpty {
let reader = LegacyBufferReader(LegacyBuffer(data: data))
guard let version = reader.readBytesAsInt32(1) else {
continue
}
guard let _ = reader.readBytesAsInt32(1) else {
continue
}
if version >= 2 {
guard let messagesMuted = reader.readBytesAsInt32(1) else {
continue
}
if messagesMuted == 1 {
peerIdsWithMutedMessages.append(peerId)
}
}
}
}
return .complete()
}
}
func loadLegacySecretChats(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, database: SqliteInterface) -> Signal<Float, NoError> {
return deferred { () -> Signal<Float, NoError> in
var peerIdToConversationId: [PeerId: Int64] = [:]
database.select("SELECT encrypted_id, cid FROM encrypted_cids_v29", { cursor in
peerIdToConversationId[PeerId(namespace: Namespaces.Peer.SecretChat, id: cursor.getInt32(at: 0))] = cursor.getInt64(at: 1)
return true
})
var chatInfos: [(TelegramSecretChat, SecretChatState, Int32?, [MessageId.Namespace: PeerReadState], Int64)] = []
for (peerId, conversationId) in peerIdToConversationId {
database.select("SELECT chat_photo, unread_count, participants, date FROM convesations_v29 WHERE cid=\(conversationId)", { cursor in
guard let (secretChatData, readStates, minMessageDate) = parseSecretChatData(peerId: peerId, data: cursor.getData(at: 0), unreadCount: cursor.getInt32(at: 1)) else {
return false
}
guard let (role, userPeerId) = readSecretChatParticipantData(accountPeerId: accountPeerId, data: cursor.getData(at: 2)) else {
return false
}
let chatMessageDate = cursor.getInt32(at: 3)
let messageDate = min(minMessageDate, chatMessageDate)
let messageLifetime = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .string("messageLifetime")) ?? 0
let state: SecretChatState
var seqOut: Int32?
switch secretChatData.handshakeState {
case 1: //requested
guard let a = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("a")), !a.isEmpty else {
return false
}
state = SecretChatState(role: .creator, embeddedState: .handshake(.requested(g: 3, p: MemoryBuffer(data: defaultPrime), a: MemoryBuffer(data: a))), keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil)
case 2: //accepting
return false
case 3: //terminated
state = SecretChatState(role: .creator, embeddedState: .terminated, keychain: SecretChatKeychain(keys: []), keyFingerprint: nil, messageAutoremoveTimeout: nil)
case 4:
guard let sha1Fingerprint = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeySha1")) else {
return false
}
guard let sha256Fingerprint = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeySha256")) else {
return false
}
guard let keysData = loadLegacyPeerCustomProperyData(database: database, peerId: conversationId, key: .string("encryptionKeys")) else {
return false
}
guard let keysArray = NSKeyedUnarchiver.unarchiveObject(with: keysData) as? [TGEncryptionKeyData] else {
return false
}
let parsedKeys: [SecretChatKey] = keysArray.map({ key in
return SecretChatKey(fingerprint: key.keyId, key: MemoryBuffer(data: key.key), validity: .sequenceBasedIndexRange(fromCanonicalIndex: key.firstSeqOut), useCount: 1)
})
let requestedLayerValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(reportedLayer_hash)) ?? 0
let appliedSeqInValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(seq_in_hash)) ?? 0
guard let seqOutValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(seq_out_hash)) else {
return false
}
seqOut = seqOutValue
guard let activeLayerValue = loadLegacyPeerCustomProperyInt32(database: database, peerId: conversationId, key: .hash(layer_hash)) else {
return false
}
guard let activeLayer = SecretChatSequenceBasedLayer(rawValue: activeLayerValue) else {
return false
}
let rekeyState: SecretChatRekeySessionState? = secretChatData.rekeyState
let embeddedState: SecretChatEmbeddedState = .sequenceBasedLayer(SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: activeLayer, locallyRequestedLayer: requestedLayerValue == 0 ? nil : requestedLayerValue, remotelyRequestedLayer: nil), rekeyState: rekeyState, baseIncomingOperationIndex: 0, baseOutgoingOperationIndex: 0, topProcessedCanonicalIncomingOperationIndex: appliedSeqInValue == 0 ? nil : max(0, appliedSeqInValue - 1)))
state = SecretChatState(role: role, embeddedState: embeddedState, keychain: SecretChatKeychain(keys: parsedKeys), keyFingerprint: SecretChatKeyFingerprint(sha1: SecretChatKeySha1Fingerprint(digest: sha1Fingerprint), sha256: SecretChatKeySha256Fingerprint(digest: sha256Fingerprint)), messageAutoremoveTimeout: messageLifetime == 0 ? nil : messageLifetime)
default:
return false
}
let secretChat = TelegramSecretChat(id: peerId, creationDate: messageDate, regularPeerId: userPeerId, accessHash: secretChatData.accessHash, role: role, embeddedState: state.embeddedState.peerState, messageAutoremoveTimeout: messageLifetime == 0 ? nil : messageLifetime)
chatInfos.append((secretChat, state, seqOut, readStates, conversationId))
return false
})
}
var userPeers: [PeerId: Peer] = [:]
var presences: [PeerId: PeerPresence] = [:]
for info in chatInfos {
if let (peer, presence) = loadLegacyUser(database: database, id: info.0.regularPeerId.id) {
userPeers[peer.id] = peer
presences[peer.id] = presence
}
}
let storedChats = account.postbox.transaction { transaction -> Void in
updatePeers(transaction: transaction, peers: Array(userPeers.values), update: { _, updated in
return updated
})
transaction.updatePeerPresencesInternal(presences: presences, merge: { _, updated in return updated })
for (peer, state, seqOutValue, readStates, _) in chatInfos {
if userPeers[peer.regularPeerId] == nil {
continue
}
updatePeers(transaction: transaction, peers: [peer], update: { _, updated in
return updated
})
transaction.setPeerChatState(peer.id, state: state)
switch state.embeddedState {
case .sequenceBasedLayer:
if let seqOutValue = seqOutValue {
transaction.operationLogResetIndices(peerId: peer.id, tag: OperationLogTags.SecretOutgoing, nextTagLocalIndex: seqOutValue + 1)
}
default:
break
}
transaction.resetIncomingReadStates([peer.id: readStates])
}
}
|> ignoreValues
let _ = registeredAttachmentParsers
var countByConversationId: [Int64: Int32] = [:]
var totalCount: Int32 = 0
for info in chatInfos {
database.select("SELECT COUNT(*) FROM messages_v29 WHERE cid=\(info.4)", { cursor in
let count = cursor.getInt32(at: 0)
countByConversationId[info.4] = count
totalCount += count
return true
})
}
var storedMessagesSignals: Signal<Float, NoError> = .single(0.0)
var cumulativeCount: Int32 = 0
for info in chatInfos {
let localBaseline = cumulativeCount
let localCount = countByConversationId[info.4] ?? 0
storedMessagesSignals = storedMessagesSignals
|> then(
loadLegacyMessages(account: account, basePath: basePath, accountPeerId: accountPeerId, peerId: info.0.id, userPeerId: info.0.regularPeerId, database: database, conversationId: info.4, expectedTotalCount: localCount)
|> map { localProgress -> Float in
if totalCount <= 0 {
return 0.0
}
let globalCount = localBaseline + Int32(localProgress * Float(localCount))
return Float(globalCount) / Float(totalCount)
}
)
cumulativeCount += countByConversationId[info.4] ?? 0
}
return storedChats
|> map { _ -> Float in return 0.0 }
|> then(storedMessagesSignals)
}
}
@@ -0,0 +1,245 @@
import Foundation
import UIKit
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import MtProtoKit
import LegacyDataImportImpl
public enum AccountImportError: Error {
case generic
}
public enum AccountImportProgressType {
case generic
case messages
case media
}
private func importedAccountData(basePath: String, documentsPath: String, accountManager: AccountManager, account: TemporaryAccount, database: SqliteInterface) -> Signal<(AccountImportProgressType, Float), AccountImportError> {
return deferred { () -> Signal<(AccountImportProgressType, Float), AccountImportError> in
let keychain = MTFileBasedKeychain(name: "Telegram", documentsPath: documentsPath)
guard let masterDatacenterId = keychain.object(forKey: "defaultDatacenterId", group: "persistent") as? Int else {
return .fail(.generic)
}
let keychainContents = keychain.contents(forGroup: "persistent")
let importKeychain = account.postbox.transaction { transaction -> Void in
for (key, value) in keychainContents {
let data = NSKeyedArchiver.archivedData(withRootObject: value)
transaction.setKeychainEntry(data, forKey: "persistent" + ":" + key)
}
}
|> ignoreValues
|> castError(AccountImportError.self)
let importData = importPreferencesData(documentsPath: documentsPath, masterDatacenterId: Int32(masterDatacenterId), account: account, database: database)
|> mapToSignal { accountUserId -> Signal<(AccountImportProgressType, Float), AccountImportError> in
return importDatabaseData(accountManager: accountManager, account: account, basePath: basePath, database: database, accountUserId: accountUserId)
}
return importKeychain
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
|> then(importData)
}
}
private func importPreferencesData(documentsPath: String, masterDatacenterId: Int32, account: TemporaryAccount, database: SqliteInterface) -> Signal<Int32, AccountImportError> {
return deferred { () -> Signal<Int32, AccountImportError> in
let defaultsPath = documentsPath + "/standard.defaults"
var parsedAccountUserId: Int32?
if let data = try? Data(contentsOf: URL(fileURLWithPath: defaultsPath)), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any], let id = dict["telegraphUserId"] as? Int {
parsedAccountUserId = Int32(id)
}
if parsedAccountUserId == nil {
if let id = UserDefaults.standard.object(forKey: "telegraphUserId") as? Int {
parsedAccountUserId = Int32(id)
}
}
if let parsedAccountUserId = parsedAccountUserId {
return account.postbox.transaction { transaction -> Int32 in
transaction.setState(AuthorizedAccountState(isTestingEnvironment: false, masterDatacenterId: masterDatacenterId, peerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: parsedAccountUserId), state: nil))
return parsedAccountUserId
}
|> castError(AccountImportError.self)
} else {
return .fail(.generic)
}
}
}
private func importDatabaseData(accountManager: AccountManager, account: TemporaryAccount, basePath: String, database: SqliteInterface, accountUserId: Int32) -> Signal<(AccountImportProgressType, Float), AccountImportError> {
return deferred { () -> Signal<(AccountImportProgressType, Float), AccountImportError> in
var importedAccountUser: Signal<Never, AccountImportError> = .complete()
if let (user, presence) = loadLegacyUser(database: database, id: accountUserId) {
importedAccountUser = account.postbox.transaction { transaction -> Void in
updatePeers(transaction: transaction, peers: [user], update: { _, updated in updated })
transaction.updatePeerPresencesInternal(presences: [user.id: presence], merge: { _, updated in return updated })
}
|> ignoreValues
|> castError(AccountImportError.self)
}
let importedSecretChats = loadLegacySecretChats(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|> castError(AccountImportError.self)
/*let importedFiles = loadLegacyFiles(account: account, basePath: basePath, accountPeerId: PeerId(namespace: Namespaces.Peer.CloudUser, id: accountUserId), database: database)
|> castError(AccountImportError.self)*/
let importedLegacyPreferences = importLegacyPreferences(accountManager: accountManager, account: account, documentsPath: basePath + "/Documents", database: database)
|> castError(AccountImportError.self)
return importedAccountUser
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
|> then(
importedLegacyPreferences
|> map { _ -> (AccountImportProgressType, Float) in return (.generic, 0.0) }
)
|> then(
importedSecretChats
|> map { value -> (AccountImportProgressType, Float) in return (.messages, value) }
)
}
}
public enum ImportedLegacyAccountEvent {
case progress(AccountImportProgressType, Float)
case result(AccountRecordId?)
}
public func importedLegacyAccount(basePath: String, accountManager: AccountManager, encryptionParameters: ValueBoxEncryptionParameters, present: @escaping (UIViewController) -> Void) -> Signal<ImportedLegacyAccountEvent, AccountImportError> {
let queue = Queue()
return deferred { () -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
let documentsPath = basePath + "/Documents"
if FileManager.default.fileExists(atPath: documentsPath + "/importcompleted") {
return .single(.result(nil))
}
let unlockedDatabasePathAndKey: Signal<(String, Data?)?, AccountImportError>
if FileManager.default.fileExists(atPath: documentsPath + "/tgdata.db.y") {
let databasePath = documentsPath + "/tgdata.db.y"
let unlockDatabase = Signal<(String, Data?)?, AccountImportError> { subscriber in
let alertController = UIAlertController(title: nil, message: "Enter your passcode", preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "Enter", style: .default) { _ in
let passcode = alertController.textFields?[0].text
func checkPasscode(_ value: String) -> Bool {
guard let database = SqliteInterface(databasePath: databasePath) else {
return false
}
let key = value.data(using: .utf8)!
if !database.unlock(password: hexString(key).data(using: .utf8)!) {
return false
}
return true
}
if checkPasscode(passcode ?? "") {
subscriber.putNext((databasePath, (passcode ?? "").data(using: .utf8)!))
subscriber.putCompletion()
} else {
let alertController = UIAlertController(title: nil, message: "Invalid passcode. Please try again.", preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "OK", style: .default) { _ in
subscriber.putCompletion()
}
alertController.addAction(confirmAction)
present(alertController)
}
}
let cancelAction = UIAlertAction(title: "Skip", style: .cancel) { _ in
subscriber.putNext(nil)
subscriber.putCompletion()
}
alertController.addTextField { textField in
textField.placeholder = "Passcode"
}
alertController.addAction(confirmAction)
alertController.addAction(cancelAction)
present(alertController)
return EmptyDisposable
}
|> runOn(Queue.mainQueue())
unlockedDatabasePathAndKey = (unlockDatabase
|> mapToSignal { result -> Signal<(String, Data?)?, AccountImportError> in
if let result = result {
return .single(result)
} else {
let askAgain = Signal<(String, Data?)?, AccountImportError> { subscriber in
let alertController = UIAlertController(title: "Warning", message: "If you continue without entering your passcode, all your secret chats will be lost.", preferredStyle: .alert)
let confirmAction = UIAlertAction(title: "Skip", style: .destructive) { _ in
subscriber.putError(.generic)
}
let cancelAction = UIAlertAction(title: "Try Again", style: .cancel) { _ in
subscriber.putCompletion()
}
alertController.addAction(confirmAction)
alertController.addAction(cancelAction)
present(alertController)
return EmptyDisposable
}
|> runOn(Queue.mainQueue())
return askAgain
}
})
|> restart
|> take(1)
} else if FileManager.default.fileExists(atPath: documentsPath + "/tgdata.db") {
unlockedDatabasePathAndKey = .single((documentsPath + "/tgdata.db", nil))
} else {
return .single(.result(nil))
}
return unlockedDatabasePathAndKey
|> mapToSignal { pathAndKey -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
guard let pathAndKey = pathAndKey else {
return .fail(.generic)
}
guard let database = SqliteInterface(databasePath: pathAndKey.0) else {
return .fail(.generic)
}
if let key = pathAndKey.1 {
if !database.unlock(password: hexString(key).data(using: .utf8)!) {
return .fail(.generic)
}
}
return temporaryAccount(manager: accountManager, rootPath: rootPathForBasePath(basePath), encryptionParameters: encryptionParameters)
|> castError(AccountImportError.self)
|> mapToSignal { account -> Signal<ImportedLegacyAccountEvent, AccountImportError> in
let actions = importedAccountData(basePath: basePath, documentsPath: documentsPath, accountManager: accountManager, account: account, database: database)
var result = actions
|> map { typeAndProgress -> ImportedLegacyAccountEvent in
return .progress(typeAndProgress.0, typeAndProgress.1)
}
#if DEBUG
//result = result
//|> then(.never())
#endif
result = result
|> then(.single(.result(account.id)))
return result
}
}
}
|> runOn(queue)
}
@@ -0,0 +1,89 @@
import Foundation
import Display
import AsyncDisplayKit
import TelegramPresentationData
import RadialStatusNode
public protocol LegacyDataImportSplash: WindowCoveringView {
var progress: (AccountImportProgressType, Float) { get set }
var serviceAction: (() -> Void)? { get set }
}
private final class LegacyDataImportSplashImpl: WindowCoveringView, LegacyDataImportSplash {
private let theme: PresentationTheme?
private let strings: PresentationStrings?
public var progress: (AccountImportProgressType, Float) = (.generic, 0.0) {
didSet {
if self.progress.0 != oldValue.0 {
if let size = self.validSize {
switch self.progress.0 {
case .generic:
self.textNode.attributedText = NSAttributedString(string: self.strings?.AppUpgrade_Running ?? "Optimizing...", font: Font.regular(17.0), textColor: self.theme?.list.itemPrimaryTextColor ?? .black)
case .media:
self.textNode.attributedText = NSAttributedString(string: "Optimizing cache", font: Font.regular(17.0), textColor: self.theme?.list.itemPrimaryTextColor ?? .black)
case .messages:
self.textNode.attributedText = NSAttributedString(string: "Optimizing database", font: Font.regular(17.0), textColor: self.theme?.list.itemPrimaryTextColor ?? .black)
}
self.updateLayout(size)
}
}
self.progressNode.transitionToState(.progress(color: self.theme?.list.itemAccentColor ?? UIColor(rgb: 0x007ee5), lineWidth: 2.0, value: CGFloat(max(0.025, self.progress.1)), cancelEnabled: false, animateRotation: true), animated: false, completion: {})
}
}
public var serviceAction: (() -> Void)?
private let progressNode: RadialStatusNode
private let textNode: ImmediateTextNode
private var validSize: CGSize?
public init(theme: PresentationTheme?, strings: PresentationStrings?) {
self.theme = theme
self.strings = strings
self.progressNode = RadialStatusNode(backgroundNodeColor: theme?.list.plainBackgroundColor ?? .white)
self.textNode = ImmediateTextNode()
self.textNode.maximumNumberOfLines = 0
self.textNode.textAlignment = .center
self.textNode.attributedText = NSAttributedString(string: self.strings?.AppUpgrade_Running ?? "Optimizing...", font: Font.regular(17.0), textColor: self.theme?.list.itemPrimaryTextColor ?? .black)
super.init(frame: CGRect())
self.backgroundColor = self.theme?.list.plainBackgroundColor ?? .white
self.addSubnode(self.progressNode)
self.progressNode.isUserInteractionEnabled = false
self.addSubnode(self.textNode)
self.textNode.isUserInteractionEnabled = false
self.addGestureRecognizer(UILongPressGestureRecognizer(target: self, action: #selector(self.longPressGesture(_:))))
}
required public init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override public func updateLayout(_ size: CGSize) {
self.validSize = size
let progressSize = CGSize(width: 60.0, height: 60.0)
let textSize = self.textNode.updateLayout(CGSize(width: size.width - 20.0, height: .greatestFiniteMagnitude))
let progressFrame = CGRect(origin: CGPoint(x: floor((size.width - progressSize.width) / 2.0), y: floor((size.height - progressSize.height - 15.0 - textSize.height) / 2.0)), size: progressSize)
self.progressNode.frame = progressFrame
self.textNode.frame = CGRect(origin: CGPoint(x: floor((size.width - textSize.width) / 2.0), y: progressFrame.maxY + 15.0), size: textSize)
}
@objc private func longPressGesture(_ recognizer: UILongPressGestureRecognizer) {
if case .began = recognizer.state {
self.serviceAction?()
}
}
}
public func makeLegacyDataImportSplash(theme: PresentationTheme?, strings: PresentationStrings?) -> LegacyDataImportSplash {
return LegacyDataImportSplashImpl(theme: theme, strings: strings)
}
@@ -0,0 +1,189 @@
import Foundation
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import LegacyComponents
private func importMediaFromMessageData(_ data: Data, basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) {
if let message = TGMessage(keyValueCoder: PSKeyValueDecoder(data: data)) {
if let mediaAttachments = message.mediaAttachments {
importMediaFromMediaList(mediaAttachments, basePath: basePath, copyLocalFiles: &copyLocalFiles, cache: cache)
}
}
}
private func importMediaFromMediaData(_ data: Data, basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) {
if let mediaAttachments = TGMessage.parseMediaAttachments(data) {
importMediaFromMediaList(mediaAttachments, basePath: basePath, copyLocalFiles: &copyLocalFiles, cache: cache)
}
}
private func importMediaFromMediaList(_ mediaAttachments: [Any], basePath: String, copyLocalFiles: inout [(MediaResource, String)], cache: TGCache) {
for media in mediaAttachments {
if let media = media as? TGDocumentMediaAttachment {
var fileName = "file"
if let itemAttributes = media.attributes {
for attribute in itemAttributes {
if let attribute = attribute as? TGDocumentAttributeFilename {
fileName = attribute.filename ?? "file"
}
}
}
if media.documentId != 0 {
let filePath = pathFromLegacyFile(basePath: basePath, fileId: media.documentId, isLocal: false, fileName: TGDocumentMediaAttachment.safeFileName(forFileName: fileName) ?? "")
if FileManager.default.fileExists(atPath: filePath) {
copyLocalFiles.append((CloudDocumentMediaResource(datacenterId: Int(media.datacenterId), fileId: media.documentId, accessHash: media.accessHash, size: nil, fileReference: nil, fileName: nil), filePath))
}
}
} else if let media = media as? TGVideoMediaAttachment {
if media.videoId != 0, let videoUrl = media.videoInfo?.url(withQuality: 1, actualQuality: nil, actualSize: nil) {
if let (id, accessHash, datacenterId, path) = pathFromLegacyVideoUrl(basePath: basePath, url: videoUrl) {
copyLocalFiles.append((CloudDocumentMediaResource(datacenterId: Int(datacenterId), fileId: id, accessHash: accessHash, size: nil, fileReference: nil, fileName: nil), path))
}
}
} else if let media = media as? TGImageMediaAttachment {
if let allSizes = media.imageInfo?.allSizes() as? [String: NSValue] {
for (imageUrl, _) in allSizes {
if let path = cache.path(forCachedData: imageUrl), let resource = resourceFromLegacyImageUrl(imageUrl), FileManager.default.fileExists(atPath: path) {
copyLocalFiles.append((resource, path))
}
}
}
}
}
}
private func makeMessageSortKey(tag: Int32, conversationId: Int64, space: Int8, timestamp: Int32, messageId: Int32) -> Data {
let key = ValueBoxKey(length: 4 + 8 + 1 + 4 + 4)
key.setInt32(0, value: tag.byteSwapped)
key.setInt64(4, value: conversationId.byteSwapped)
key.setInt8(4 + 8, value: space)
key.setInt32(4 + 8 + 1, value: timestamp)
key.setInt32(4 + 8 + 1 + 4, value: messageId.byteSwapped)
return Data(bytes: key.memory, count: key.length)
}
func loadLegacyFiles(account: TemporaryAccount, basePath: String, accountPeerId: PeerId, database: SqliteInterface) -> Signal<Float, NoError> {
return Signal<Float, NoError> { subscriber in
let _ = registeredAttachmentParsers
subscriber.putNext(0.0)
var channelIds: [Int64] = []
database.select("SELECT DISTINCT cid FROM channel_message_tags_v29", { cursor in
channelIds.append(cursor.getInt64(at: 0))
return true
})
print(database.explain("SELECT DISTINCT cid FROM channel_message_tags_v29"))
var channelMessageIds: [(Int64, Int32)] = []
print(database.explain("SELECT mid FROM channel_message_tags_v29 WHERE tag_sort_key<100 AND tag_sort_key>0 ORDER BY tag_sort_key DESC LIMIT 4000"))
if !channelIds.isEmpty {
/*
TGSharedMediaCacheItemTypePhoto = 0,
TGSharedMediaCacheItemTypeVideo = 1,
TGSharedMediaCacheItemTypeFile = 2,
TGSharedMediaCacheItemTypePhotoVideo = 3,
TGSharedMediaCacheItemTypePhotoVideoFile = 4,
TGSharedMediaCacheItemTypeAudio = 5,
TGSharedMediaCacheItemTypeLink = 6,
TGSharedMediaCacheItemTypeSticker = 7,
TGSharedMediaCacheItemTypeGif = 8,
TGSharedMediaCacheItemTypeVoiceVideoMessage = 9
*/
let tags: [Int32] = [
2, // File
5, // Audio
3, // PhotoVideo
]
database.withStatement("SELECT mid FROM channel_message_tags_v29 WHERE tag_sort_key<? AND tag_sort_key>? ORDER BY tag_sort_key DESC LIMIT 4000", { select in
for channelId in channelIds {
for tag in tags {
select([.data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 0, timestamp: Int32.max - 1, messageId: 0)), .data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 0, timestamp: 0, messageId: 0))], { cursor in
channelMessageIds.append((channelId, cursor.getInt32(at: 0)))
return true
})
select([.data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 1, timestamp: Int32.max - 1, messageId: 0)), .data(makeMessageSortKey(tag: tag, conversationId: channelId, space: 1, timestamp: 0, messageId: 0))], { cursor in
channelMessageIds.append((channelId, cursor.getInt32(at: 0)))
return true
})
}
}
})
}
var chatMessageIds: [Int32] = []
let mediaTypes: [Int32] = [
1, // video
2, // image
3, // file
]
for type in mediaTypes {
database.select("SELECT mids FROM media_cache_v29 WHERE media_type=\(type) ORDER BY date DESC LIMIT 32000", { cursor in
let midsData = cursor.getData(at: 0)
let reader = LegacyBufferReader(LegacyBuffer(data: midsData))
while true {
if let mid = reader.readInt32() {
chatMessageIds.append(mid)
} else {
break
}
}
return true
})
}
var copyLocalFiles: [(MediaResource, String)] = []
let totalCount = channelMessageIds.count + chatMessageIds.count
let reportBase = max(1, totalCount / 100)
var itemIndex = -1
let cache = TGCache(cachesPath: basePath + "/Caches")!
if !channelMessageIds.isEmpty {
database.withStatement("SELECT data FROM channel_messages_v29 WHERE cid=? AND mid=?", { select in
for (peerId, messageId) in channelMessageIds {
itemIndex += 1
if itemIndex % reportBase == 0 {
subscriber.putNext(Float(itemIndex) / Float(totalCount))
}
select([.int64(peerId), .int32(messageId)], { cursor in
let data = cursor.getData(at: 0)
importMediaFromMessageData(data, basePath: basePath, copyLocalFiles: &copyLocalFiles, cache: cache)
return true
})
}
})
}
if !chatMessageIds.isEmpty {
database.withStatement("SELECT media FROM messages_v29 WHERE mid=?", { select in
for messageId in chatMessageIds {
itemIndex += 1
if itemIndex % reportBase == 0 {
subscriber.putNext(Float(itemIndex) / Float(totalCount))
}
select([.int32(messageId)], { cursor in
let data = cursor.getData(at: 0)
importMediaFromMediaData(data, basePath: basePath, copyLocalFiles: &copyLocalFiles, cache: cache)
return true
})
}
})
}
for (resource, path) in copyLocalFiles {
account.postbox.mediaBox.copyResourceData(resource.id, fromTempPath: path)
}
subscriber.putCompletion()
return EmptyDisposable
}
}
@@ -0,0 +1,439 @@
import Foundation
import UIKit
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import MtProtoKit
import TelegramUIPreferences
import LegacyComponents
import TelegramNotices
import LegacyDataImportImpl
@objc(TGPresentationState) private final class TGPresentationState: NSObject, NSCoding {
let pallete: Int32
let userInfo: Int32
let fontSize: Int32
init?(coder aDecoder: NSCoder) {
self.pallete = aDecoder.decodeInt32(forKey: "p")
self.userInfo = aDecoder.decodeInt32(forKey: "u")
self.fontSize = aDecoder.decodeInt32(forKey: "f")
}
func encode(with aCoder: NSCoder) {
assertionFailure()
}
}
private enum PreferencesProvider {
case dict([String: Any])
case standard(UserDefaults)
subscript(_ key: String) -> Any? {
get {
switch self {
case let .dict(dict):
return dict[key]
case let .standard(standard):
return standard.object(forKey: key)
}
}
}
}
private func loadLegacyCustomProperyData(database: SqliteInterface, key: String) -> Data? {
var result: Data?
database.select("SELECT value FROM service_v29 WHERE key=\(HashFunctions.murMurHash32(key))", { cursor in
result = cursor.getData(at: 0)
return false
})
return result
}
private func convertLegacyProxyPort(_ value: Int) -> Int32 {
if value < 0 {
return Int32(UInt16(bitPattern: Int16(clamping: value)))
} else {
return Int32(clamping: value)
}
}
func importLegacyPreferences(accountManager: AccountManager, account: TemporaryAccount, documentsPath: String, database: SqliteInterface) -> Signal<Never, NoError> {
return deferred { () -> Signal<Never, NoError> in
var presentationState: TGPresentationState?
if let value = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/presentation.dat") as? TGPresentationState {
presentationState = value
}
var autoNightPreferences: TGPresentationAutoNightPreferences?
if let value = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/autonight.dat") as? TGPresentationAutoNightPreferences {
autoNightPreferences = value
}
let autoDownloadPreferences: TGAutoDownloadPreferences? = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/autoDownload.pref") as? TGAutoDownloadPreferences
let preferencesProvider: PreferencesProvider
let defaultsPath = documentsPath + "/standard.defaults"
let standardPreferences = PreferencesProvider.standard(UserDefaults.standard)
if let data = try? Data(contentsOf: URL(fileURLWithPath: defaultsPath)), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any] {
preferencesProvider = .dict(dict)
} else {
preferencesProvider = standardPreferences
}
var showCallsTab: Bool?
if let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/enablecalls.tab")), !data.isEmpty {
showCallsTab = data.withUnsafeBytes { (bytes: UnsafePointer<Int8>) -> Bool in
return bytes.pointee != 0
}
}
let parsedAutoplayGifs: Bool? = preferencesProvider["autoPlayAnimations"] as? Bool
let soundEnabled: Bool? = preferencesProvider["soundEnabled"] as? Bool
let vibrationEnabled: Bool? = preferencesProvider["vibrationEnabled"] as? Bool
let bannerEnabled: Bool? = preferencesProvider["bannerEnabled"] as? Bool
let callsDataUsageMode: Int? = preferencesProvider["callsDataUsageMode"] as? Int
let callsDisableCallKit: Bool? = preferencesProvider["callsDisableCallKit"] as? Bool
let callsUseProxy: Bool? = preferencesProvider["callsUseProxy"] as? Bool
let contactsInhibitSync: Bool? = preferencesProvider["contactsInhibitSync"] as? Bool
let stickersSuggestMode: Int? = preferencesProvider["stickersSuggestMode"] as? Int
let allowSecretWebpages: Bool? = preferencesProvider["allowSecretWebpages"] as? Bool
let allowSecretWebpagesInitialized: Bool? = preferencesProvider["allowSecretWebpagesInitialized"] as? Bool
let secretInlineBotsInitialized: Bool? = preferencesProvider["secretInlineBotsInitialized"] as? Bool
let musicPlayerOrderType: Int? = standardPreferences["musicPlayerOrderType_v1"] as? Int
let musicPlayerRepeatType: Int? = standardPreferences["musicPlayerRepeatType_v1"] as? Int
let instantPageFontSize: Float? = standardPreferences["instantPage_fontMultiplier_v0"] as? Float
let instantPageFontSerif: Int? = standardPreferences["instantPage_fontSerif_v0"] as? Int
let instantPageTheme: Int? = standardPreferences["instantPage_theme_v0"] as? Int
let instantPageAutoNightMode: Int? = standardPreferences["instantPage_autoNightTheme_v0"] as? Int
let proxyList = NSKeyedUnarchiver.unarchiveObject(withFile: documentsPath + "/proxies.data") as? [TGProxyItem]
var selectedProxy: (ProxyServerSettings, Bool)?
if let data = loadLegacyCustomProperyData(database: database, key: "socksProxyData"), let dict = NSKeyedUnarchiver.unarchiveObject(with: data) as? [String: Any], let host = dict["ip"] as? String, let port = dict["port"] as? Int {
let inactive = (dict["inactive"] as? Bool) ?? true
var connection: ProxyServerConnection?
if let secretString = dict["secret"] as? String {
let secret = MTProxySecret.parse(secretString)
if let secret = secret {
connection = .mtp(secret: secret.serialize())
}
} else {
connection = .socks5(username: (dict["username"] as? String) ?? "", password: (dict["password"] as? String) ?? "")
}
if let connection = connection {
selectedProxy = (ProxyServerSettings(host: host, port: convertLegacyProxyPort(port), connection: connection), !inactive)
}
}
var passcodeChallenge: PostboxAccessChallengeData?
if let data = try? Data(contentsOf: URL(fileURLWithPath: documentsPath + "/x.y")) {
let reader = LegacyBufferReader(LegacyBuffer(data: data))
if let mode = reader.readBytesAsInt32(1), let length = reader.readInt32(), let passwordData = reader.readBuffer(Int(length))?.makeData(), let passwordText = String(data: passwordData, encoding: .utf8) {
var lockTimeout: Int32?
if let value = UserDefaults.standard.object(forKey: "Passcode_lockTimeout") as? Int {
if value == 0 {
lockTimeout = nil
} else {
lockTimeout = max(60, Int32(clamping: value))
}
} else {
lockTimeout = 1 * 60 * 60
}
if mode == 3 {
passcodeChallenge = .numericalPassword(value: passwordText)
} else if mode == 4 {
passcodeChallenge = PostboxAccessChallengeData.plaintextPassword(value: passwordText)
}
}
}
var passcodeEnableBiometrics: Bool = true
if let value = UserDefaults.standard.object(forKey: "Passcode_useTouchId") as? Bool {
passcodeEnableBiometrics = value
}
var localization: TGLocalization?
if let nativeDocumentsPath = NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true).first {
localization = NSKeyedUnarchiver.unarchiveObject(withFile: nativeDocumentsPath + "/localization") as? TGLocalization
}
return accountManager.transaction { transaction -> Signal<Void, NoError> in
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationThemeSettings, { current in
var settings = (current as? PresentationThemeSettings) ?? PresentationThemeSettings.defaultSettings
if let presentationState = presentationState {
switch presentationState.pallete {
case 1:
settings.theme = .builtin(.day)
if presentationState.userInfo != 0 {
//themeSpecificAccentColors: current.themeSpecificAccentColors
//settings.themeAccentColor = presentationState.userInfo
}
settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0xffffff)
case 2:
settings.theme = .builtin(.night)
settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0x000000)
case 3:
settings.theme = .builtin(.nightAccent)
settings.themeSpecificChatWallpapers[settings.theme.index] = .color(0x18222d)
default:
settings.theme = .builtin(.dayClassic)
settings.themeSpecificChatWallpapers[settings.theme.index] = .builtin(WallpaperSettings())
}
let fontSizeMap: [Int32: PresentationFontSize] = [
14: .extraSmall,
15: .small,
16: .medium,
17: .regular,
19: .large,
23: .extraLarge,
26: .extraLargeX2
]
settings.fontSize = fontSizeMap[presentationState.fontSize] ?? .regular
if presentationState.userInfo != 0 {
//themeSpecificAccentColors: current.themeSpecificAccentColors
//settings.themeAccentColor = presentationState.userInfo
}
}
if let autoNightPreferences = autoNightPreferences {
let nightTheme: PresentationBuiltinThemeReference
switch autoNightPreferences.preferredPalette {
case 1:
nightTheme = .night
default:
nightTheme = .nightAccent
}
switch autoNightPreferences.mode {
case TGPresentationAutoNightModeSunsetSunrise:
settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .timeBased(setting: .automatic(latitude: Double(autoNightPreferences.latitude), longitude: Double(autoNightPreferences.longitude), localizedName: autoNightPreferences.cachedLocationName)), theme: .builtin(nightTheme))
case TGPresentationAutoNightModeScheduled:
settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .timeBased(setting: .manual(fromSeconds: autoNightPreferences.scheduleStart, toSeconds: autoNightPreferences.scheduleEnd)), theme: .builtin(nightTheme))
case TGPresentationAutoNightModeBrightness:
settings.automaticThemeSwitchSetting = AutomaticThemeSwitchSetting(trigger: .brightness(threshold: Double(autoNightPreferences.brightnessThreshold)), theme: .builtin(nightTheme))
default:
break
}
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.automaticMediaDownloadSettings, { current in
var settings: MediaAutoDownloadSettings = current as? MediaAutoDownloadSettings ?? .defaultSettings
if let preferences = autoDownloadPreferences, !preferences.isDefaultPreferences() {
settings.cellular.enabled = !preferences.disabled
settings.wifi.enabled = !preferences.disabled
}
if let parsedAutoplayGifs = parsedAutoplayGifs {
settings.autoplayGifs = parsedAutoplayGifs
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.inAppNotificationSettings, { current in
var settings: InAppNotificationSettings = current as? InAppNotificationSettings ?? .defaultSettings
if let soundEnabled = soundEnabled {
settings.playSounds = soundEnabled
}
if let vibrationEnabled = vibrationEnabled {
settings.vibrate = vibrationEnabled
}
if let bannerEnabled = bannerEnabled {
settings.displayPreviews = bannerEnabled
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.voiceCallSettings, { current in
var settings: VoiceCallSettings = current as? VoiceCallSettings ?? .defaultSettings
if let callsDataUsageMode = callsDataUsageMode {
switch callsDataUsageMode {
case 1:
settings.dataSaving = .cellular
case 2:
settings.dataSaving = .always
default:
settings.dataSaving = .never
}
}
if let callsDisableCallKit = callsDisableCallKit, callsDisableCallKit {
settings.enableSystemIntegration = false
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.callListSettings, { current in
var settings: CallListSettings = current as? CallListSettings ?? .defaultSettings
if let showCallsTab = showCallsTab {
settings.showTab = showCallsTab
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.presentationPasscodeSettings, { current in
var settings: PresentationPasscodeSettings = current as? PresentationPasscodeSettings ?? .defaultSettings
if let passcodeChallenge = passcodeChallenge {
transaction.setAccessChallengeData(passcodeChallenge)
settings.enableBiometrics = passcodeEnableBiometrics
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.stickerSettings, { current in
var settings: StickerSettings = current as? StickerSettings ?? .defaultSettings
if let stickersSuggestMode = stickersSuggestMode {
switch stickersSuggestMode {
case 1:
settings.emojiStickerSuggestionMode = .installed
case 2:
settings.emojiStickerSuggestionMode = .none
default:
settings.emojiStickerSuggestionMode = .all
}
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.musicPlaybackSettings, { current in
var settings: MusicPlaybackSettings = current as? MusicPlaybackSettings ?? .defaultSettings
if let musicPlayerOrderType = musicPlayerOrderType {
switch musicPlayerOrderType {
case 1:
settings.order = .reversed
case 2:
settings.order = .random
default:
settings.order = .regular
}
}
if let musicPlayerRepeatType = musicPlayerRepeatType {
switch musicPlayerRepeatType {
case 1:
settings.looping = .all
case 2:
settings.looping = .item
default:
settings.looping = .none
}
}
return settings
})
transaction.updateSharedData(ApplicationSpecificSharedDataKeys.instantPagePresentationSettings, { current in
let settings: InstantPagePresentationSettings = current as? InstantPagePresentationSettings ?? .defaultSettings
if let instantPageFontSize = instantPageFontSize {
switch instantPageFontSize {
case 0.85:
settings.fontSize = .small
case 1.15:
settings.fontSize = .large
case 1.30:
settings.fontSize = .xlarge
case 1.50:
settings.fontSize = .xxlarge
default:
settings.fontSize = .standard
}
}
if let instantPageFontSerif = instantPageFontSerif {
settings.forceSerif = instantPageFontSerif == 1
}
if let instantPageTheme = instantPageTheme {
switch instantPageTheme {
case 1:
settings.themeType = .sepia
case 2:
settings.themeType = .gray
case 3:
settings.themeType = .dark
default:
settings.themeType = .light
}
}
if let instantPageAutoNightMode = instantPageAutoNightMode {
settings.autoNightMode = instantPageAutoNightMode == 1
}
return settings
})
if let localization = localization {
transaction.updateSharedData(SharedDataKeys.localizationSettings, { _ in
var entries: [LocalizationEntry] = []
for (key, value) in localization.dict() {
entries.append(LocalizationEntry.string(key: key, value: value))
}
return LocalizationSettings(primaryComponent: LocalizationComponent(languageCode: localization.code, localizedName: "", localization: Localization(version: 0, entries: entries), customPluralizationCode: nil), secondaryComponent: nil)
})
}
transaction.updateSharedData(SharedDataKeys.proxySettings, { current in
var settings: ProxySettings = current as? ProxySettings ?? .defaultSettings
if let callsUseProxy = callsUseProxy {
settings.useForCalls = callsUseProxy
}
if let proxyList = proxyList {
for item in proxyList {
let connection: ProxyServerConnection?
if item.isMTProxy, let secret = item.secret {
let parsedSecret = MTProxySecret.parse(secret)
if let parsedSecret = parsedSecret {
connection = .mtp(secret: parsedSecret.serialize())
} else {
connection = nil
}
} else if !item.isMTProxy {
connection = .socks5(username: item.username ?? "", password: item.password ?? "")
} else {
connection = nil
}
if let connection = connection {
settings.servers.append(ProxyServerSettings(host: item.server, port: convertLegacyProxyPort(Int(item.port)), connection: connection))
}
}
}
if let (server, active) = selectedProxy {
if !settings.servers.contains(server) {
settings.servers.insert(server, at: 0)
}
settings.activeServer = server
settings.enabled = active
}
return settings
})
if let secretInlineBotsInitialized = secretInlineBotsInitialized, secretInlineBotsInitialized {
ApplicationSpecificNotice.setSecretChatInlineBotUsage(transaction: transaction)
}
if let allowSecretWebpagesInitialized = allowSecretWebpagesInitialized, allowSecretWebpagesInitialized, let allowSecretWebpages = allowSecretWebpages {
ApplicationSpecificNotice.setSecretChatLinkPreviews(transaction: transaction, value: allowSecretWebpages)
}
return account.postbox.transaction { transaction -> Void in
transaction.updatePreferencesEntry(key: PreferencesKeys.contactsSettings, { current in
var settings = current as? ContactsSettings ?? ContactsSettings.defaultSettings
if let contactsInhibitSync = contactsInhibitSync, contactsInhibitSync {
settings.synchronizeContacts = false
}
return settings
})
}
}
|> switchToLatest
|> ignoreValues
}
}
@@ -0,0 +1,138 @@
import Foundation
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
import LegacyComponents
func resourceFromLegacyImageUrl(_ fileRef: String) -> TelegramMediaResource? {
if fileRef.isEmpty {
return nil
}
let components = fileRef.components(separatedBy: "_")
if components.count != 4 {
return nil
}
guard let datacenterId = Int32(components[0]) else {
return nil
}
guard let volumeId = Int64(components[1]) else {
return nil
}
guard let localId = Int32(components[2]) else {
return nil
}
guard let secret = Int64(components[3]) else {
return nil
}
return CloudFileMediaResource(datacenterId: Int(datacenterId), volumeId: volumeId, localId: localId, secret: secret, size: nil, fileReference: nil)
}
func pathFromLegacyImageUrl(basePath: String, url: String) -> String {
let cache = TGCache(cachesPath: basePath + "/Caches")!
return cache.path(forCachedData: url)
}
func pathFromLegacyVideoUrl(basePath: String, url: String) -> (id: Int64, accessHash: Int64, datacenterId: Int32, path: String)? {
if !url.hasPrefix("video:") {
return nil
}
//[videoInfo addVideoWithQuality:1 url:[[NSString alloc] initWithFormat:@"video:%lld:%lld:%d:%d", videoMedia.videoId, videoMedia.accessHash, concreteResult.document.datacenterId, concreteResult.document.size] size:concreteResult.document.size];
let components = url.components(separatedBy: ":")
if components.count != 5 {
return nil
}
guard let videoId = Int64(components[1]) else {
return nil
}
guard let accessHash = Int64(components[2]) else {
return nil
}
guard let datacenterId = Int32(components[3]) else {
return nil
}
let documentsPath = basePath + "/Documents"
let videoPath = documentsPath + "/video/remote\(String(videoId, radix: 16)).mov"
return (videoId, accessHash, datacenterId, videoPath)
}
func pathFromLegacyLocalVideoUrl(basePath: String, url: String) -> String? {
let documentsPath = basePath + "/Documents"
if !url.hasPrefix("local-video:") {
return nil
}
let videoPath = documentsPath + "/video/" + String(url[url.index(url.startIndex, offsetBy: "local-video:".count)...])
return videoPath
}
func pathFromLegacyFile(basePath: String, fileId: Int64, isLocal: Bool, fileName: String) -> String {
let documentsPath = basePath + "/Documents"
let filePath = documentsPath + "/files/" + (isLocal ? "local" : "") + "\(String(fileId, radix: 16))/\(fileName)"
return filePath
}
enum EncryptedFileType {
case image
case video
case document(fileName: String)
case audio
}
func pathAndResourceFromEncryptedFileUrl(basePath: String, url: String, type: EncryptedFileType) -> (String, TelegramMediaResource)? {
let cache = TGCache(cachesPath: basePath + "/Caches")!
if url.hasPrefix("encryptedThumbnail:") {
let path = cache.path(forCachedData: url)!
return (path, LocalFileMediaResource(fileId: arc4random64()))
}
if !url.hasPrefix("mt-encrypted-file://?") {
return nil
}
guard let dict = TGStringUtils.argumentDictionary(inUrlString: String(url[url.index(url.startIndex, offsetBy: "mt-encrypted-file://?".count)...])) else {
return nil
}
guard let idString = dict["id"] as? String, let id = Int64(idString) else {
return nil
}
guard let datacenterIdString = dict["dc"] as? String, let datacenterId = Int32(datacenterIdString) else {
return nil
}
guard let accessHashString = dict["accessHash"] as? String, let accessHash = Int64(accessHashString) else {
return nil
}
guard let sizeString = dict["size"] as? String, let size = Int32(sizeString) else {
return nil
}
guard let decryptedSizeString = dict["decryptedSize"] as? String, let decryptedSize = Int32(decryptedSizeString) else {
return nil
}
guard let keyFingerprintString = dict["fingerprint"] as? String, let _ = Int32(keyFingerprintString) else {
return nil
}
guard let keyString = dict["key"] as? String else {
return nil
}
let keyData = dataWithHexString(keyString)
guard keyData.count == 64 else {
return nil
}
let resource = SecretFileMediaResource(fileId: id, accessHash: accessHash, containerSize: size, decryptedSize: decryptedSize, datacenterId: Int(datacenterId), key: SecretFileEncryptionKey(aesKey: keyData.subdata(in: 0 ..< 32), aesIv: keyData.subdata(in: 32 ..< 64)))
let filePath: String
switch type {
case .video:
filePath = basePath + "Documents/video/remote\(String(id, radix: 16)).mov"
case .image:
filePath = cache.path(forCachedData: url)
case let .document(fileName):
filePath = basePath + "Documents/files/\(String(id, radix: 16))/\(TGDocumentMediaAttachment.safeFileName(forFileName: fileName)!)"
case .audio:
filePath = basePath + "Documents/audio/\(String(id, radix: 16))"
}
return (filePath, resource)
}
@@ -0,0 +1,48 @@
import Foundation
import UIKit
import TelegramCore
import SyncCore
import SwiftSignalKit
import Postbox
func loadLegacyUser(database: SqliteInterface, id: Int32) -> (TelegramUser, TelegramUserPresence)? {
var result: (TelegramUser, TelegramUserPresence)?
database.select("SELECT uid, first_name, last_name, phone_number, access_hash, photo_small, photo_big, last_seen, username FROM users_v29 WHERE uid=\(id)", { cursor in
let accessHash = cursor.getInt64(at: 4)
let firstName = cursor.getString(at: 1)
let lastName = cursor.getString(at: 2)
let username = cursor.getString(at: 8)
let phone = cursor.getString(at: 3)
let photoSmall = cursor.getString(at: 5)
let photoBig = cursor.getString(at: 6)
var photo: [TelegramMediaImageRepresentation] = []
if let resource = resourceFromLegacyImageUrl(photoSmall) {
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 80, height: 80), resource: resource, progressiveSizes: []))
}
if let resource = resourceFromLegacyImageUrl(photoBig) {
photo.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: 600, height: 600), resource: resource, progressiveSizes: []))
}
let user = TelegramUser(id: PeerId(namespace: Namespaces.Peer.CloudUser, id: cursor.getInt32(at: 0)), accessHash: accessHash == 0 ? nil : .personal(accessHash), firstName: firstName.isEmpty ? nil : firstName, lastName: lastName.isEmpty ? nil : lastName, username: username.isEmpty ? nil : username, phone: phone.isEmpty ? nil : phone, photo: photo, botInfo: nil, restrictionInfo: nil, flags: [])
let status: UserPresenceStatus
let lastSeen = cursor.getInt32(at: 7)
if lastSeen == -2 {
status = .recently
} else if lastSeen == -3 {
status = .lastWeek
} else if lastSeen == -4 {
status = .lastMonth
} else if lastSeen <= 0 {
status = .none
} else {
status = .present(until: lastSeen)
}
let presence = TelegramUserPresence(status: status, lastActivity: 0)
result = (user, presence)
return false
})
return result
}