mirror of
https://github.com/Swiftgram/Telegram-iOS.git
synced 2025-08-17 19:09:56 +00:00
411 lines
21 KiB
Swift
411 lines
21 KiB
Swift
import Foundation
|
|
#if os(macOS)
|
|
import PostboxMac
|
|
import SwiftSignalKitMac
|
|
#else
|
|
import Postbox
|
|
import SwiftSignalKit
|
|
#endif
|
|
|
|
public enum EnqueueMessageGrouping {
|
|
case none
|
|
case auto
|
|
}
|
|
|
|
public enum EnqueueMessage {
|
|
case message(text: String, attributes: [MessageAttribute], media: Media?, replyToMessageId: MessageId?, localGroupingKey: Int64?)
|
|
case forward(source: MessageId, grouping: EnqueueMessageGrouping)
|
|
|
|
public func withUpdatedReplyToMessageId(_ replyToMessageId: MessageId?) -> EnqueueMessage {
|
|
switch self {
|
|
case let .message(text, attributes, media, _, localGroupingKey):
|
|
return .message(text: text, attributes: attributes, media: media, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey)
|
|
case .forward:
|
|
return self
|
|
}
|
|
}
|
|
|
|
public func withUpdatedAttributes(_ f: ([MessageAttribute]) -> [MessageAttribute]) -> EnqueueMessage {
|
|
switch self {
|
|
case let .message(text, attributes, media, replyToMessageId, localGroupingKey):
|
|
return .message(text: text, attributes: f(attributes), media: media, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey)
|
|
case .forward:
|
|
return self
|
|
}
|
|
}
|
|
}
|
|
|
|
private func filterMessageAttributesForOutgoingMessage(_ attributes: [MessageAttribute]) -> [MessageAttribute] {
|
|
return attributes.filter { attribute in
|
|
switch attribute {
|
|
case _ as TextEntitiesMessageAttribute:
|
|
return true
|
|
case _ as InlineBotMessageAttribute:
|
|
return true
|
|
case _ as OutgoingMessageInfoAttribute:
|
|
return true
|
|
case _ as OutgoingContentInfoMessageAttribute:
|
|
return true
|
|
case _ as ReplyMarkupMessageAttribute:
|
|
return true
|
|
case _ as OutgoingChatContextResultMessageAttribute:
|
|
return true
|
|
case _ as AutoremoveTimeoutMessageAttribute:
|
|
return true
|
|
case _ as NotificationInfoMessageAttribute:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
private func filterMessageAttributesForForwardedMessage(_ attributes: [MessageAttribute]) -> [MessageAttribute] {
|
|
return attributes.filter { attribute in
|
|
switch attribute {
|
|
case _ as TextEntitiesMessageAttribute:
|
|
return true
|
|
case _ as InlineBotMessageAttribute:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
}
|
|
|
|
func opportunisticallyTransformMessageWithMedia(network: Network, postbox: Postbox, transformOutgoingMessageMedia: TransformOutgoingMessageMedia, media: Media, userInteractive: Bool) -> Signal<Media?, NoError> {
|
|
return transformOutgoingMessageMedia(postbox, network, media, userInteractive)
|
|
|> timeout(2.0, queue: Queue.concurrentDefaultQueue(), alternate: .single(nil))
|
|
}
|
|
|
|
private func opportunisticallyTransformOutgoingMedia(network: Network, postbox: Postbox, transformOutgoingMessageMedia: TransformOutgoingMessageMedia, messages: [EnqueueMessage], userInteractive: Bool) -> Signal<[(Bool, EnqueueMessage)], NoError> {
|
|
var hasMedia = false
|
|
loop: for message in messages {
|
|
switch message {
|
|
case let .message(_, _, media, _, _):
|
|
if media != nil {
|
|
hasMedia = true
|
|
break loop
|
|
}
|
|
case .forward:
|
|
break
|
|
}
|
|
}
|
|
|
|
if !hasMedia {
|
|
return .single(messages.map { (true, $0) })
|
|
}
|
|
|
|
var signals: [Signal<(Bool, EnqueueMessage), NoError>] = []
|
|
for message in messages {
|
|
switch message {
|
|
case let .message(text, attributes, media, replyToMessageId, localGroupingKey):
|
|
if let media = media {
|
|
signals.append(opportunisticallyTransformMessageWithMedia(network: network, postbox: postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, media: media, userInteractive: userInteractive) |> map { result -> (Bool, EnqueueMessage) in
|
|
if let result = result {
|
|
return (true, .message(text: text, attributes: attributes, media: result, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey))
|
|
} else {
|
|
return (false, .message(text: text, attributes: attributes, media: media, replyToMessageId: replyToMessageId, localGroupingKey: localGroupingKey))
|
|
}
|
|
})
|
|
} else {
|
|
signals.append(.single((false, message)))
|
|
}
|
|
case .forward:
|
|
signals.append(.single((false, message)))
|
|
}
|
|
}
|
|
return combineLatest(signals)
|
|
}
|
|
|
|
public func enqueueMessages(account: Account, peerId: PeerId, messages: [EnqueueMessage]) -> Signal<[MessageId?], NoError> {
|
|
let signal: Signal<[(Bool, EnqueueMessage)], NoError>
|
|
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia {
|
|
signal = opportunisticallyTransformOutgoingMedia(network: account.network, postbox: account.postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messages: messages, userInteractive: true)
|
|
} else {
|
|
signal = .single(messages.map { (false, $0) })
|
|
}
|
|
return signal
|
|
|> mapToSignal { messages -> Signal<[MessageId?], NoError> in
|
|
return account.postbox.modify { modifier -> [MessageId?] in
|
|
return enqueueMessages(modifier: modifier, account: account, peerId: peerId, messages: messages)
|
|
}
|
|
}
|
|
}
|
|
|
|
public func enqueueMessagesToMultiplePeers(account: Account, peerIds: [PeerId], messages: [EnqueueMessage]) -> Signal<[MessageId], NoError> {
|
|
let signal: Signal<[(Bool, EnqueueMessage)], NoError>
|
|
if let transformOutgoingMessageMedia = account.transformOutgoingMessageMedia {
|
|
signal = opportunisticallyTransformOutgoingMedia(network: account.network, postbox: account.postbox, transformOutgoingMessageMedia: transformOutgoingMessageMedia, messages: messages, userInteractive: true)
|
|
} else {
|
|
signal = .single(messages.map { (false, $0) })
|
|
}
|
|
return signal
|
|
|> mapToSignal { messages -> Signal<[MessageId], NoError> in
|
|
return account.postbox.modify { modifier -> [MessageId] in
|
|
var messageIds: [MessageId] = []
|
|
for peerId in peerIds {
|
|
for id in enqueueMessages(modifier: modifier, account: account, peerId: peerId, messages: messages) {
|
|
if let id = id {
|
|
messageIds.append(id)
|
|
}
|
|
}
|
|
}
|
|
return messageIds
|
|
}
|
|
}
|
|
}
|
|
|
|
public func resendMessages(account: Account, messageIds: [MessageId]) -> Signal<Void, NoError> {
|
|
return account.postbox.modify { modifier -> Void in
|
|
var removeMessageIds: [MessageId] = []
|
|
for (peerId, ids) in messagesIdsGroupedByPeerId(messageIds) {
|
|
var messages: [EnqueueMessage] = []
|
|
for id in ids {
|
|
if let message = modifier.getMessage(id), !message.flags.contains(.Incoming) {
|
|
removeMessageIds.append(id)
|
|
|
|
var replyToMessageId: MessageId?
|
|
for attribute in message.attributes {
|
|
if let attribute = attribute as? ReplyMessageAttribute {
|
|
replyToMessageId = attribute.messageId
|
|
}
|
|
}
|
|
|
|
messages.append(.message(text: message.text, attributes: message.attributes, media: message.media.first, replyToMessageId: replyToMessageId, localGroupingKey: message.groupingKey))
|
|
}
|
|
}
|
|
let _ = enqueueMessages(modifier: modifier, account: account, peerId: peerId, messages: messages.map { (false, $0) })
|
|
}
|
|
modifier.deleteMessages(removeMessageIds)
|
|
}
|
|
}
|
|
|
|
func enqueueMessages(modifier: Modifier, account: Account, peerId: PeerId, messages: [(Bool, EnqueueMessage)]) -> [MessageId?] {
|
|
if let peer = modifier.getPeer(peerId), let accountPeer = modifier.getPeer(account.peerId) {
|
|
var storeMessages: [StoreMessage] = []
|
|
var timestamp = Int32(account.network.context.globalTime())
|
|
switch peerId.namespace {
|
|
case Namespaces.Peer.CloudChannel, Namespaces.Peer.CloudGroup, Namespaces.Peer.CloudUser:
|
|
if let topIndex = modifier.getTopPeerMessageIndex(peerId: peerId, namespace: Namespaces.Message.Cloud) {
|
|
timestamp = max(timestamp, topIndex.timestamp)
|
|
}
|
|
default:
|
|
break
|
|
}
|
|
|
|
var addedHashtags: [String] = []
|
|
|
|
var localGroupingKeyBySourceKey: [Int64: Int64] = [:]
|
|
|
|
var globallyUniqueIds: [Int64] = []
|
|
for (transformedMedia, message) in messages {
|
|
var attributes: [MessageAttribute] = []
|
|
var flags = StoreMessageFlags()
|
|
flags.insert(.Unsent)
|
|
|
|
var randomId: Int64 = 0
|
|
arc4random_buf(&randomId, 8)
|
|
var infoFlags = OutgoingMessageInfoFlags()
|
|
if transformedMedia {
|
|
infoFlags.insert(.transformedMedia)
|
|
}
|
|
attributes.append(OutgoingMessageInfoAttribute(uniqueId: randomId, flags: infoFlags))
|
|
globallyUniqueIds.append(randomId)
|
|
|
|
switch message {
|
|
case let .message(text, requestedAttributes, media, replyToMessageId, localGroupingKey):
|
|
if let peer = peer as? TelegramSecretChat {
|
|
var isAction = false
|
|
if let _ = media as? TelegramMediaAction {
|
|
isAction = true
|
|
}
|
|
if let messageAutoremoveTimeout = peer.messageAutoremoveTimeout, !isAction {
|
|
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil))
|
|
}
|
|
}
|
|
|
|
attributes.append(contentsOf: filterMessageAttributesForOutgoingMessage(requestedAttributes))
|
|
|
|
if let replyToMessageId = replyToMessageId {
|
|
attributes.append(ReplyMessageAttribute(messageId: replyToMessageId))
|
|
}
|
|
var mediaList: [Media] = []
|
|
if let media = media {
|
|
mediaList.append(media)
|
|
}
|
|
|
|
if let file = media as? TelegramMediaFile, file.isVoice {
|
|
if peerId.namespace == Namespaces.Peer.CloudUser || peerId.namespace == Namespaces.Peer.CloudGroup {
|
|
attributes.append(ConsumableContentMessageAttribute(consumed: false))
|
|
}
|
|
}
|
|
if let peer = peer as? TelegramChannel {
|
|
switch peer.info {
|
|
case let .broadcast(info):
|
|
attributes.append(ViewCountMessageAttribute(count: 1))
|
|
if info.flags.contains(.messagesShouldHaveSignatures) {
|
|
attributes.append(AuthorSignatureMessageAttribute(signature: accountPeer.displayTitle))
|
|
}
|
|
case .group:
|
|
break
|
|
}
|
|
}
|
|
|
|
var entitiesAttribute: TextEntitiesMessageAttribute?
|
|
for attribute in attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
entitiesAttribute = attribute
|
|
var maybeNsText: NSString?
|
|
for entity in attribute.entities {
|
|
if case .Hashtag = entity.type {
|
|
let nsText: NSString
|
|
if let maybeNsText = maybeNsText {
|
|
nsText = maybeNsText
|
|
} else {
|
|
nsText = text as NSString
|
|
maybeNsText = nsText
|
|
}
|
|
var entityRange = NSRange(location: entity.range.lowerBound, length: entity.range.upperBound - entity.range.lowerBound)
|
|
if entityRange.location + entityRange.length > nsText.length {
|
|
entityRange.location = max(0, nsText.length - entityRange.length)
|
|
entityRange.length = nsText.length - entityRange.location
|
|
}
|
|
if entityRange.length > 1 {
|
|
entityRange.location += 1
|
|
entityRange.length -= 1
|
|
let hashtag = nsText.substring(with: entityRange)
|
|
addedHashtags.append(hashtag)
|
|
}
|
|
}
|
|
}
|
|
break
|
|
}
|
|
}
|
|
|
|
let authorId: PeerId?
|
|
if let peer = peer as? TelegramChannel, case let .broadcast(info) = peer.info, !info.flags.contains(.messagesShouldHaveSignatures) {
|
|
authorId = peer.id
|
|
} else {
|
|
authorId = account.peerId
|
|
}
|
|
|
|
let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: mediaList, textEntities: entitiesAttribute?.entities)
|
|
|
|
var localTags: LocalMessageTags = []
|
|
for media in mediaList {
|
|
if let media = media as? TelegramMediaMap, media.liveBroadcastingTimeout != nil {
|
|
localTags.insert(.OutgoingLiveLocation)
|
|
}
|
|
}
|
|
|
|
storeMessages.append(StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: localTags, forwardInfo: nil, authorId: authorId, text: text, attributes: attributes, media: mediaList))
|
|
case let .forward(source, grouping):
|
|
let sourceMessage = modifier.getMessage(source)
|
|
if let sourceMessage = sourceMessage, let author = sourceMessage.author ?? sourceMessage.peers[sourceMessage.id.peerId] {
|
|
if let peer = peer as? TelegramSecretChat {
|
|
var isAction = false
|
|
for media in sourceMessage.media {
|
|
if let _ = media as? TelegramMediaAction {
|
|
isAction = true
|
|
break
|
|
}
|
|
}
|
|
if let messageAutoremoveTimeout = peer.messageAutoremoveTimeout, !isAction {
|
|
attributes.append(AutoremoveTimeoutMessageAttribute(timeout: messageAutoremoveTimeout, countdownBeginTime: nil))
|
|
}
|
|
}
|
|
|
|
var forwardInfo: StoreMessageForwardInfo?
|
|
|
|
if peerId.namespace != Namespaces.Peer.SecretChat {
|
|
attributes.append(ForwardSourceInfoAttribute(messageId: sourceMessage.id))
|
|
|
|
if peerId == account.peerId {
|
|
attributes.append(SourceReferenceMessageAttribute(messageId: sourceMessage.id))
|
|
}
|
|
|
|
attributes.append(contentsOf: filterMessageAttributesForForwardedMessage(sourceMessage.attributes))
|
|
if let sourceForwardInfo = sourceMessage.forwardInfo {
|
|
forwardInfo = StoreMessageForwardInfo(authorId: sourceForwardInfo.author.id, sourceId: sourceForwardInfo.source?.id, sourceMessageId: sourceForwardInfo.sourceMessageId, date: sourceForwardInfo.date, authorSignature: sourceForwardInfo.authorSignature)
|
|
} else {
|
|
if sourceMessage.id.peerId != account.peerId {
|
|
var sourceId: PeerId? = nil
|
|
var sourceMessageId: MessageId? = nil
|
|
if let peer = messageMainPeer(sourceMessage) as? TelegramChannel, case .broadcast = peer.info {
|
|
sourceId = peer.id
|
|
sourceMessageId = sourceMessage.id
|
|
}
|
|
|
|
var authorSignature: String?
|
|
for attribute in sourceMessage.attributes {
|
|
if let attribute = attribute as? AuthorSignatureMessageAttribute {
|
|
authorSignature = attribute.signature
|
|
break
|
|
}
|
|
}
|
|
|
|
forwardInfo = StoreMessageForwardInfo(authorId: author.id, sourceId: sourceId, sourceMessageId: sourceMessageId, date: sourceMessage.timestamp, authorSignature: authorSignature)
|
|
} else {
|
|
forwardInfo = nil
|
|
}
|
|
}
|
|
} else {
|
|
attributes.append(contentsOf: filterMessageAttributesForOutgoingMessage(sourceMessage.attributes))
|
|
}
|
|
|
|
let authorId: PeerId?
|
|
if let peer = peer as? TelegramChannel, case let .broadcast(info) = peer.info, !info.flags.contains(.messagesShouldHaveSignatures) {
|
|
authorId = peer.id
|
|
} else {
|
|
authorId = account.peerId
|
|
}
|
|
|
|
var entitiesAttribute: TextEntitiesMessageAttribute?
|
|
for attribute in attributes {
|
|
if let attribute = attribute as? TextEntitiesMessageAttribute {
|
|
entitiesAttribute = attribute
|
|
break
|
|
}
|
|
}
|
|
|
|
let (tags, globalTags) = tagsForStoreMessage(incoming: false, attributes: attributes, media: sourceMessage.media, textEntities: entitiesAttribute?.entities)
|
|
|
|
let localGroupingKey: Int64?
|
|
switch grouping {
|
|
case .none:
|
|
localGroupingKey = nil
|
|
case .auto:
|
|
if let groupingKey = sourceMessage.groupingKey {
|
|
if let generatedKey = localGroupingKeyBySourceKey[groupingKey] {
|
|
localGroupingKey = generatedKey
|
|
} else {
|
|
let generatedKey = arc4random64()
|
|
localGroupingKeyBySourceKey[groupingKey] = generatedKey
|
|
localGroupingKey = generatedKey
|
|
}
|
|
} else {
|
|
localGroupingKey = nil
|
|
}
|
|
}
|
|
|
|
storeMessages.append(StoreMessage(peerId: peerId, namespace: Namespaces.Message.Local, globallyUniqueId: randomId, groupingKey: localGroupingKey, timestamp: timestamp, flags: flags, tags: tags, globalTags: globalTags, localTags: [], forwardInfo: forwardInfo, authorId: authorId, text: sourceMessage.text, attributes: attributes, media: sourceMessage.media))
|
|
}
|
|
}
|
|
}
|
|
var messageIds: [MessageId?] = []
|
|
if !storeMessages.isEmpty {
|
|
let globallyUniqueIdToMessageId = modifier.addMessages(storeMessages, location: .Random)
|
|
for globallyUniqueId in globallyUniqueIds {
|
|
messageIds.append(globallyUniqueIdToMessageId[globallyUniqueId])
|
|
}
|
|
}
|
|
for hashtag in addedHashtags {
|
|
addRecentlyUsedHashtag(modifier: modifier, string: hashtag)
|
|
}
|
|
return messageIds
|
|
} else {
|
|
return []
|
|
}
|
|
}
|