import Foundation import Postbox import TelegramApi import EncryptionProvider private enum MessageParsingError: Error { case contentParsingError case unsupportedLayer case invalidChatState case alreadyProcessedMessageInSequenceBasedLayer case holesInSequenceBasedLayer case secretChatCorruption } enum SecretChatRekeyServiceAction { case pfsRequestKey(rekeySessionId: Int64, gA: MemoryBuffer) case pfsAcceptKey(rekeySessionId: Int64, gB: MemoryBuffer, keyFingerprint: Int64) case pfsAbortSession(rekeySessionId: Int64) case pfsCommitKey(rekeySessionId: Int64, keyFingerprint: Int64) } private enum SecretChatServiceAction { case deleteMessages(globallyUniqueIds: [Int64]) case clearHistory case reportLayerSupport(Int32) case markMessagesContentAsConsumed(globallyUniqueIds: [Int64]) case setMessageAutoremoveTimeout(Int32) case resendOperations(fromSeq: Int32, toSeq: Int32) case rekeyAction(SecretChatRekeyServiceAction) } private func parsedServiceAction(_ operation: SecretChatIncomingDecryptedOperation) -> SecretChatServiceAction? { guard let parsedLayer = SecretChatLayer(rawValue: operation.layer) else { return nil } switch parsedLayer { case .layer8: if let parsedObject = SecretApi8.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi8.DecryptedMessage { return SecretChatServiceAction(apiMessage) } case .layer46: if let parsedObject = SecretApi46.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi46.DecryptedMessage { return SecretChatServiceAction(apiMessage) } case .layer73: if let parsedObject = SecretApi73.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi73.DecryptedMessage { return SecretChatServiceAction(apiMessage) } case .layer101: if let parsedObject = SecretApi101.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi101.DecryptedMessage { return SecretChatServiceAction(apiMessage) } case .layer144: if let parsedObject = SecretApi144.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi144.DecryptedMessage { return SecretChatServiceAction(apiMessage) } } return nil } struct SecretChatOperationProcessResult { let addedMessages: [StoreMessage] } func processSecretChatIncomingDecryptedOperations(encryptionProvider: EncryptionProvider, mediaBox: MediaBox, transaction: Transaction, peerId: PeerId) -> SecretChatOperationProcessResult { if let state = transaction.getPeerChatState(peerId) as? SecretChatState, let peer = transaction.getPeer(peerId) as? TelegramSecretChat { var removeTagLocalIndices: [Int32] = [] var updatedState = state var couldNotResendRequestedMessages = false var maxAcknowledgedCanonicalOperationIndex: Int32? var updatedPeer = peer var addedMessages: [StoreMessage] = [] transaction.operationLogEnumerateEntries(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted, { entry in if let operation = entry.contents as? SecretChatIncomingDecryptedOperation, let serviceAction = parsedServiceAction(operation), case let .resendOperations(fromSeq, toSeq) = serviceAction { switch updatedState.role { case .creator: if fromSeq < 0 || toSeq < 0 || (fromSeq & 1) == 0 || (toSeq & 1) == 0 { return true } case .participant: if fromSeq < 0 || toSeq < 0 || (fromSeq & 1) != 0 || (toSeq & 1) != 0 { return true } } switch updatedState.embeddedState { case let .sequenceBasedLayer(sequenceState): let fromOperationIndex = sequenceState.outgoingOperationIndexFromCanonicalOperationIndex(fromSeq / 2) let toOperationIndex = sequenceState.outgoingOperationIndexFromCanonicalOperationIndex(toSeq / 2) if fromOperationIndex <= toOperationIndex { for index in fromOperationIndex ... toOperationIndex { var notFound = false transaction.operationLogUpdateEntry(peerId: peerId, tag: OperationLogTags.SecretOutgoing, tagLocalIndex: index, { entry in if let _ = entry { return PeerOperationLogEntryUpdate(mergedIndex: .newAutomatic, contents: .none) } else { notFound = true return PeerOperationLogEntryUpdate(mergedIndex: .none, contents: .none) } }) if notFound { couldNotResendRequestedMessages = true return false } } } default: break } } return true }) if !couldNotResendRequestedMessages { transaction.operationLogEnumerateEntries(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted, { entry in if let operation = entry.contents as? SecretChatIncomingDecryptedOperation { do { var message: StoreMessage? var contentParsingError = false var resources: [(MediaResource, Data)] = [] var serviceAction: SecretChatServiceAction? guard let parsedLayer = SecretChatLayer(rawValue: operation.layer) else { throw MessageParsingError.unsupportedLayer } switch parsedLayer { case .layer8: if let parsedObject = SecretApi8.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi8.DecryptedMessage { message = StoreMessage(peerId: peerId, authorId: updatedPeer.regularPeerId, tagLocalIndex: entry.tagLocalIndex, timestamp: operation.timestamp, apiMessage: apiMessage, file: operation.file) serviceAction = SecretChatServiceAction(apiMessage) } else { throw MessageParsingError.contentParsingError } case .layer46: if let parsedObject = SecretApi46.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi46.DecryptedMessage { if let (parsedMessage, parsedResources) = parseMessage(peerId: peerId, authorId: updatedPeer.regularPeerId, tagLocalIndex: entry.tagLocalIndex, timestamp: operation.timestamp, apiMessage: apiMessage, file: operation.file, messageIdForGloballyUniqueMessageId: { id in return transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) }) { message = parsedMessage resources = parsedResources } serviceAction = SecretChatServiceAction(apiMessage) } else { throw MessageParsingError.contentParsingError } case .layer73: if let parsedObject = SecretApi73.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi73.DecryptedMessage { if let (parsedMessage, parsedResources) = parseMessage(peerId: peerId, authorId: updatedPeer.regularPeerId, tagLocalIndex: entry.tagLocalIndex, timestamp: operation.timestamp, apiMessage: apiMessage, file: operation.file, messageIdForGloballyUniqueMessageId: { id in return transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) }) { message = parsedMessage resources = parsedResources } serviceAction = SecretChatServiceAction(apiMessage) } else { contentParsingError = true } case .layer101: if let parsedObject = SecretApi101.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi101.DecryptedMessage { if let (parsedMessage, parsedResources) = parseMessage(peerId: peerId, authorId: updatedPeer.regularPeerId, tagLocalIndex: entry.tagLocalIndex, timestamp: operation.timestamp, apiMessage: apiMessage, file: operation.file, messageIdForGloballyUniqueMessageId: { id in return transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) }) { message = parsedMessage resources = parsedResources } serviceAction = SecretChatServiceAction(apiMessage) } else { contentParsingError = true } case .layer144: if let parsedObject = SecretApi144.parse(Buffer(bufferNoCopy: operation.contents)), let apiMessage = parsedObject as? SecretApi144.DecryptedMessage { if let (parsedMessage, parsedResources) = parseMessage(peerId: peerId, authorId: updatedPeer.regularPeerId, tagLocalIndex: entry.tagLocalIndex, timestamp: operation.timestamp, apiMessage: apiMessage, file: operation.file, messageIdForGloballyUniqueMessageId: { id in return transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) }) { message = parsedMessage resources = parsedResources } serviceAction = SecretChatServiceAction(apiMessage) } else { contentParsingError = true } } switch updatedState.embeddedState { case .terminated: throw MessageParsingError.invalidChatState case .handshake: throw MessageParsingError.invalidChatState case .basicLayer: if parsedLayer != .layer8 { throw MessageParsingError.contentParsingError } case let .sequenceBasedLayer(sequenceState): if let sequenceInfo = operation.sequenceInfo { let canonicalIncomingIndex = sequenceState.canonicalIncomingOperationIndex(entry.tagLocalIndex) assert(canonicalIncomingIndex == sequenceInfo.operationIndex) if let topProcessedCanonicalIncomingOperationIndex = sequenceState.topProcessedCanonicalIncomingOperationIndex { if canonicalIncomingIndex != topProcessedCanonicalIncomingOperationIndex + 1 { if canonicalIncomingIndex <= topProcessedCanonicalIncomingOperationIndex { throw MessageParsingError.alreadyProcessedMessageInSequenceBasedLayer } else { if let layer = SecretChatSequenceBasedLayer(rawValue: parsedLayer.rawValue) { let role = updatedState.role let fromSeqNo: Int32 = (topProcessedCanonicalIncomingOperationIndex + 1) * 2 + (role == .creator ? 0 : 1) let toSeqNo: Int32 = (canonicalIncomingIndex - 1) * 2 + (role == .creator ? 0 : 1) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) } else { assertionFailure() } throw MessageParsingError.holesInSequenceBasedLayer } } } else { if canonicalIncomingIndex != 0 && canonicalIncomingIndex != 1 { if let layer = SecretChatSequenceBasedLayer(rawValue: parsedLayer.rawValue) { let role = updatedState.role let fromSeqNo: Int32 = Int32(0 * 2) + (role == .creator ? Int32(0) : Int32(1)) let toSeqNo: Int32 = (canonicalIncomingIndex - 1) * 2 + (role == .creator ? 0 : 1) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: SecretChatOutgoingOperationContents.resendOperations(layer: layer, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), fromSeqNo: fromSeqNo, toSeqNo: toSeqNo), state: updatedState) } else { assertionFailure() } throw MessageParsingError.holesInSequenceBasedLayer } } updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedTopProcessedCanonicalIncomingOperationIndex(canonicalIncomingIndex))) } else { throw MessageParsingError.contentParsingError } } if let serviceAction = serviceAction { switch serviceAction { case let .reportLayerSupport(layerSupport): switch updatedState.embeddedState { case .terminated: throw MessageParsingError.invalidChatState case .handshake: throw MessageParsingError.invalidChatState case .basicLayer: if layerSupport >= 144 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer144, locallyRequestedLayer: 144, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer144, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 144), state: updatedState) } else if layerSupport >= 101 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer101, locallyRequestedLayer: 101, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer101, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 101), state: updatedState) } else if layerSupport >= 73 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: 73, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer73, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 144), state: updatedState) } else if layerSupport >= 46 { let sequenceBasedLayerState = SecretChatSequenceBasedLayerState(layerNegotiationState: SecretChatLayerNegotiationState(activeLayer: .layer73, locallyRequestedLayer: 46, remotelyRequestedLayer: layerSupport), rekeyState: nil, baseIncomingOperationIndex: entry.tagLocalIndex, baseOutgoingOperationIndex: transaction.operationLogGetNextEntryLocalIndex(peerId: peerId, tag: OperationLogTags.SecretOutgoing), topProcessedCanonicalIncomingOperationIndex: nil) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceBasedLayerState)) updatedState = addSecretChatOutgoingOperation(transaction: transaction, peerId: peerId, operation: .reportLayerSupport(layer: .layer73, actionGloballyUniqueId: Int64.random(in: Int64.min ... Int64.max), layerSupport: 144), state: updatedState) } else { throw MessageParsingError.contentParsingError } case let .sequenceBasedLayer(sequenceState): if sequenceState.layerNegotiationState.remotelyRequestedLayer != layerSupport { let updatedNegotiationState = sequenceState.layerNegotiationState.withUpdatedRemotelyRequestedLayer(layerSupport) updatedState = updatedState.withUpdatedEmbeddedState(.sequenceBasedLayer(sequenceState.withUpdatedLayerNegotiationState(updatedNegotiationState))) updatedState = secretChatCheckLayerNegotiationIfNeeded(transaction: transaction, peerId: peerId, state: updatedState) } } case let .setMessageAutoremoveTimeout(timeout): updatedPeer = updatedPeer.withUpdatedMessageAutoremoveTimeout(timeout == 0 ? nil : timeout) updatedState = updatedState.withUpdatedMessageAutoremoveTimeout(timeout == 0 ? nil : timeout) case let .rekeyAction(action): updatedState = secretChatAdvanceRekeySessionIfNeeded(encryptionProvider: encryptionProvider, transaction: transaction, peerId: peerId, state: updatedState, action: action) case let .deleteMessages(globallyUniqueIds): var messageIds: [MessageId] = [] for id in globallyUniqueIds { if let messageId = transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) { messageIds.append(messageId) } } if !messageIds.isEmpty { var filteredMessageIds = messageIds outer: for i in (0 ..< filteredMessageIds.count).reversed() { if let message = transaction.getMessage(filteredMessageIds[i]) { for media in message.media { if let media = media as? TelegramMediaAction { if case .historyScreenshot = media.action { filteredMessageIds.remove(at: i) continue outer } } } } } _internal_deleteMessages(transaction: transaction, mediaBox: mediaBox, ids: filteredMessageIds) } case .clearHistory: _internal_clearHistory(transaction: transaction, mediaBox: mediaBox, peerId: peerId, threadId: nil, namespaces: .all) case let .markMessagesContentAsConsumed(globallyUniqueIds): var messageIds: [MessageId] = [] for id in globallyUniqueIds { if let messageId = transaction.messageIdForGloballyUniqueMessageId(peerId: peerId, id: id) { messageIds.append(messageId) } } for messageId in messageIds { markMessageContentAsConsumedRemotely(transaction: transaction, messageId: messageId, consumeDate: nil) } default: break } } removeTagLocalIndices.append(entry.tagLocalIndex) if let sequenceInfo = operation.sequenceInfo { if maxAcknowledgedCanonicalOperationIndex == nil || maxAcknowledgedCanonicalOperationIndex! < sequenceInfo.topReceivedOperationIndex { maxAcknowledgedCanonicalOperationIndex = sequenceInfo.topReceivedOperationIndex } } if let message = message { for (resource, data) in resources { mediaBox.storeResourceData(resource.id, data: data) } let _ = transaction.addMessages([message], location: .Random) addedMessages.append(message) } else if contentParsingError { Logger.shared.log("SecretChat", "Couldn't parse secret message content") } } catch let error { if let error = error as? MessageParsingError { switch error { case .contentParsingError: Logger.shared.log("SecretChat", "Couldn't parse secret message payload") removeTagLocalIndices.append(entry.tagLocalIndex) return true case .unsupportedLayer: return false case .invalidChatState: removeTagLocalIndices.append(entry.tagLocalIndex) return false case .alreadyProcessedMessageInSequenceBasedLayer: removeTagLocalIndices.append(entry.tagLocalIndex) return true case .holesInSequenceBasedLayer: Logger.shared.log("SecretChat", "Found holes in incoming operation sequence") return false case .secretChatCorruption: Logger.shared.log("SecretChat", "Secret chat corrupted") return false } } else { assertionFailure() } } } else { assertionFailure() } return true }) } for index in removeTagLocalIndices { let removed = transaction.operationLogRemoveEntry(peerId: peerId, tag: OperationLogTags.SecretIncomingDecrypted, tagLocalIndex: index) assert(removed) } if let maxAcknowledgedCanonicalOperationIndex = maxAcknowledgedCanonicalOperationIndex { switch updatedState.embeddedState { case let .sequenceBasedLayer(sequenceState): let tagLocalIndex = max(-1, sequenceState.outgoingOperationIndexFromCanonicalOperationIndex(maxAcknowledgedCanonicalOperationIndex) - 1) if tagLocalIndex >= 0 { Logger.shared.log("SecretChat", "peer \(peerId) dropping acknowledged operations <= \(tagLocalIndex)") transaction.operationLogRemoveEntries(peerId: peerId, tag: OperationLogTags.SecretOutgoing, withTagLocalIndicesEqualToOrLowerThan: tagLocalIndex) } default: break } } if updatedState != state { transaction.setPeerChatState(peerId, state: updatedState) updatedPeer = updatedPeer.withUpdatedEmbeddedState(updatedState.embeddedState.peerState) } if !peer.isEqual(updatedPeer) { updatePeersCustom(transaction: transaction, peers: [updatedPeer], update: { _, updated in return updated }) } return SecretChatOperationProcessResult(addedMessages: addedMessages) } else { assertionFailure() return SecretChatOperationProcessResult(addedMessages: []) } } extension SecretChatServiceAction { init?(_ apiMessage: SecretApi8.DecryptedMessage) { switch apiMessage { case .decryptedMessage: return nil case let .decryptedMessageService(_, _, action): switch action { case let .decryptedMessageActionDeleteMessages(randomIds): self = .deleteMessages(globallyUniqueIds: randomIds) case .decryptedMessageActionFlushHistory: self = .clearHistory case let .decryptedMessageActionNotifyLayer(layer): self = .reportLayerSupport(layer) case let .decryptedMessageActionReadMessages(randomIds): self = .markMessagesContentAsConsumed(globallyUniqueIds: randomIds) case .decryptedMessageActionScreenshotMessages: return nil case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self = .setMessageAutoremoveTimeout(ttlSeconds) } } } } extension SecretChatServiceAction { init?(_ apiMessage: SecretApi46.DecryptedMessage) { switch apiMessage { case .decryptedMessage: return nil case let .decryptedMessageService(_, action): switch action { case let .decryptedMessageActionDeleteMessages(randomIds): self = .deleteMessages(globallyUniqueIds: randomIds) case .decryptedMessageActionFlushHistory: self = .clearHistory case let .decryptedMessageActionNotifyLayer(layer): self = .reportLayerSupport(layer) case let .decryptedMessageActionReadMessages(randomIds): self = .markMessagesContentAsConsumed(globallyUniqueIds: randomIds) case .decryptedMessageActionScreenshotMessages: return nil case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self = .setMessageAutoremoveTimeout(ttlSeconds) case let .decryptedMessageActionResend(startSeqNo, endSeqNo): self = .resendOperations(fromSeq: startSeqNo, toSeq: endSeqNo) case let .decryptedMessageActionRequestKey(exchangeId, gA): self = .rekeyAction(.pfsRequestKey(rekeySessionId: exchangeId, gA: MemoryBuffer(gA))) case let .decryptedMessageActionAcceptKey(exchangeId, gB, keyFingerprint): self = .rekeyAction(.pfsAcceptKey(rekeySessionId: exchangeId, gB: MemoryBuffer(gB), keyFingerprint: keyFingerprint)) case let .decryptedMessageActionCommitKey(exchangeId, keyFingerprint): self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) case .decryptedMessageActionNoop: return nil } } } } extension SecretChatServiceAction { init?(_ apiMessage: SecretApi73.DecryptedMessage) { switch apiMessage { case .decryptedMessage: return nil case let .decryptedMessageService(_, action): switch action { case let .decryptedMessageActionDeleteMessages(randomIds): self = .deleteMessages(globallyUniqueIds: randomIds) case .decryptedMessageActionFlushHistory: self = .clearHistory case let .decryptedMessageActionNotifyLayer(layer): self = .reportLayerSupport(layer) case let .decryptedMessageActionReadMessages(randomIds): self = .markMessagesContentAsConsumed(globallyUniqueIds: randomIds) case .decryptedMessageActionScreenshotMessages: return nil case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self = .setMessageAutoremoveTimeout(ttlSeconds) case let .decryptedMessageActionResend(startSeqNo, endSeqNo): self = .resendOperations(fromSeq: startSeqNo, toSeq: endSeqNo) case let .decryptedMessageActionRequestKey(exchangeId, gA): self = .rekeyAction(.pfsRequestKey(rekeySessionId: exchangeId, gA: MemoryBuffer(gA))) case let .decryptedMessageActionAcceptKey(exchangeId, gB, keyFingerprint): self = .rekeyAction(.pfsAcceptKey(rekeySessionId: exchangeId, gB: MemoryBuffer(gB), keyFingerprint: keyFingerprint)) case let .decryptedMessageActionCommitKey(exchangeId, keyFingerprint): self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) case .decryptedMessageActionNoop: return nil } } } } extension SecretChatServiceAction { init?(_ apiMessage: SecretApi101.DecryptedMessage) { switch apiMessage { case .decryptedMessage: return nil case let .decryptedMessageService(_, action): switch action { case let .decryptedMessageActionDeleteMessages(randomIds): self = .deleteMessages(globallyUniqueIds: randomIds) case .decryptedMessageActionFlushHistory: self = .clearHistory case let .decryptedMessageActionNotifyLayer(layer): self = .reportLayerSupport(layer) case let .decryptedMessageActionReadMessages(randomIds): self = .markMessagesContentAsConsumed(globallyUniqueIds: randomIds) case .decryptedMessageActionScreenshotMessages: return nil case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self = .setMessageAutoremoveTimeout(ttlSeconds) case let .decryptedMessageActionResend(startSeqNo, endSeqNo): self = .resendOperations(fromSeq: startSeqNo, toSeq: endSeqNo) case let .decryptedMessageActionRequestKey(exchangeId, gA): self = .rekeyAction(.pfsRequestKey(rekeySessionId: exchangeId, gA: MemoryBuffer(gA))) case let .decryptedMessageActionAcceptKey(exchangeId, gB, keyFingerprint): self = .rekeyAction(.pfsAcceptKey(rekeySessionId: exchangeId, gB: MemoryBuffer(gB), keyFingerprint: keyFingerprint)) case let .decryptedMessageActionCommitKey(exchangeId, keyFingerprint): self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) case .decryptedMessageActionNoop: return nil } } } } extension SecretChatServiceAction { init?(_ apiMessage: SecretApi144.DecryptedMessage) { switch apiMessage { case .decryptedMessage: return nil case let .decryptedMessageService(_, action): switch action { case let .decryptedMessageActionDeleteMessages(randomIds): self = .deleteMessages(globallyUniqueIds: randomIds) case .decryptedMessageActionFlushHistory: self = .clearHistory case let .decryptedMessageActionNotifyLayer(layer): self = .reportLayerSupport(layer) case let .decryptedMessageActionReadMessages(randomIds): self = .markMessagesContentAsConsumed(globallyUniqueIds: randomIds) case .decryptedMessageActionScreenshotMessages: return nil case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self = .setMessageAutoremoveTimeout(ttlSeconds) case let .decryptedMessageActionResend(startSeqNo, endSeqNo): self = .resendOperations(fromSeq: startSeqNo, toSeq: endSeqNo) case let .decryptedMessageActionRequestKey(exchangeId, gA): self = .rekeyAction(.pfsRequestKey(rekeySessionId: exchangeId, gA: MemoryBuffer(gA))) case let .decryptedMessageActionAcceptKey(exchangeId, gB, keyFingerprint): self = .rekeyAction(.pfsAcceptKey(rekeySessionId: exchangeId, gB: MemoryBuffer(gB), keyFingerprint: keyFingerprint)) case let .decryptedMessageActionCommitKey(exchangeId, keyFingerprint): self = .rekeyAction(.pfsCommitKey(rekeySessionId: exchangeId, keyFingerprint: keyFingerprint)) case let .decryptedMessageActionAbortKey(exchangeId): self = .rekeyAction(.pfsAbortSession(rekeySessionId: exchangeId)) case .decryptedMessageActionNoop: return nil } } } } extension StoreMessage { convenience init?(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32, timestamp: Int32, apiMessage: SecretApi8.DecryptedMessage, file: SecretChatFileReference?) { switch apiMessage { case let .decryptedMessage(randomId, _, message, _): self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: message, attributes: [], media: []) case let .decryptedMessageService(randomId, _, action): switch action { case .decryptedMessageActionDeleteMessages: return nil case .decryptedMessageActionFlushHistory: return nil case .decryptedMessageActionNotifyLayer: return nil case .decryptedMessageActionReadMessages: return nil case .decryptedMessageActionScreenshotMessages: self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]) case let .decryptedMessageActionSetMessageTTL(ttlSeconds): self.init(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]) } } } } extension TelegramMediaFileAttribute { init?(_ apiAttribute: SecretApi46.DocumentAttribute) { switch apiAttribute { case .documentAttributeAnimated: self = .Animated case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() self = .Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer) case let .documentAttributeFilename(fileName): self = .FileName(fileName: fileName) case let .documentAttributeImageSize(w, h): self = .ImageSize(size: PixelDimensions(width: w, height: h)) case let .documentAttributeSticker(alt, stickerset): let packReference: StickerPackReference? switch stickerset { case .inputStickerSetEmpty: packReference = nil case let .inputStickerSetShortName(shortName): packReference = .name(shortName) } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(duration, w, h): self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil) } } } extension TelegramMediaFileAttribute { init?(_ apiAttribute: SecretApi73.DocumentAttribute) { switch apiAttribute { case .documentAttributeAnimated: self = .Animated case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() self = .Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer) case let .documentAttributeFilename(fileName): self = .FileName(fileName: fileName) case let .documentAttributeImageSize(w, h): self = .ImageSize(size: PixelDimensions(width: w, height: h)) case let .documentAttributeSticker(alt, stickerset): let packReference: StickerPackReference? switch stickerset { case .inputStickerSetEmpty: packReference = nil case let .inputStickerSetShortName(shortName): packReference = .name(shortName) } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(flags, duration, w, h): var videoFlags: TelegramMediaVideoFlags = [] if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } extension TelegramMediaFileAttribute { init?(_ apiAttribute: SecretApi101.DocumentAttribute) { switch apiAttribute { case .documentAttributeAnimated: self = .Animated case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() self = .Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer) case let .documentAttributeFilename(fileName): self = .FileName(fileName: fileName) case let .documentAttributeImageSize(w, h): self = .ImageSize(size: PixelDimensions(width: w, height: h)) case let .documentAttributeSticker(alt, stickerset): let packReference: StickerPackReference? switch stickerset { case .inputStickerSetEmpty: packReference = nil case let .inputStickerSetShortName(shortName): packReference = .name(shortName) } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(flags, duration, w, h): var videoFlags: TelegramMediaVideoFlags = [] if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } extension TelegramMediaFileAttribute { init?(_ apiAttribute: SecretApi144.DocumentAttribute) { switch apiAttribute { case .documentAttributeAnimated: self = .Animated case let .documentAttributeAudio(flags, duration, title, performer, waveform): let isVoice = (flags & (1 << 10)) != 0 let waveformBuffer: Data? = waveform?.makeData() self = .Audio(isVoice: isVoice, duration: Int(duration), title: title, performer: performer, waveform: waveformBuffer) case let .documentAttributeFilename(fileName): self = .FileName(fileName: fileName) case let .documentAttributeImageSize(w, h): self = .ImageSize(size: PixelDimensions(width: w, height: h)) case let .documentAttributeSticker(alt, stickerset): let packReference: StickerPackReference? switch stickerset { case .inputStickerSetEmpty: packReference = nil case let .inputStickerSetShortName(shortName): packReference = .name(shortName) } self = .Sticker(displayText: alt, packReference: packReference, maskData: nil) case let .documentAttributeVideo(flags, duration, w, h): var videoFlags: TelegramMediaVideoFlags = [] if (flags & (1 << 0)) != 0 { videoFlags.insert(.instantRoundVideo) } self = .Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: videoFlags, preloadSize: nil, coverTime: nil, videoCodec: nil) } } } private func parseEntities(_ entities: [SecretApi46.MessageEntity]?) -> TextEntitiesMessageAttribute { var result: [MessageTextEntity] = [] if let entities = entities { for entity in entities { switch entity { case let .messageEntityMention(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Mention)) case let .messageEntityHashtag(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Hashtag)) case let .messageEntityBotCommand(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BotCommand)) case let .messageEntityUrl(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Url)) case let .messageEntityEmail(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Email)) case let .messageEntityBold(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Bold)) case let .messageEntityItalic(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Italic)) case let .messageEntityCode(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Code)) case let .messageEntityPre(offset, length, language): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Pre(language: language))) case let .messageEntityTextUrl(offset, length, url): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextUrl(url: url))) case .messageEntityUnknown: break } } } return TextEntitiesMessageAttribute(entities: result) } private func maximumMediaAutoremoveTimeout(_ media: [Media]) -> Int32 { var maxDuration: Int32 = 0 for media in media { if let file = media as? TelegramMediaFile { if let duration = file.duration { maxDuration = max(maxDuration, Int32(duration)) } } } return maxDuration } private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32, timestamp: Int32, apiMessage: SecretApi46.DecryptedMessage, file: SecretChatFileReference?, messageIdForGloballyUniqueMessageId: (Int64) -> MessageId?) -> (StoreMessage, [(MediaResource, Data)])? { switch apiMessage { case let .decryptedMessage(flags, randomId, ttl, message, media, entities, viaBotName, replyToRandomId): var text = message var parsedMedia: [Media] = [] var attributes: [MessageAttribute] = [] var resources: [(MediaResource, Data)] = [] attributes.append(parseEntities(entities)) if let viaBotName = viaBotName, !viaBotName.isEmpty { attributes.append(InlineBotMessageAttribute(peerId: nil, title: viaBotName)) } if (flags & 1 << 5) != 0 { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } if let media = media { switch media { case let .decryptedMessageMediaPhoto(thumb, thumbW, thumbH, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaDocument(thumb, thumbW, thumbH, mimeType, size, key, iv, attributes, caption): if !caption.isEmpty { text = caption } if let file = file { var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in attributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaVideo(thumb, thumbW, thumbH, duration, mimeType, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in attributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] switch thumb { case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } case let .photoCachedSize(_, location, w, h, bytes): if bytes.size > 0 { switch location { case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } } default: break } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } } if ttl > 0 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: ttl, countdownBeginTime: nil)) } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entitiesAttribute = attribute break } } let (tags, globalTags) = tagsForStoreMessage(incoming: true, attributes: attributes, media: parsedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: parsedMedia), resources) case let .decryptedMessageService(randomId, action): switch action { case .decryptedMessageActionDeleteMessages: return nil case .decryptedMessageActionFlushHistory: return nil case .decryptedMessageActionNotifyLayer: return nil case .decryptedMessageActionReadMessages: return nil case .decryptedMessageActionScreenshotMessages: return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), []) case let .decryptedMessageActionSetMessageTTL(ttlSeconds): return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), []) case .decryptedMessageActionResend: return nil case .decryptedMessageActionRequestKey: return nil case .decryptedMessageActionAcceptKey: return nil case .decryptedMessageActionCommitKey: return nil case .decryptedMessageActionAbortKey: return nil case .decryptedMessageActionNoop: return nil } } } private func parseEntities(_ entities: [SecretApi73.MessageEntity]) -> TextEntitiesMessageAttribute { var result: [MessageTextEntity] = [] for entity in entities { switch entity { case let .messageEntityMention(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Mention)) case let .messageEntityHashtag(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Hashtag)) case let .messageEntityBotCommand(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BotCommand)) case let .messageEntityUrl(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Url)) case let .messageEntityEmail(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Email)) case let .messageEntityBold(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Bold)) case let .messageEntityItalic(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Italic)) case let .messageEntityCode(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Code)) case let .messageEntityPre(offset, length, language): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Pre(language: language))) case let .messageEntityTextUrl(offset, length, url): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextUrl(url: url))) case .messageEntityUnknown: break } } return TextEntitiesMessageAttribute(entities: result) } private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32, timestamp: Int32, apiMessage: SecretApi73.DecryptedMessage, file: SecretChatFileReference?, messageIdForGloballyUniqueMessageId: (Int64) -> MessageId?) -> (StoreMessage, [(MediaResource, Data)])? { switch apiMessage { case let .decryptedMessage(flags, randomId, ttl, message, media, entities, viaBotName, replyToRandomId, groupedId): var text = message var parsedMedia: [Media] = [] var attributes: [MessageAttribute] = [] var resources: [(MediaResource, Data)] = [] if let entitiesAttribute = entities.flatMap(parseEntities) { attributes.append(entitiesAttribute) } if let viaBotName = viaBotName, !viaBotName.isEmpty { attributes.append(InlineBotMessageAttribute(peerId: nil, title: viaBotName)) } if (flags & 1 << 5) != 0 { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } if let media = media { switch media { case let .decryptedMessageMediaPhoto(thumb, thumbW, thumbH, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } case let .decryptedMessageMediaDocument(thumb, thumbW, thumbH, mimeType, size, key, iv, decryptedAttributes, caption): if !caption.isEmpty { text = caption } if let file = file { var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in decryptedAttributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } break loop case let .Audio(isVoice, _, _, _, _): if isVoice { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } default: break } } } case let .decryptedMessageMediaVideo(thumb, thumbW, thumbH, duration, mimeType, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in attributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] switch thumb { case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } case let .photoCachedSize(_, location, w, h, bytes): if bytes.size > 0 { switch location { case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } } default: break } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } } if ttl > 0 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: ttl, countdownBeginTime: nil)) } var groupingKey: Int64? if let groupedId = groupedId { inner: for media in parsedMedia { if let _ = media as? TelegramMediaImage { groupingKey = groupedId break inner } else if let _ = media as? TelegramMediaFile { groupingKey = groupedId break inner } } } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entitiesAttribute = attribute break } } let (tags, globalTags) = tagsForStoreMessage(incoming: true, attributes: attributes, media: parsedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: groupingKey, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: parsedMedia), resources) case let .decryptedMessageService(randomId, action): switch action { case .decryptedMessageActionDeleteMessages: return nil case .decryptedMessageActionFlushHistory: return nil case .decryptedMessageActionNotifyLayer: return nil case .decryptedMessageActionReadMessages: return nil case .decryptedMessageActionScreenshotMessages: return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), []) case let .decryptedMessageActionSetMessageTTL(ttlSeconds): return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), []) case .decryptedMessageActionResend: return nil case .decryptedMessageActionRequestKey: return nil case .decryptedMessageActionAcceptKey: return nil case .decryptedMessageActionCommitKey: return nil case .decryptedMessageActionAbortKey: return nil case .decryptedMessageActionNoop: return nil } } } private func parseEntities(_ entities: [SecretApi101.MessageEntity]) -> TextEntitiesMessageAttribute { var result: [MessageTextEntity] = [] for entity in entities { switch entity { case let .messageEntityMention(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Mention)) case let .messageEntityHashtag(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Hashtag)) case let .messageEntityBotCommand(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BotCommand)) case let .messageEntityUrl(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Url)) case let .messageEntityEmail(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Email)) case let .messageEntityBold(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Bold)) case let .messageEntityItalic(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Italic)) case let .messageEntityCode(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Code)) case let .messageEntityPre(offset, length, language): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Pre(language: language))) case let .messageEntityTextUrl(offset, length, url): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextUrl(url: url))) case let .messageEntityStrike(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Strikethrough)) case let .messageEntityUnderline(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Underline)) case let .messageEntityBlockquote(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BlockQuote(isCollapsed: false))) case .messageEntityUnknown: break } } return TextEntitiesMessageAttribute(entities: result) } private func parseEntities(_ entities: [SecretApi144.MessageEntity]) -> TextEntitiesMessageAttribute { var result: [MessageTextEntity] = [] for entity in entities { switch entity { case let .messageEntityMention(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Mention)) case let .messageEntityHashtag(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Hashtag)) case let .messageEntityBotCommand(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BotCommand)) case let .messageEntityUrl(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Url)) case let .messageEntityEmail(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Email)) case let .messageEntityBold(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Bold)) case let .messageEntityItalic(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Italic)) case let .messageEntityCode(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Code)) case let .messageEntityPre(offset, length, language): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Pre(language: language))) case let .messageEntityTextUrl(offset, length, url): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .TextUrl(url: url))) case let .messageEntityStrike(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Strikethrough)) case let .messageEntityUnderline(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Underline)) case let .messageEntityBlockquote(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .BlockQuote(isCollapsed: false))) case let .messageEntitySpoiler(offset, length): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .Spoiler)) case let .messageEntityCustomEmoji(offset, length, documentId): result.append(MessageTextEntity(range: Int(offset) ..< Int(offset + length), type: .CustomEmoji(stickerPack: nil, fileId: documentId))) case .messageEntityUnknown: break } } return TextEntitiesMessageAttribute(entities: result) } private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32, timestamp: Int32, apiMessage: SecretApi101.DecryptedMessage, file: SecretChatFileReference?, messageIdForGloballyUniqueMessageId: (Int64) -> MessageId?) -> (StoreMessage, [(MediaResource, Data)])? { switch apiMessage { case let .decryptedMessage(flags, randomId, ttl, message, media, entities, viaBotName, replyToRandomId, groupedId): var text = message var parsedMedia: [Media] = [] var attributes: [MessageAttribute] = [] var resources: [(MediaResource, Data)] = [] if let entitiesAttribute = entities.flatMap(parseEntities) { attributes.append(entitiesAttribute) } if let viaBotName = viaBotName, !viaBotName.isEmpty { attributes.append(InlineBotMessageAttribute(peerId: nil, title: viaBotName)) } if (flags & 1 << 5) != 0 { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } if let media = media { switch media { case let .decryptedMessageMediaPhoto(thumb, thumbW, thumbH, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } case let .decryptedMessageMediaDocument(thumb, thumbW, thumbH, mimeType, size, key, iv, decryptedAttributes, caption): if !caption.isEmpty { text = caption } if let file = file { var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in decryptedAttributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } break loop case let .Audio(isVoice, _, _, _, _): if isVoice { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } default: break } } } case let .decryptedMessageMediaVideo(thumb, thumbW, thumbH, duration, mimeType, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in attributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] switch thumb { case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } case let .photoCachedSize(_, location, w, h, bytes): if bytes.size > 0 { switch location { case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } } default: break } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } } if ttl > 0 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: ttl, countdownBeginTime: nil)) } var groupingKey: Int64? if let groupedId = groupedId { inner: for media in parsedMedia { if let _ = media as? TelegramMediaImage { groupingKey = groupedId break inner } else if let _ = media as? TelegramMediaFile { groupingKey = groupedId break inner } } } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entitiesAttribute = attribute break } } let (tags, globalTags) = tagsForStoreMessage(incoming: true, attributes: attributes, media: parsedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: groupingKey, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: parsedMedia), resources) case let .decryptedMessageService(randomId, action): switch action { case .decryptedMessageActionDeleteMessages: return nil case .decryptedMessageActionFlushHistory: return nil case .decryptedMessageActionNotifyLayer: return nil case .decryptedMessageActionReadMessages: return nil case .decryptedMessageActionScreenshotMessages: return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), []) case let .decryptedMessageActionSetMessageTTL(ttlSeconds): return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), []) case .decryptedMessageActionResend: return nil case .decryptedMessageActionRequestKey: return nil case .decryptedMessageActionAcceptKey: return nil case .decryptedMessageActionCommitKey: return nil case .decryptedMessageActionAbortKey: return nil case .decryptedMessageActionNoop: return nil } } } private func parseMessage(peerId: PeerId, authorId: PeerId, tagLocalIndex: Int32, timestamp: Int32, apiMessage: SecretApi144.DecryptedMessage, file: SecretChatFileReference?, messageIdForGloballyUniqueMessageId: (Int64) -> MessageId?) -> (StoreMessage, [(MediaResource, Data)])? { switch apiMessage { case let .decryptedMessage(flags, randomId, ttl, message, media, entities, viaBotName, replyToRandomId, groupedId): var text = message var parsedMedia: [Media] = [] var attributes: [MessageAttribute] = [] var resources: [(MediaResource, Data)] = [] if let entitiesAttribute = entities.flatMap(parseEntities) { attributes.append(entitiesAttribute) } if let viaBotName = viaBotName, !viaBotName.isEmpty { attributes.append(InlineBotMessageAttribute(peerId: nil, title: viaBotName)) } if (flags & 1 << 5) != 0 { attributes.append(NotificationInfoMessageAttribute(flags: .muted)) } if let media = media { switch media { case let .decryptedMessageMediaPhoto(thumb, thumbW, thumbH, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { var representations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } representations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) let image = TelegramMediaImage(imageId: MediaId(namespace: Namespaces.Media.CloudSecretImage, id: file.id), representations: representations, immediateThumbnailData: nil, reference: nil, partialReference: nil, flags: []) parsedMedia.append(image) } case let .decryptedMessageMediaAudio(duration, mimeType, size, key, iv): if let file = file { let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: [TelegramMediaFileAttribute.Audio(isVoice: true, duration: Int(duration), title: nil, performer: nil, waveform: nil)], alternativeRepresentations: []) parsedMedia.append(fileMedia) attributes.append(ConsumableContentMessageAttribute(consumed: false)) } case let .decryptedMessageMediaDocument(thumb, thumbW, thumbH, mimeType, size, key, iv, decryptedAttributes, caption): if !caption.isEmpty { text = caption } if let file = file { var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in decryptedAttributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: size), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) loop: for attr in parsedAttributes { switch attr { case let .Video(_, _, flags, _, _, _): if flags.contains(.instantRoundVideo) { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } break loop case let .Audio(isVoice, _, _, _, _): if isVoice { attributes.append(ConsumableContentMessageAttribute(consumed: false)) } default: break } } } case let .decryptedMessageMediaVideo(thumb, thumbW, thumbH, duration, mimeType, w, h, size, key, iv, caption): if !caption.isEmpty { text = caption } if let file = file { let parsedAttributes: [TelegramMediaFileAttribute] = [.Video(duration: Double(duration), size: PixelDimensions(width: w, height: h), flags: [], preloadSize: nil, coverTime: nil, videoCodec: nil), .FileName(fileName: "video.mov")] var previewRepresentations: [TelegramMediaImageRepresentation] = [] if thumb.size != 0 { let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max)) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: thumbW, height: thumbH), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) resources.append((resource, thumb.makeData())) } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudSecretFile, id: file.id), partialReference: nil, resource: file.resource(key: SecretFileEncryptionKey(aesKey: key.makeData(), aesIv: iv.makeData()), decryptedSize: Int64(size)), previewRepresentations: previewRepresentations, videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) } case let .decryptedMessageMediaExternalDocument(id, accessHash, _, mimeType, size, thumb, dcId, attributes): var parsedAttributes: [TelegramMediaFileAttribute] = [] for attribute in attributes { if let parsedAttribute = TelegramMediaFileAttribute(attribute) { parsedAttributes.append(parsedAttribute) } } var previewRepresentations: [TelegramMediaImageRepresentation] = [] switch thumb { case let .photoSize(_, location, w, h, size): switch location { case let .fileLocation(dcId, volumeId, localId, secret): previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: size == 0 ? nil : Int64(size), fileReference: nil), progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } case let .photoCachedSize(_, location, w, h, bytes): if bytes.size > 0 { switch location { case let .fileLocation(dcId, volumeId, localId, secret): let resource = CloudFileMediaResource(datacenterId: Int(dcId), volumeId: volumeId, localId: localId, secret: secret, size: Int64(bytes.size), fileReference: nil) resources.append((resource, bytes.makeData())) previewRepresentations.append(TelegramMediaImageRepresentation(dimensions: PixelDimensions(width: w, height: h), resource: resource, progressiveSizes: [], immediateThumbnailData: nil, hasVideo: false, isPersonal: false)) case .fileLocationUnavailable: break } } default: break } let fileMedia = TelegramMediaFile(fileId: MediaId(namespace: Namespaces.Media.CloudFile, id: id), partialReference: nil, resource: CloudDocumentMediaResource(datacenterId: Int(dcId), fileId: id, accessHash: accessHash, size: Int64(size), fileReference: nil, fileName: nil), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: mimeType, size: Int64(size), attributes: parsedAttributes, alternativeRepresentations: []) parsedMedia.append(fileMedia) case let .decryptedMessageMediaWebPage(url): parsedMedia.append(TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: Int64.random(in: Int64.min ... Int64.max)), content: .Pending(0, url))) case let .decryptedMessageMediaGeoPoint(lat, long): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: nil, liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case let .decryptedMessageMediaContact(phoneNumber, firstName, lastName, userId): parsedMedia.append(TelegramMediaContact(firstName: firstName, lastName: lastName, phoneNumber: phoneNumber, peerId: userId == 0 ? nil : PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(Int64(userId))), vCardData: nil)) case let .decryptedMessageMediaVenue(lat, long, title, address, provider, venueId): parsedMedia.append(TelegramMediaMap(latitude: lat, longitude: long, heading: nil, accuracyRadius: nil, venue: MapVenue(title: title, address: address, provider: provider, id: venueId, type: nil), liveBroadcastingTimeout: nil, liveProximityNotificationRadius: nil)) case .decryptedMessageMediaEmpty: break } } if ttl > 0 { attributes.append(AutoremoveTimeoutMessageAttribute(timeout: ttl, countdownBeginTime: nil)) } var groupingKey: Int64? if let groupedId = groupedId { inner: for media in parsedMedia { if let _ = media as? TelegramMediaImage { groupingKey = groupedId break inner } else if let _ = media as? TelegramMediaFile { groupingKey = groupedId break inner } } } if let replyToRandomId = replyToRandomId, let replyMessageId = messageIdForGloballyUniqueMessageId(replyToRandomId) { attributes.append(ReplyMessageAttribute(messageId: replyMessageId, threadMessageId: nil, quote: nil, isQuote: false)) } var entitiesAttribute: TextEntitiesMessageAttribute? for attribute in attributes { if let attribute = attribute as? TextEntitiesMessageAttribute { entitiesAttribute = attribute break } } let (tags, globalTags) = tagsForStoreMessage(incoming: true, attributes: attributes, media: parsedMedia, textEntities: entitiesAttribute?.entities, isPinned: false) return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: groupingKey, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: tags, globalTags: globalTags, localTags: [], forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: parsedMedia), resources) case let .decryptedMessageService(randomId, action): switch action { case .decryptedMessageActionDeleteMessages: return nil case .decryptedMessageActionFlushHistory: return nil case .decryptedMessageActionNotifyLayer: return nil case .decryptedMessageActionReadMessages: return nil case .decryptedMessageActionScreenshotMessages: return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .historyScreenshot)]), []) case let .decryptedMessageActionSetMessageTTL(ttlSeconds): return (StoreMessage(id: MessageId(peerId: peerId, namespace: Namespaces.Message.SecretIncoming, id: tagLocalIndex), globallyUniqueId: randomId, groupingKey: nil, threadId: nil, timestamp: timestamp, flags: [.Incoming], tags: [], globalTags: [], localTags: [], forwardInfo: nil, authorId: authorId, text: "", attributes: [], media: [TelegramMediaAction(action: .messageAutoremoveTimeoutUpdated(period: ttlSeconds, autoSettingSource: nil))]), []) case .decryptedMessageActionResend: return nil case .decryptedMessageActionRequestKey: return nil case .decryptedMessageActionAcceptKey: return nil case .decryptedMessageActionCommitKey: return nil case .decryptedMessageActionAbortKey: return nil case .decryptedMessageActionNoop: return nil } } }